From 14807be74e9abe373e098f716f2be4245b10e9ac Mon Sep 17 00:00:00 2001 From: rosspeili Date: Wed, 3 Jun 2026 12:15:41 +0300 Subject: [PATCH 1/2] style: format codebase with Black (#153) Repo-wide formatting pass on 16 files; no logic changes. --- examples/build_dataset_demo.py | 28 ++++----- examples/mica_rag_flow.py | 4 +- examples/ollama_skills_test.py | 57 +++++++++++-------- examples/pii_guardrail_flow.py | 16 ++++-- examples/prompt_compression_demo.py | 13 +++-- skills/compliance/pii_masker/skill.py | 31 +++++----- skills/compliance/tos_evaluator/skill.py | 47 ++++++++++----- .../synthetic_generator/skill.py | 47 ++++++--------- skills/optimization/prompt_rewriter/skill.py | 35 +++++++++--- skillware/core/loader.py | 6 +- tests/skills/compliance/test_pii_masker.py | 49 +++++++++++----- tests/skills/compliance/test_tos_evaluator.py | 16 ++++-- .../test_synthetic_generator.py | 24 ++++---- .../optimization/test_prompt_rewriter.py | 6 +- tests/test_loader.py | 45 +++++++++------ tests/test_skill_issuer.py | 32 ++++++----- 16 files changed, 271 insertions(+), 185 deletions(-) diff --git a/examples/build_dataset_demo.py b/examples/build_dataset_demo.py index 4cbcd7c..bc26164 100644 --- a/examples/build_dataset_demo.py +++ b/examples/build_dataset_demo.py @@ -9,10 +9,8 @@ def main(): load_env_file() print("Loading Synthetic Data Generator Skill...") - skill_bundle = SkillLoader.load_skill( - "data_engineering/synthetic_generator" - ) - SyntheticGeneratorSkill = skill_bundle['module'].SyntheticGeneratorSkill + skill_bundle = SkillLoader.load_skill("data_engineering/synthetic_generator") + SyntheticGeneratorSkill = skill_bundle["module"].SyntheticGeneratorSkill generator = SyntheticGeneratorSkill() @@ -27,21 +25,23 @@ def main(): "scenarios like obscure comorbidities fighting with dual-insurance." ) - result = generator.execute({ - "domain": "medical_coding_disputes", - "num_samples": 10, - "entropy_temperature": 0.9, - "diversity_prompt": prompt, - "model_provider": "gemini", - "model_name": "gemini-2.5-flash-lite" - }) + result = generator.execute( + { + "domain": "medical_coding_disputes", + "num_samples": 10, + "entropy_temperature": 0.9, + "diversity_prompt": prompt, + "model_provider": "gemini", + "model_name": "gemini-2.5-flash-lite", + } + ) elapsed = time.time() - start_time print(f"Time Taken: {elapsed:.2f} seconds") if result.get("status") == "success": - score = result.get('entropy_score') - samples = result.get('samples', []) + score = result.get("entropy_score") + samples = result.get("samples", []) print(f"āœ… Success! Entropy Score: {score}") print(f"Extracted {len(samples)} samples out of requested 10.") dataset.extend(samples) diff --git a/examples/mica_rag_flow.py b/examples/mica_rag_flow.py index 8c5f019..dc15886 100644 --- a/examples/mica_rag_flow.py +++ b/examples/mica_rag_flow.py @@ -33,9 +33,7 @@ def main(): Wait for the response before making your final compliant determination. """ - user_query = ( - "How do I get an authorization to be a crypto-asset service provider (CASP) in the EU?" - ) + user_query = "How do I get an authorization to be a crypto-asset service provider (CASP) in the EU?" print(f"\n[User]: {user_query}") print("-" * 50) diff --git a/examples/ollama_skills_test.py b/examples/ollama_skills_test.py index b04c037..4ef86bb 100644 --- a/examples/ollama_skills_test.py +++ b/examples/ollama_skills_test.py @@ -14,7 +14,11 @@ def load_and_initialize_skill(path): skill_class = None for attr_name in dir(bundle["module"]): attr = getattr(bundle["module"], attr_name) - if isinstance(attr, type) and issubclass(attr, BaseSkill) and attr is not BaseSkill: + if ( + isinstance(attr, type) + and issubclass(attr, BaseSkill) + and attr is not BaseSkill + ): skill_class = attr break if not skill_class: @@ -26,7 +30,7 @@ def load_and_initialize_skill(path): SKILL_PATHS = [ "finance/wallet_screening", "office/pdf_form_filler", - "optimization/prompt_rewriter" + "optimization/prompt_rewriter", ] skills_registry = {} @@ -63,7 +67,9 @@ def load_and_initialize_skill(path): Once you have the results, provide your final answer to the user. Here are the available skills and their instructions: -""" + "\n---\n".join(tool_descriptions) +""" + "\n---\n".join( + tool_descriptions +) # 3. Setup Ollama Chat model_name = "llama3" @@ -76,17 +82,14 @@ def load_and_initialize_skill(path): messages = [ {"role": "system", "content": combined_system_prompt}, - {"role": "user", "content": user_query} + {"role": "user", "content": user_query}, ] print(f"\nšŸ¤– Calling Ollama model: {model_name}...") # 4. Handle Conversation & Tool Parsing Loop for _ in range(5): # Max steps to prevent infinite loops - response = ollama.chat( - model=model_name, - messages=messages - ) + response = ollama.chat(model=model_name, messages=messages) message_content = response.get("message", {}).get("content", "") print(f"\n[Model Output]:\n{message_content}") @@ -115,26 +118,32 @@ def load_and_initialize_skill(path): print(f"šŸ“¤ Result generated ({len(result_str)} bytes)") # Send the result back to the model masquerading as a system/user update - messages.append({ - "role": "user", - "content": ( - f"SYSTEM RESPONSE (Result from {fn_name}):\n" - f"```json\n{result_str}\n```\n" - "Please continue based on this result." - ) - }) + messages.append( + { + "role": "user", + "content": ( + f"SYSTEM RESPONSE (Result from {fn_name}):\n" + f"```json\n{result_str}\n```\n" + "Please continue based on this result." + ), + } + ) else: print(f"Unknown function requested: {fn_name}") - messages.append({ - "role": "user", - "content": f"SYSTEM ERROR: Tool '{fn_name}' not found." - }) + messages.append( + { + "role": "user", + "content": f"SYSTEM ERROR: Tool '{fn_name}' not found.", + } + ) except json.JSONDecodeError: print("Failed to decode JSON from tool call block.") - messages.append({ - "role": "user", - "content": "SYSTEM ERROR: Invalid JSON format. Please output valid JSON." - }) + messages.append( + { + "role": "user", + "content": "SYSTEM ERROR: Invalid JSON format. Please output valid JSON.", + } + ) else: # If no tool block was found, assume the agent is done and providing final answer print("\nšŸ’¬ Final Answer reached. End of execution.") diff --git a/examples/pii_guardrail_flow.py b/examples/pii_guardrail_flow.py index 1fd220e..13aef30 100644 --- a/examples/pii_guardrail_flow.py +++ b/examples/pii_guardrail_flow.py @@ -22,17 +22,21 @@ def simulate_agentic_flow(): # 2. Load the Privacy Firewall Skill print("[System] Loading compliance/pii_masker skill...") - pii_skill = SkillLoader.load_skill("compliance/pii_masker")["module"].PIIMaskerSkill() + pii_skill = SkillLoader.load_skill("compliance/pii_masker")[ + "module" + ].PIIMaskerSkill() # 3. Intercept and Sanitize (Redact mode) print("[System] Intercepting prompt...") # NOTE: This requires Ollama running locally with the arpacorp/micro-f1-mask model. # If Ollama is not running, the skill falls back to returning the original string. - result = pii_skill.execute({ - "text": raw_user_input, - "mode": "redact", # Change to "mask" to see entity tags like [PERSON_1] instead of XXXX - "ollama_url": "http://localhost:11434" - }) + result = pii_skill.execute( + { + "text": raw_user_input, + "mode": "redact", # Change to "mask" to see entity tags like [PERSON_1] instead of XXXX + "ollama_url": "http://localhost:11434", + } + ) scrubbed_input = result["sanitized_text"] metadata = result["metadata"] diff --git a/examples/prompt_compression_demo.py b/examples/prompt_compression_demo.py index 48f9da8..3302f59 100644 --- a/examples/prompt_compression_demo.py +++ b/examples/prompt_compression_demo.py @@ -5,7 +5,7 @@ def run_demo(): print("Loading Prompt Token Rewriter...") # Load the skill via the global loader just like an LLM agent would skill_bundle = SkillLoader.load_skill("optimization/prompt_rewriter") - skill_instance = skill_bundle['module'].PromptRewriter() + skill_instance = skill_bundle["module"].PromptRewriter() massive_prompt = ( "Hello, could you please make sure to read this entirely? " @@ -16,13 +16,14 @@ def run_demo(): print(f"\n[RAW TEXT]: {massive_prompt}") # Execute the offline compression logic - result = skill_instance.execute({ - "raw_text": massive_prompt, - "compression_aggression": "high" - }) + result = skill_instance.execute( + {"raw_text": massive_prompt, "compression_aggression": "high"} + ) print(f"\n[COMPRESSED TEXT]: {result['compressed_text']}") - print(f"[REDUCTION]: {result['original_tokens']} tokens -> {result['new_tokens']} tokens") + print( + f"[REDUCTION]: {result['original_tokens']} tokens -> {result['new_tokens']} tokens" + ) print(f"[SAVED]: {result['tokens_saved']} tokens") diff --git a/skills/compliance/pii_masker/skill.py b/skills/compliance/pii_masker/skill.py index 0c57237..4142350 100644 --- a/skills/compliance/pii_masker/skill.py +++ b/skills/compliance/pii_masker/skill.py @@ -11,10 +11,7 @@ class PIIMaskerSkill(BaseSkill): @property def manifest(self) -> Dict[str, Any]: - return { - "name": "compliance/pii_masker", - "version": "0.1.0" - } + return {"name": "compliance/pii_masker", "version": "0.1.0"} def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: text = params.get("text", "") @@ -25,7 +22,7 @@ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: sanitized_text = self._apply_mode(sanitized_text, mode) # Build unique entity types list - entities = list(set([re.sub(r'_[0-9]+$', '', e) for e in detected_entities])) + entities = list(set([re.sub(r"_[0-9]+$", "", e) for e in detected_entities])) return { "sanitized_text": sanitized_text, @@ -33,17 +30,21 @@ def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: "detected_entities": entities, "entity_count": len(detected_entities), "security_level": "local-only", - "model": "arpacorp/micro-f1-mask" - } + "model": "arpacorp/micro-f1-mask", + }, } def _call_ollama(self, text: str, endpoint: str) -> Tuple[str, List[str]]: try: - response = requests.post(f"{endpoint}/api/generate", json={ - "model": "arpacorp/micro-f1-mask", - "prompt": text, - "stream": False - }, timeout=30) + response = requests.post( + f"{endpoint}/api/generate", + json={ + "model": "arpacorp/micro-f1-mask", + "prompt": text, + "stream": False, + }, + timeout=30, + ) if response.status_code == 200: result_text = response.json().get("response", text) else: @@ -56,7 +57,7 @@ def _call_ollama(self, text: str, endpoint: str) -> Tuple[str, List[str]]: result_text = text # Detect entities in the response - detected = re.findall(r'\[([A-Z_]+(?:_[0-9]+)?)\]', result_text) + detected = re.findall(r"\[([A-Z_]+(?:_[0-9]+)?)\]", result_text) return result_text, detected def _apply_mode(self, text: str, mode: str) -> str: @@ -64,14 +65,14 @@ def _apply_mode(self, text: str, mode: str) -> str: return text # Pattern to catch [DOCUMENT], [PERSON_1], etc. - pattern = r'\[[A-Z_]+(?:_[0-9]+)?\]' + pattern = r"\[[A-Z_]+(?:_[0-9]+)?\]" if mode == "redact": return re.sub(pattern, "XXXX", text) elif mode == "remove": # Replace token and any immediate preceding/following spaces safely # A simple sub is sufficient. Cleaning up double spaces. text = re.sub(pattern, "", text) - text = re.sub(r'\s+', ' ', text).strip() + text = re.sub(r"\s+", " ", text).strip() return text return text diff --git a/skills/compliance/tos_evaluator/skill.py b/skills/compliance/tos_evaluator/skill.py index f974bd4..fc3a404 100644 --- a/skills/compliance/tos_evaluator/skill.py +++ b/skills/compliance/tos_evaluator/skill.py @@ -213,7 +213,9 @@ def _evaluate_robots( try: response = self.session.get(robots_url, timeout=10) if response.status_code >= 400: - assessment["reason"] = f"robots.txt returned HTTP {response.status_code}." + assessment["reason"] = ( + f"robots.txt returned HTTP {response.status_code}." + ) return assessment parser = RobotFileParser() @@ -266,13 +268,20 @@ def _discover_policy_pages( else: candidates[item["url"]] = item - ordered = sorted(candidates.values(), key=lambda item: item["score"], reverse=True) + ordered = sorted( + candidates.values(), key=lambda item: item["score"], reverse=True + ) return {"candidates": ordered[:max_terms_pages]} - def _extract_candidate_links(self, page_url: str, origin: str) -> List[Dict[str, Any]]: + def _extract_candidate_links( + self, page_url: str, origin: str + ) -> List[Dict[str, Any]]: links: List[Dict[str, Any]] = [] response = self._safe_get(page_url, timeout=10) - if not response or "html" not in response.headers.get("Content-Type", "").lower(): + if ( + not response + or "html" not in response.headers.get("Content-Type", "").lower() + ): return links soup = BeautifulSoup(response.text[:300000], "html.parser") @@ -449,7 +458,10 @@ def _should_use_llm( return False if robots_assessment.get("can_fetch") is False: return False - return bool(tos_assessment.get("matched_clauses") or tos_assessment["status"] == "caution") + return bool( + tos_assessment.get("matched_clauses") + or tos_assessment["status"] == "caution" + ) def _run_llm_evaluator( self, normalized: Dict[str, Any], tos_assessment: Dict[str, Any] @@ -512,26 +524,33 @@ def _build_final_result( verdict = "INSUFFICIENT_EVIDENCE" confidence_score = 0.35 reason = "Insufficient policy evidence to safely approve the requested action." - recommended_next_step = "Review the discovered policy pages manually before proceeding." + recommended_next_step = ( + "Review the discovered policy pages manually before proceeding." + ) if robots_assessment.get("can_fetch") is False: verdict = "UNSAFE" confidence_score = 0.98 reason = robots_assessment["reason"] - recommended_next_step = ( - "Do not automate access to this path unless you have explicit permission." - ) + recommended_next_step = "Do not automate access to this path unless you have explicit permission." elif tos_assessment["status"] == "blocked": verdict = "UNSAFE" confidence_score = 0.9 reason = tos_assessment["summary"] - recommended_next_step = "Avoid the requested action or obtain explicit written permission." + recommended_next_step = ( + "Avoid the requested action or obtain explicit written permission." + ) elif tos_assessment["status"] == "caution": verdict = "CAUTION" confidence_score = 0.65 reason = tos_assessment["summary"] - recommended_next_step = "Prefer an official API or documented integration path if one exists." - elif tos_assessment["status"] == "allowed" and robots_assessment.get("can_fetch") is not False: + recommended_next_step = ( + "Prefer an official API or documented integration path if one exists." + ) + elif ( + tos_assessment["status"] == "allowed" + and robots_assessment.get("can_fetch") is not False + ): verdict = "SAFE" confidence_score = 0.72 reason = tos_assessment["summary"] @@ -575,7 +594,9 @@ def _build_final_result( "tos_assessment": tos_assessment, "llm_assessment": llm_assessment or {"status": "not_used"}, "discovered_policy_urls": { - "candidates": [item["url"] for item in policy_candidates.get("candidates", [])] + "candidates": [ + item["url"] for item in policy_candidates.get("candidates", []) + ] }, "evidence": evidence, } diff --git a/skills/data_engineering/synthetic_generator/skill.py b/skills/data_engineering/synthetic_generator/skill.py index 1006ab6..a762a3f 100644 --- a/skills/data_engineering/synthetic_generator/skill.py +++ b/skills/data_engineering/synthetic_generator/skill.py @@ -18,6 +18,7 @@ def manifest(self) -> Dict[str, Any]: manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml") if os.path.exists(manifest_path): import yaml + with open(manifest_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) return {} @@ -35,16 +36,11 @@ def _calculate_entropy_score(self, text: str) -> float: # Scaled for readability return round(min(ratio * 1.5, 1.0), 3) - def _call_gemini( - self, prompt: str, temperature: float, model_name: str - ) -> str: + def _call_gemini(self, prompt: str, temperature: float, model_name: str) -> str: import google.genai as genai from google.genai import types - api_key = ( - self.config.get("GOOGLE_API_KEY") - or os.environ.get("GOOGLE_API_KEY") - ) + api_key = self.config.get("GOOGLE_API_KEY") or os.environ.get("GOOGLE_API_KEY") client = genai.Client(api_key=api_key) response = client.models.generate_content( model=model_name, @@ -53,31 +49,28 @@ def _call_gemini( ) return response.text - def _call_anthropic( - self, prompt: str, temperature: float, model_name: str - ) -> str: + def _call_anthropic(self, prompt: str, temperature: float, model_name: str) -> str: import anthropic - api_key = ( - self.config.get("ANTHROPIC_API_KEY") - or os.environ.get("ANTHROPIC_API_KEY") + + api_key = self.config.get("ANTHROPIC_API_KEY") or os.environ.get( + "ANTHROPIC_API_KEY" ) client = anthropic.Anthropic(api_key=api_key) message = client.messages.create( model=model_name, max_tokens=4096, temperature=temperature, - messages=[{"role": "user", "content": prompt}] + messages=[{"role": "user", "content": prompt}], ) return message.content[0].text - def _call_ollama( - self, prompt: str, temperature: float, model_name: str - ) -> str: + def _call_ollama(self, prompt: str, temperature: float, model_name: str) -> str: import ollama + response = ollama.chat( model=model_name, messages=[{"role": "user", "content": prompt}], - options={"temperature": temperature} + options={"temperature": temperature}, ) return response.get("message", {}).get("content", "") @@ -107,21 +100,15 @@ def execute(self, params: Dict[str, Any]) -> Any: try: if provider == "gemini": - raw_text = self._call_gemini( - system_prompt, temperature, model_name - ) + raw_text = self._call_gemini(system_prompt, temperature, model_name) elif provider == "anthropic": - raw_text = self._call_anthropic( - system_prompt, temperature, model_name - ) + raw_text = self._call_anthropic(system_prompt, temperature, model_name) else: - raw_text = self._call_ollama( - system_prompt, temperature, model_name - ) + raw_text = self._call_ollama(system_prompt, temperature, model_name) except Exception as e: return { "status": "error", - "message": f"LLM Call Failed via {provider}: {str(e)}" + "message": f"LLM Call Failed via {provider}: {str(e)}", } samples = [] @@ -141,7 +128,7 @@ def execute(self, params: Dict[str, Any]) -> Any: return { "status": "error", "message": f"Parsing failed: {e}", - "raw_output": raw_text + "raw_output": raw_text, } all_text = " ".join([str(s) for s in samples]) @@ -152,5 +139,5 @@ def execute(self, params: Dict[str, Any]) -> Any: "entropy_score": score, "status": "success", "provider_used": provider, - "samples_generated": len(samples) + "samples_generated": len(samples), } diff --git a/skills/optimization/prompt_rewriter/skill.py b/skills/optimization/prompt_rewriter/skill.py index fc7e5e9..7e28421 100644 --- a/skills/optimization/prompt_rewriter/skill.py +++ b/skills/optimization/prompt_rewriter/skill.py @@ -30,27 +30,44 @@ def execute(self, params: Dict[str, Any]) -> Any: original_tokens = self._estimate_tokens(raw_text) # Level 1: Standardize Whitespace (Low Aggression) - compressed = re.sub(r'\s+', ' ', raw_text).strip() + compressed = re.sub(r"\s+", " ", raw_text).strip() # Level 2: Remove Filler Words (Medium Aggression) if aggression in ["medium", "high"]: fillers = [ - "please", "could you", "would you", "kindly", "make sure to", - "ensure that", "I want you to", "can you" + "please", + "could you", + "would you", + "kindly", + "make sure to", + "ensure that", + "I want you to", + "can you", ] for filler in fillers: - compressed = re.compile(re.escape(filler), re.IGNORECASE).sub("", compressed) - compressed = re.sub(r'\s+', ' ', compressed).strip() + compressed = re.compile(re.escape(filler), re.IGNORECASE).sub( + "", compressed + ) + compressed = re.sub(r"\s+", " ", compressed).strip() # Level 3: Intense Vowel/Punctuation Dropping (High Aggression) if aggression == "high": # Remove non-essential punctuation - compressed = re.sub(r'[^\w\s\.\-]', '', compressed) + compressed = re.sub(r"[^\w\s\.\-]", "", compressed) # Remove common extremely high frequency stop words naively - stop_words = [" a ", " an ", " the ", " is ", " that ", " this ", " and ", " to "] + stop_words = [ + " a ", + " an ", + " the ", + " is ", + " that ", + " this ", + " and ", + " to ", + ] for word in stop_words: compressed = re.compile(word, re.IGNORECASE).sub(" ", compressed) - compressed = re.sub(r'\s+', ' ', compressed).strip() + compressed = re.sub(r"\s+", " ", compressed).strip() new_tokens = self._estimate_tokens(compressed) @@ -58,5 +75,5 @@ def execute(self, params: Dict[str, Any]) -> Any: "compressed_text": compressed, "original_tokens": original_tokens, "new_tokens": new_tokens, - "tokens_saved": original_tokens - new_tokens + "tokens_saved": original_tokens - new_tokens, } diff --git a/skillware/core/loader.py b/skillware/core/loader.py index 9809ff6..c333e95 100644 --- a/skillware/core/loader.py +++ b/skillware/core/loader.py @@ -43,7 +43,11 @@ def _env_skill_roots() -> List[Path]: raw = os.environ.get(SKILLWARE_SKILL_PATH_ENV, "").strip() if not raw: return [] - return [Path(entry).expanduser().resolve() for entry in raw.split(os.pathsep) if entry.strip()] + return [ + Path(entry).expanduser().resolve() + for entry in raw.split(os.pathsep) + if entry.strip() + ] @staticmethod def _cwd_skill_roots() -> List[Path]: diff --git a/tests/skills/compliance/test_pii_masker.py b/tests/skills/compliance/test_pii_masker.py index 237d944..3222a96 100644 --- a/tests/skills/compliance/test_pii_masker.py +++ b/tests/skills/compliance/test_pii_masker.py @@ -14,28 +14,49 @@ def test_pii_masker_modes(mocker): skill = skill_class() # Mock the Ollama API call - mock_response = "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified." + mock_response = ( + "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified." + ) # The _call_ollama method returns (sanitized_text, [entities]) - mocker.patch.object(skill, '_call_ollama', return_value=(mock_response, ["PERSON_1", "CRYPTO_ADDRESS", "EMAIL"])) + mocker.patch.object( + skill, + "_call_ollama", + return_value=(mock_response, ["PERSON_1", "CRYPTO_ADDRESS", "EMAIL"]), + ) # Test Mask mode (default) - result_mask = skill.execute({"text": "Hello John Doe, your wallet 0xabc and john@doe.com have been verified."}) - expected_text = "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified." + result_mask = skill.execute( + { + "text": "Hello John Doe, your wallet 0xabc and john@doe.com have been verified." + } + ) + expected_text = ( + "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified." + ) assert result_mask["sanitized_text"] == expected_text assert "PERSON" in result_mask["metadata"]["detected_entities"] assert "CRYPTO_ADDRESS" in result_mask["metadata"]["detected_entities"] # Test Redact mode - result_redact = skill.execute({ - "text": "Hello John Doe, your wallet 0xabc and john@doe.com have been verified.", - "mode": "redact" - }) - assert result_redact["sanitized_text"] == "Hello XXXX, your wallet XXXX and XXXX have been verified." + result_redact = skill.execute( + { + "text": "Hello John Doe, your wallet 0xabc and john@doe.com have been verified.", + "mode": "redact", + } + ) + assert ( + result_redact["sanitized_text"] + == "Hello XXXX, your wallet XXXX and XXXX have been verified." + ) # Test Remove mode - result_remove = skill.execute({ - "text": "Hello John Doe, your wallet 0xabc and john@doe.com have been verified.", - "mode": "remove" - }) + result_remove = skill.execute( + { + "text": "Hello John Doe, your wallet 0xabc and john@doe.com have been verified.", + "mode": "remove", + } + ) # Remove simple mode removes the tags. It cleans spaces around them. - assert result_remove["sanitized_text"] == "Hello , your wallet and have been verified." + assert ( + result_remove["sanitized_text"] == "Hello , your wallet and have been verified." + ) diff --git a/tests/skills/compliance/test_tos_evaluator.py b/tests/skills/compliance/test_tos_evaluator.py index 3409f1a..85b87eb 100644 --- a/tests/skills/compliance/test_tos_evaluator.py +++ b/tests/skills/compliance/test_tos_evaluator.py @@ -74,7 +74,9 @@ def test_tos_evaluator_policy_clause_blocks_scraping(mock_get): def side_effect(url, **kwargs): if url.endswith("/robots.txt"): - return make_response(text="User-agent: *\nAllow: /\n", content_type="text/plain") + return make_response( + text="User-agent: *\nAllow: /\n", content_type="text/plain" + ) return make_response(text=html) mock_get.side_effect = side_effect @@ -106,7 +108,9 @@ def test_tos_evaluator_api_only_language_returns_caution(mock_get): def side_effect(url, **kwargs): if url.endswith("/robots.txt"): - return make_response(text="User-agent: *\nAllow: /\n", content_type="text/plain") + return make_response( + text="User-agent: *\nAllow: /\n", content_type="text/plain" + ) return make_response(text=html) mock_get.side_effect = side_effect @@ -137,7 +141,9 @@ def test_tos_evaluator_allowed_policy_can_return_safe(mock_get): def side_effect(url, **kwargs): if url.endswith("/robots.txt"): - return make_response(text="User-agent: *\nAllow: /\n", content_type="text/plain") + return make_response( + text="User-agent: *\nAllow: /\n", content_type="text/plain" + ) return make_response(text=html) mock_get.side_effect = side_effect @@ -168,7 +174,9 @@ def test_tos_evaluator_llm_fallback_is_mockable(mock_get): def side_effect(url, **kwargs): if url.endswith("/robots.txt"): - return make_response(text="User-agent: *\nAllow: /\n", content_type="text/plain") + return make_response( + text="User-agent: *\nAllow: /\n", content_type="text/plain" + ) return make_response(text=html) mock_get.side_effect = side_effect diff --git a/tests/skills/data_engineering/test_synthetic_generator.py b/tests/skills/data_engineering/test_synthetic_generator.py index e980bf5..9522d41 100644 --- a/tests/skills/data_engineering/test_synthetic_generator.py +++ b/tests/skills/data_engineering/test_synthetic_generator.py @@ -4,7 +4,7 @@ def test_synthetic_generator_manifest(): bundle = SkillLoader.load_skill("data_engineering/synthetic_generator") assert bundle["manifest"]["name"] == "data_engineering/synthetic_generator" - assert "entropy_temperature" in bundle["manifest"]["parameters"]['properties'] + assert "entropy_temperature" in bundle["manifest"]["parameters"]["properties"] def test_entropy_score(): @@ -28,21 +28,23 @@ def test_execute_success(mocker): bundle = SkillLoader.load_skill("data_engineering/synthetic_generator") skill = bundle["module"].SyntheticGeneratorSkill() - mock_json_response = '''```json + mock_json_response = """```json [ {"instruction": "x", "input": "y", "output": "z"} ] -```''' +```""" # Mock the gemini call to avoid hitting realistic endpoints - mocker.patch.object(skill, '_call_gemini', return_value=mock_json_response) - - result = skill.execute({ - "domain": "test domain", - "num_samples": 1, - "diversity_prompt": "be diverse", - "model_provider": "gemini" - }) + mocker.patch.object(skill, "_call_gemini", return_value=mock_json_response) + + result = skill.execute( + { + "domain": "test domain", + "num_samples": 1, + "diversity_prompt": "be diverse", + "model_provider": "gemini", + } + ) assert result["status"] == "success" assert result["provider_used"] == "gemini" diff --git a/tests/skills/optimization/test_prompt_rewriter.py b/tests/skills/optimization/test_prompt_rewriter.py index 776be2f..6c787fb 100644 --- a/tests/skills/optimization/test_prompt_rewriter.py +++ b/tests/skills/optimization/test_prompt_rewriter.py @@ -3,7 +3,7 @@ def get_skill(): bundle = SkillLoader.load_skill("optimization/prompt_rewriter") - return bundle['module'].PromptRewriter() + return bundle["module"].PromptRewriter() def test_manifest_schema(): @@ -17,7 +17,7 @@ def test_rewriter_execution_low(): skill = get_skill() params = { "raw_text": "This is a very\n\n\nspaced out prompt.", - "compression_aggression": "low" + "compression_aggression": "low", } result = skill.execute(params) assert result["compressed_text"] == "This is a very spaced out prompt." @@ -28,7 +28,7 @@ def test_rewriter_execution_high(): skill = get_skill() params = { "raw_text": "Please make sure to read this and analyze the data.", - "compression_aggression": "high" + "compression_aggression": "high", } result = skill.execute(params) assert "Please" not in result["compressed_text"] diff --git a/tests/test_loader.py b/tests/test_loader.py index f77c0ec..d186dd8 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -105,7 +105,16 @@ def test_wheel_includes_skill_manifest(tmp_path): wheel_dir.mkdir() subprocess.run( - [sys.executable, "-m", "pip", "wheel", str(REPO_ROOT), "--no-deps", "-w", str(wheel_dir)], + [ + sys.executable, + "-m", + "pip", + "wheel", + str(REPO_ROOT), + "--no-deps", + "-w", + str(wheel_dir), + ], check=True, capture_output=True, text=True, @@ -117,8 +126,12 @@ def test_wheel_includes_skill_manifest(tmp_path): with zipfile.ZipFile(wheel_path) as archive: names = archive.namelist() - assert any(n.endswith("skills/compliance/tos_evaluator/manifest.yaml") for n in names) - assert any(n.endswith("skills/office/pdf_form_filler/manifest.yaml") for n in names) + assert any( + n.endswith("skills/compliance/tos_evaluator/manifest.yaml") for n in names + ) + assert any( + n.endswith("skills/office/pdf_form_filler/manifest.yaml") for n in names + ) def test_to_ollama_prompt(): @@ -131,8 +144,8 @@ def test_to_ollama_prompt(): "properties": { "arg1": {"type": "string", "description": "The first arg"} }, - "required": ["arg1"] - } + "required": ["arg1"], + }, } } @@ -148,10 +161,8 @@ def test_to_gemini_tool(): "name": "test_gemini_skill", "parameters": { "type": "object", - "properties": { - "param1": {"type": "string"} - } - } + "properties": {"param1": {"type": "string"}}, + }, } } tool = SkillLoader.to_gemini_tool(dummy_bundle) @@ -168,8 +179,8 @@ def test_to_claude_tool(): "description": "desc", "parameters": { "type": "object", - "properties": {"arg_claude": {"type": "string"}} - } + "properties": {"arg_claude": {"type": "string"}}, + }, } } tool = SkillLoader.to_claude_tool(dummy_bundle) @@ -182,7 +193,9 @@ def test_sanitize_openai_tool_name(): SkillLoader._sanitize_openai_tool_name("compliance/tos_evaluator") == "compliance_tos_evaluator" ) - assert SkillLoader._sanitize_openai_tool_name("wallet_screening") == "wallet_screening" + assert ( + SkillLoader._sanitize_openai_tool_name("wallet_screening") == "wallet_screening" + ) assert SkillLoader._sanitize_openai_tool_name("") == "unknown_tool" assert SkillLoader._sanitize_openai_tool_name("a" * 80).startswith("a") assert len(SkillLoader._sanitize_openai_tool_name("a" * 80)) == 64 @@ -195,9 +208,7 @@ def test_to_openai_tool(): "description": "Evaluate site policy.", "parameters": { "type": "object", - "properties": { - "target_url": {"type": "string", "description": "URL"} - }, + "properties": {"target_url": {"type": "string", "description": "URL"}}, "required": ["target_url"], }, } @@ -224,9 +235,7 @@ def test_to_deepseek_tool(): "description": "Evaluate site policy.", "parameters": { "type": "object", - "properties": { - "target_url": {"type": "string", "description": "URL"} - }, + "properties": {"target_url": {"type": "string", "description": "URL"}}, "required": ["target_url"], }, } diff --git a/tests/test_skill_issuer.py b/tests/test_skill_issuer.py index f27a359..8c1fd14 100644 --- a/tests/test_skill_issuer.py +++ b/tests/test_skill_issuer.py @@ -34,24 +34,24 @@ def _assert_real_issuer(issuer: dict, context: str) -> None: assert name, f"{context}: issuer.name is required" assert email, f"{context}: issuer.email is required" - assert name.lower() not in PLACEHOLDER_NAMES, ( - f"{context}: issuer.name must not be a template placeholder" - ) - assert email.lower() not in PLACEHOLDER_EMAILS, ( - f"{context}: issuer.email must not be a template placeholder" - ) + assert ( + name.lower() not in PLACEHOLDER_NAMES + ), f"{context}: issuer.name must not be a template placeholder" + assert ( + email.lower() not in PLACEHOLDER_EMAILS + ), f"{context}: issuer.email must not be a template placeholder" github = (issuer.get("github") or "").strip() if github: - assert github.lower() not in PLACEHOLDER_GITHUB, ( - f"{context}: issuer.github must not be a template placeholder" - ) + assert ( + github.lower() not in PLACEHOLDER_GITHUB + ), f"{context}: issuer.github must not be a template placeholder" org = (issuer.get("org") or "").strip() if org: - assert org.lower() not in PLACEHOLDER_ORGS, ( - f"{context}: issuer.org must not be a template placeholder" - ) + assert ( + org.lower() not in PLACEHOLDER_ORGS + ), f"{context}: issuer.org must not be a template placeholder" def test_registry_skills_declare_issuer(): @@ -66,11 +66,15 @@ def test_registry_skills_declare_issuer(): def test_registry_skills_have_packaging_init_files(): """Each registry skill must be importable under the skills package for pip wheels.""" - assert (SKILLS_ROOT / "__init__.py").is_file(), "skills/__init__.py is required for packaging" + assert ( + SKILLS_ROOT / "__init__.py" + ).is_file(), "skills/__init__.py is required for packaging" for skill_dir in _discover_skill_dirs(): rel = skill_dir.relative_to(REPO_ROOT).as_posix() - assert (skill_dir / "__init__.py").is_file(), ( + assert ( + skill_dir / "__init__.py" + ).is_file(), ( f"{rel}: add an empty __init__.py so non-Python assets ship in PyPI wheels" ) category_dir = skill_dir.parent From 153095654a04004b59ca67e427ff950f7d450798 Mon Sep 17 00:00:00 2001 From: rosspeili Date: Wed, 3 Jun 2026 12:15:58 +0300 Subject: [PATCH 2/2] ci: gate GitHub Actions on black --check (#153) Add Black check to CI, document in TESTING.md and CONTRIBUTING.md, and pin target-version py310 in pyproject.toml. --- .github/workflows/ci.yml | 3 +++ CHANGELOG.md | 2 ++ CONTRIBUTING.md | 6 +++--- docs/TESTING.md | 4 ++-- pyproject.toml | 4 ++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04b7093..88d3591 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: python -m pip install --upgrade pip pip install -e ".[dev,all]" + - name: Check formatting with black + run: python -m black --check . + - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bd0b8..70fd9bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta - **CI**: GitHub Actions installs dependencies from `pyproject.toml` only (`pip install -e ".[dev,all]"`); removed redundant manual pip pins. CI runs `pytest tests/` only; co-located `skills/**/test_skill.py` remains a local pre-PR step (#151). - **Documentation**: [TESTING.md](docs/TESTING.md) and [CONTRIBUTING.md](CONTRIBUTING.md) aligned with CI scope and local skill-test workflow (#151). - **Documentation**: Updated [COMPARISON.md](COMPARISON.md) and README for Agent Skills (SKILL.md) open standard and fairer MCP framing ([Docs]: Light refresh of COMPARISON.md #123). +- **CI**: Repo-wide Black format pass; GitHub Actions gates on `black --check` before flake8 (#153). +- **Documentation**: [TESTING.md](docs/TESTING.md) and [CONTRIBUTING.md](CONTRIBUTING.md) updated for CI Black check (#153). ## [0.3.3] - 2026-05-29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15722cf..be2c58a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,8 +111,8 @@ Follow the [Agent Code of Conduct](CODE_OF_CONDUCT.md): deterministic skill outp ### Tests and CI - Add or update tests when behavior changes. -- **GitHub Actions** installs `pip install -e ".[dev,all]"`, runs `flake8 .`, then **`pytest tests/`** only. Do not add per-skill pip lines or test paths to `.github/workflows/ci.yml`. -- Run `python -m flake8 .` and `pytest tests/` locally before opening a PR (same scope as CI). +- **GitHub Actions** installs `pip install -e ".[dev,all]"`, runs `python -m black --check .`, then `flake8 .`, then **`pytest tests/`** only. Do not add per-skill pip lines or test paths to `.github/workflows/ci.yml`. +- Run `python -m black --check .`, `python -m flake8 .`, and `pytest tests/` locally before opening a PR (same scope as CI). - For skill work, also run `pytest skills///test_skill.py` locally and install any packages from that skill's `manifest.yaml` `requirements`. - Wait for GitHub Actions CI to pass before requesting review. @@ -134,7 +134,7 @@ Agents must follow [Agent Contribution Workflow](docs/contributing/ai_native_wor 4. **Verify locally**: ```bash - python -m black . + python -m black --check . python -m flake8 . pytest tests/ ``` diff --git a/docs/TESTING.md b/docs/TESTING.md index 191239c..348dae4 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -31,7 +31,7 @@ Run Black on the entire repository to automatically fix formatting issues: python -m black . ``` -Black is recommended locally before opening a PR. CI does not gate on Black yet; a future release may add `black --check` after the codebase is fully formatted. +Run `python -m black --check .` to verify formatting without writing files. GitHub Actions runs the same check before flake8 and pytest. ## 2. Linting (Flake8) @@ -89,7 +89,7 @@ Install any packages listed in the skill's `manifest.yaml` `requirements` before Before pushing your code, run the following commands to ensure your changes are ready for review: 1. `skillware list` (Verify install and path resolution are working) -2. `python -m black .` (Format code) +2. `python -m black --check .` (Verify formatting; use `python -m black .` to fix) 3. `python -m flake8 .` (Check quality) 4. `python -m pytest tests/` (Verify framework functionality — same scope as CI) 5. `python -m pytest skills///test_skill.py` when your PR touches that skill (local only) diff --git a/pyproject.toml b/pyproject.toml index e68ca46..b38564c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,3 +69,7 @@ include = ["skillware*", "skills*"] # and this wildcard. No per-skill entries needed when adding registry skills. [tool.setuptools.package-data] skills = ["**/*"] + +[tool.black] +line-length = 88 +target-version = ["py310"]