skillware 0.3.3__tar.gz → 0.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {skillware-0.3.3/skillware.egg-info → skillware-0.3.6}/PKG-INFO +23 -8
  2. {skillware-0.3.3 → skillware-0.3.6}/README.md +13 -6
  3. {skillware-0.3.3 → skillware-0.3.6}/pyproject.toml +21 -4
  4. skillware-0.3.6/skills/compliance/mica_module/test_skill.py +67 -0
  5. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/pii_masker/skill.py +16 -15
  6. skillware-0.3.6/skills/compliance/pii_masker/test_skill.py +58 -0
  7. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/tos_evaluator/skill.py +34 -13
  8. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/novelty_extractor/test_skill.py +39 -10
  9. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/synthetic_generator/skill.py +17 -30
  10. skillware-0.3.6/skills/data_engineering/synthetic_generator/test_skill.py +58 -0
  11. skillware-0.3.6/skills/defi/evm_tx_handler/abis.py +136 -0
  12. skillware-0.3.6/skills/defi/evm_tx_handler/card.json +20 -0
  13. skillware-0.3.6/skills/defi/evm_tx_handler/config.yaml.example +11 -0
  14. skillware-0.3.6/skills/defi/evm_tx_handler/data/addressbook.yaml +4 -0
  15. skillware-0.3.6/skills/defi/evm_tx_handler/data/chains.yaml +18 -0
  16. skillware-0.3.6/skills/defi/evm_tx_handler/data/tokens.yaml +25 -0
  17. skillware-0.3.6/skills/defi/evm_tx_handler/instructions.md +81 -0
  18. skillware-0.3.6/skills/defi/evm_tx_handler/manifest.yaml +63 -0
  19. skillware-0.3.6/skills/defi/evm_tx_handler/skill.py +1114 -0
  20. skillware-0.3.6/skills/defi/evm_tx_handler/test_skill.py +542 -0
  21. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/workflow.py +16 -7
  22. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/manifest.yaml +1 -1
  23. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/skill.py +5 -0
  24. skillware-0.3.6/skills/finance/wallet_screening/test_skill.py +84 -0
  25. skillware-0.3.6/skills/office/pdf_form_filler/test_skill.py +84 -0
  26. skillware-0.3.6/skills/optimization/prompt_rewriter/__init__.py +3 -0
  27. {skillware-0.3.3 → skillware-0.3.6}/skills/optimization/prompt_rewriter/skill.py +26 -9
  28. skillware-0.3.6/skills/optimization/prompt_rewriter/test_skill.py +55 -0
  29. skillware-0.3.6/skillware/__init__.py +0 -0
  30. skillware-0.3.6/skillware/__main__.py +4 -0
  31. {skillware-0.3.3 → skillware-0.3.6}/skillware/cli.py +77 -28
  32. skillware-0.3.6/skillware/core/__init__.py +0 -0
  33. {skillware-0.3.3 → skillware-0.3.6}/skillware/core/loader.py +5 -1
  34. {skillware-0.3.3 → skillware-0.3.6}/skillware/version_policy.py +1 -1
  35. {skillware-0.3.3 → skillware-0.3.6/skillware.egg-info}/PKG-INFO +23 -8
  36. {skillware-0.3.3 → skillware-0.3.6}/skillware.egg-info/SOURCES.txt +19 -0
  37. {skillware-0.3.3 → skillware-0.3.6}/skillware.egg-info/requires.txt +11 -1
  38. {skillware-0.3.3 → skillware-0.3.6}/tests/test_cli.py +60 -0
  39. {skillware-0.3.3 → skillware-0.3.6}/tests/test_loader.py +27 -18
  40. {skillware-0.3.3 → skillware-0.3.6}/tests/test_skill_issuer.py +18 -14
  41. skillware-0.3.3/skills/optimization/prompt_rewriter/__init__.py +0 -3
  42. {skillware-0.3.3 → skillware-0.3.6}/LICENSE +0 -0
  43. {skillware-0.3.3 → skillware-0.3.6}/MANIFEST.in +0 -0
  44. {skillware-0.3.3 → skillware-0.3.6}/setup.cfg +0 -0
  45. {skillware-0.3.3 → skillware-0.3.6}/skills/__init__.py +0 -0
  46. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/__init__.py +0 -0
  47. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/mica_module/__init__.py +0 -0
  48. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/mica_module/card.json +0 -0
  49. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/mica_module/instructions.md +0 -0
  50. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/mica_module/manifest.yaml +0 -0
  51. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/mica_module/mica_corpus.json +0 -0
  52. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/mica_module/skill.py +0 -0
  53. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/pii_masker/__init__.py +0 -0
  54. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/pii_masker/card.json +0 -0
  55. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/pii_masker/instructions.md +0 -0
  56. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/pii_masker/manifest.yaml +0 -0
  57. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/tos_evaluator/__init__.py +0 -0
  58. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/tos_evaluator/card.json +0 -0
  59. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/tos_evaluator/instructions.md +0 -0
  60. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/tos_evaluator/manifest.yaml +0 -0
  61. {skillware-0.3.3 → skillware-0.3.6}/skills/compliance/tos_evaluator/test_skill.py +0 -0
  62. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/__init__.py +0 -0
  63. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/novelty_extractor/__init__.py +0 -0
  64. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/novelty_extractor/card.json +0 -0
  65. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/novelty_extractor/instructions.md +0 -0
  66. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/novelty_extractor/manifest.yaml +0 -0
  67. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/novelty_extractor/skill.py +0 -0
  68. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/synthetic_generator/__init__.py +0 -0
  69. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/synthetic_generator/card.json +0 -0
  70. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/synthetic_generator/instructions.md +0 -0
  71. {skillware-0.3.3 → skillware-0.3.6}/skills/data_engineering/synthetic_generator/manifest.yaml +0 -0
  72. {skillware-0.3.3/skills/finance → skillware-0.3.6/skills/defi}/__init__.py +0 -0
  73. {skillware-0.3.3/skills/office → skillware-0.3.6/skills/defi/evm_tx_handler}/__init__.py +0 -0
  74. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/__init__.py +0 -0
  75. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/__init__.py +0 -0
  76. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/card.json +0 -0
  77. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/instructions.md +0 -0
  78. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/manifest.yaml +0 -0
  79. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/skill.py +0 -0
  80. {skillware-0.3.3 → skillware-0.3.6}/skills/dev_tools/issue_resolver/test_skill.py +0 -0
  81. {skillware-0.3.3/skills/office/pdf_form_filler → skillware-0.3.6/skills/finance}/__init__.py +0 -0
  82. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/__init__.py +0 -0
  83. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/card.json +0 -0
  84. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/data/entities.ftm.json +0 -0
  85. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/data/malicious_scs_2025.json +0 -0
  86. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/data/normalized_fbi_lazarus_20250722_180019.json +0 -0
  87. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/data/normalized_israel_nbctf_20250722_180019.json +0 -0
  88. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/data/normalized_uniswap_trm.json +0 -0
  89. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/instructions.md +0 -0
  90. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/maintenance/normalization_tool.py +0 -0
  91. {skillware-0.3.3 → skillware-0.3.6}/skills/finance/wallet_screening/maintenance/normalize_uniswap_trm.py +0 -0
  92. {skillware-0.3.3/skills/optimization → skillware-0.3.6/skills/office}/__init__.py +0 -0
  93. {skillware-0.3.3/skillware → skillware-0.3.6/skills/office/pdf_form_filler}/__init__.py +0 -0
  94. {skillware-0.3.3 → skillware-0.3.6}/skills/office/pdf_form_filler/card.json +0 -0
  95. {skillware-0.3.3 → skillware-0.3.6}/skills/office/pdf_form_filler/instructions.md +0 -0
  96. {skillware-0.3.3 → skillware-0.3.6}/skills/office/pdf_form_filler/manifest.yaml +0 -0
  97. {skillware-0.3.3 → skillware-0.3.6}/skills/office/pdf_form_filler/skill.py +0 -0
  98. {skillware-0.3.3 → skillware-0.3.6}/skills/office/pdf_form_filler/utils.py +0 -0
  99. {skillware-0.3.3/skillware/core → skillware-0.3.6/skills/optimization}/__init__.py +0 -0
  100. {skillware-0.3.3 → skillware-0.3.6}/skills/optimization/prompt_rewriter/card.json +0 -0
  101. {skillware-0.3.3 → skillware-0.3.6}/skills/optimization/prompt_rewriter/instructions.md +0 -0
  102. {skillware-0.3.3 → skillware-0.3.6}/skills/optimization/prompt_rewriter/manifest.yaml +0 -0
  103. {skillware-0.3.3 → skillware-0.3.6}/skillware/core/base_skill.py +0 -0
  104. {skillware-0.3.3 → skillware-0.3.6}/skillware/core/env.py +0 -0
  105. {skillware-0.3.3 → skillware-0.3.6}/skillware.egg-info/dependency_links.txt +0 -0
  106. {skillware-0.3.3 → skillware-0.3.6}/skillware.egg-info/entry_points.txt +0 -0
  107. {skillware-0.3.3 → skillware-0.3.6}/skillware.egg-info/top_level.txt +0 -0
  108. {skillware-0.3.3 → skillware-0.3.6}/tests/test_mica_module.py +0 -0
  109. {skillware-0.3.3 → skillware-0.3.6}/tests/test_version_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skillware
3
- Version: 0.3.3
3
+ Version: 0.3.6
4
4
  Summary: A framework for modular, self-contained AI skills.
5
5
  Author-email: ARPA Hellenic Logical Systems <skillware-os@arpacorp.net>
6
6
  License: MIT License
@@ -38,13 +38,13 @@ Requires-Dist: pyyaml
38
38
  Requires-Dist: python-dotenv
39
39
  Requires-Dist: beautifulsoup4
40
40
  Requires-Dist: packaging
41
+ Requires-Dist: rich>=13.0
41
42
  Provides-Extra: dev
42
43
  Requires-Dist: pytest; extra == "dev"
43
44
  Requires-Dist: pytest-mock; extra == "dev"
44
45
  Requires-Dist: flake8; extra == "dev"
45
46
  Requires-Dist: black; extra == "dev"
46
47
  Provides-Extra: cli
47
- Requires-Dist: rich>=13.0; extra == "cli"
48
48
  Provides-Extra: gemini
49
49
  Requires-Dist: google-genai; extra == "gemini"
50
50
  Provides-Extra: claude
@@ -53,12 +53,20 @@ Provides-Extra: openai
53
53
  Requires-Dist: openai; extra == "openai"
54
54
  Provides-Extra: office
55
55
  Requires-Dist: pymupdf; extra == "office"
56
+ Provides-Extra: defi
57
+ Requires-Dist: web3>=6.0.0; extra == "defi"
58
+ Provides-Extra: embeddings
59
+ Requires-Dist: fastembed; extra == "embeddings"
60
+ Requires-Dist: numpy; extra == "embeddings"
56
61
  Provides-Extra: all
57
62
  Requires-Dist: rich>=13.0; extra == "all"
58
63
  Requires-Dist: google-genai; extra == "all"
59
64
  Requires-Dist: anthropic; extra == "all"
60
65
  Requires-Dist: openai; extra == "all"
61
66
  Requires-Dist: pymupdf; extra == "all"
67
+ Requires-Dist: web3>=6.0.0; extra == "all"
68
+ Requires-Dist: fastembed; extra == "all"
69
+ Requires-Dist: numpy; extra == "all"
62
70
  Dynamic: license-file
63
71
 
64
72
  <div align="center">
@@ -118,7 +126,7 @@ documentation. Runnable provider scripts are indexed in
118
126
  ```text
119
127
  Skillware/
120
128
  ├── docs/ # Introduction, testing, skill catalog, usage guides (docs/usage/)
121
- ├── examples/ # Provider reference scripts (Gemini, Claude, OpenAI, Ollama, ...)
129
+ ├── examples/ # Provider reference scripts — usage demos, not pytest (see examples/README.md)
122
130
  ├── skills/ # Skill Registry
123
131
  │ └── category/ # Domain boundaries (e.g., finance)
124
132
  │ └── skill_name/ # The Skill bundle
@@ -126,7 +134,7 @@ Skillware/
126
134
  │ ├── skill.py # Executable Python logic
127
135
  │ ├── instructions.md # Cognitive map for the LLM
128
136
  │ ├── card.json # Optional UI presentation metadata
129
- │ └── test_skill.py # Unit tests and schema validation
137
+ │ └── test_skill.py # Bundle test (required for new skills; see docs/TESTING.md)
130
138
  ├── skillware/ # Core Framework Package
131
139
  │ ├── cli.py # Command-line interface
132
140
  │ └── core/
@@ -135,7 +143,9 @@ Skillware/
135
143
  │ └── loader.py # Universal Skill Loader and Model Adapter
136
144
  ├── templates/ # Boilerplate templates for new skills
137
145
  │ └── python_skill/ # Standard template with required files
138
- └── tests/ # Automated test suite
146
+ └── tests/ # Clone-repo tests (framework + optional maintainer skill tests)
147
+ ├── test_*.py # Framework tests (loader, CLI, issuer, …)
148
+ └── skills/ # Optional maintainer skill tests (edge cases)
139
149
  ```
140
150
 
141
151
  ## Quick Start
@@ -161,11 +171,16 @@ pip install -e .
161
171
  ### 2. Verify your installation
162
172
 
163
173
  ```bash
164
- pip install "skillware[cli]"
165
174
  skillware list
166
175
  ```
167
176
 
168
- This prints a table of all locally available skills and confirms the install and path resolution are working. Running `skillware` with no arguments opens the interactive menu.
177
+ This prints a table of all locally available skills and confirms the install
178
+ and path resolution are working. Running `skillware` with no arguments opens
179
+ the interactive menu.
180
+
181
+ If `skillware` is not recognized, Python's `Scripts` directory may not be on
182
+ your PATH — use `python -m skillware list` as a fallback. See
183
+ [CLI Reference](docs/usage/cli.md#running-the-cli) for details.
169
184
 
170
185
  ### 3. Configuration
171
186
 
@@ -255,7 +270,7 @@ Also read the [Agent Code of Conduct](CODE_OF_CONDUCT.md). Open PRs with the [pu
255
270
 
256
271
  ## Comparison
257
272
 
258
- Skillware differs from the Model Context Protocol (MCP), and Anthropic's Skills repository in several ways:
273
+ Skillware differs from the Model Context Protocol (MCP), and Agent Skills (SKILL.md) in several ways:
259
274
 
260
275
  * **Model Agnostic**: Native adapters for Gemini, Claude, Ollama, and OpenAI.
261
276
  * **Code-First**: Skills are executable Python packages, not just server specs.
@@ -55,7 +55,7 @@ documentation. Runnable provider scripts are indexed in
55
55
  ```text
56
56
  Skillware/
57
57
  ├── docs/ # Introduction, testing, skill catalog, usage guides (docs/usage/)
58
- ├── examples/ # Provider reference scripts (Gemini, Claude, OpenAI, Ollama, ...)
58
+ ├── examples/ # Provider reference scripts — usage demos, not pytest (see examples/README.md)
59
59
  ├── skills/ # Skill Registry
60
60
  │ └── category/ # Domain boundaries (e.g., finance)
61
61
  │ └── skill_name/ # The Skill bundle
@@ -63,7 +63,7 @@ Skillware/
63
63
  │ ├── skill.py # Executable Python logic
64
64
  │ ├── instructions.md # Cognitive map for the LLM
65
65
  │ ├── card.json # Optional UI presentation metadata
66
- │ └── test_skill.py # Unit tests and schema validation
66
+ │ └── test_skill.py # Bundle test (required for new skills; see docs/TESTING.md)
67
67
  ├── skillware/ # Core Framework Package
68
68
  │ ├── cli.py # Command-line interface
69
69
  │ └── core/
@@ -72,7 +72,9 @@ Skillware/
72
72
  │ └── loader.py # Universal Skill Loader and Model Adapter
73
73
  ├── templates/ # Boilerplate templates for new skills
74
74
  │ └── python_skill/ # Standard template with required files
75
- └── tests/ # Automated test suite
75
+ └── tests/ # Clone-repo tests (framework + optional maintainer skill tests)
76
+ ├── test_*.py # Framework tests (loader, CLI, issuer, …)
77
+ └── skills/ # Optional maintainer skill tests (edge cases)
76
78
  ```
77
79
 
78
80
  ## Quick Start
@@ -98,11 +100,16 @@ pip install -e .
98
100
  ### 2. Verify your installation
99
101
 
100
102
  ```bash
101
- pip install "skillware[cli]"
102
103
  skillware list
103
104
  ```
104
105
 
105
- This prints a table of all locally available skills and confirms the install and path resolution are working. Running `skillware` with no arguments opens the interactive menu.
106
+ This prints a table of all locally available skills and confirms the install
107
+ and path resolution are working. Running `skillware` with no arguments opens
108
+ the interactive menu.
109
+
110
+ If `skillware` is not recognized, Python's `Scripts` directory may not be on
111
+ your PATH — use `python -m skillware list` as a fallback. See
112
+ [CLI Reference](docs/usage/cli.md#running-the-cli) for details.
106
113
 
107
114
  ### 3. Configuration
108
115
 
@@ -192,7 +199,7 @@ Also read the [Agent Code of Conduct](CODE_OF_CONDUCT.md). Open PRs with the [pu
192
199
 
193
200
  ## Comparison
194
201
 
195
- Skillware differs from the Model Context Protocol (MCP), and Anthropic's Skills repository in several ways:
202
+ Skillware differs from the Model Context Protocol (MCP), and Agent Skills (SKILL.md) in several ways:
196
203
 
197
204
  * **Model Agnostic**: Native adapters for Gemini, Claude, Ollama, and OpenAI.
198
205
  * **Code-First**: Skills are executable Python packages, not just server specs.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skillware"
7
- version = "0.3.3"
7
+ version = "0.3.6"
8
8
  description = "A framework for modular, self-contained AI skills."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "python-dotenv",
24
24
  "beautifulsoup4",
25
25
  "packaging",
26
+ "rich>=13.0",
26
27
  ]
27
28
  requires-python = ">=3.10"
28
29
 
@@ -36,9 +37,7 @@ dev = [
36
37
  "flake8",
37
38
  "black",
38
39
  ]
39
- cli = [
40
- "rich>=13.0",
41
- ]
40
+ cli = []
42
41
  gemini = [
43
42
  "google-genai",
44
43
  ]
@@ -51,12 +50,22 @@ openai = [
51
50
  office = [
52
51
  "pymupdf",
53
52
  ]
53
+ defi = [
54
+ "web3>=6.0.0",
55
+ ]
56
+ embeddings = [
57
+ "fastembed",
58
+ "numpy",
59
+ ]
54
60
  all = [
55
61
  "rich>=13.0",
56
62
  "google-genai",
57
63
  "anthropic",
58
64
  "openai",
59
65
  "pymupdf",
66
+ "web3>=6.0.0",
67
+ "fastembed",
68
+ "numpy",
60
69
  ]
61
70
 
62
71
  [tool.setuptools]
@@ -69,3 +78,11 @@ include = ["skillware*", "skills*"]
69
78
  # and this wildcard. No per-skill entries needed when adding registry skills.
70
79
  [tool.setuptools.package-data]
71
80
  skills = ["**/*"]
81
+
82
+ [tool.pytest.ini_options]
83
+ testpaths = ["tests", "skills"]
84
+ addopts = "--ignore=examples"
85
+
86
+ [tool.black]
87
+ line-length = 88
88
+ target-version = ["py310"]
@@ -0,0 +1,67 @@
1
+ import os
2
+
3
+ import pytest
4
+ import yaml
5
+
6
+ from .skill import MiCAModuleSkill
7
+
8
+
9
+ @pytest.fixture
10
+ def skill():
11
+ return MiCAModuleSkill()
12
+
13
+
14
+ @pytest.fixture
15
+ def manifest():
16
+ manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml")
17
+ with open(manifest_path, "r", encoding="utf-8") as f:
18
+ return yaml.safe_load(f)
19
+
20
+
21
+ def test_skill_manifest_consistency(skill, manifest):
22
+ skill_manifest = skill.manifest
23
+ assert skill_manifest["name"] == manifest["name"]
24
+ assert skill_manifest["version"] == manifest["version"]
25
+
26
+
27
+ def test_stateless_rag_execution(skill):
28
+ result = skill.execute(
29
+ {
30
+ "user_prompt": (
31
+ "I want to issue an asset-referenced token. "
32
+ "What are the authorization rules?"
33
+ ),
34
+ "run_evaluator": False,
35
+ }
36
+ )
37
+ assert result["policy_status"] == "CAUTION"
38
+ assert "retrieved_sections" in result
39
+ assert "final_context_for_agent" in result
40
+ assert "Evaluator disabled" in result["gemini_evaluator_feedback"]["holes_found"]
41
+
42
+
43
+ def test_router_normalization(skill):
44
+ mock_corpus = [
45
+ {
46
+ "title_num": "Title V",
47
+ "article_num": "Article 59",
48
+ "keywords": ["authorisation", "casp"],
49
+ "content": "CASP Authorization rules...",
50
+ }
51
+ ]
52
+ matched = skill._route_and_fetch("Authorization of a CASP", mock_corpus)
53
+ assert len(matched) > 0
54
+ assert matched[0]["article_num"] == "Article 59"
55
+
56
+
57
+ def test_router_deduplication(skill):
58
+ mock_corpus = [
59
+ {
60
+ "title_num": "Title V",
61
+ "article_num": "Article 59",
62
+ "keywords": ["authorisation", "casp"],
63
+ "content": "CASP Authorization rules...",
64
+ }
65
+ ]
66
+ matched = skill._route_and_fetch("authorisation casp", mock_corpus)
67
+ assert len(matched) == 1
@@ -11,10 +11,7 @@ class PIIMaskerSkill(BaseSkill):
11
11
 
12
12
  @property
13
13
  def manifest(self) -> Dict[str, Any]:
14
- return {
15
- "name": "compliance/pii_masker",
16
- "version": "0.1.0"
17
- }
14
+ return {"name": "compliance/pii_masker", "version": "0.1.0"}
18
15
 
19
16
  def execute(self, params: Dict[str, Any]) -> Dict[str, Any]:
20
17
  text = params.get("text", "")
@@ -25,7 +22,7 @@ class PIIMaskerSkill(BaseSkill):
25
22
  sanitized_text = self._apply_mode(sanitized_text, mode)
26
23
 
27
24
  # Build unique entity types list
28
- entities = list(set([re.sub(r'_[0-9]+$', '', e) for e in detected_entities]))
25
+ entities = list(set([re.sub(r"_[0-9]+$", "", e) for e in detected_entities]))
29
26
 
30
27
  return {
31
28
  "sanitized_text": sanitized_text,
@@ -33,17 +30,21 @@ class PIIMaskerSkill(BaseSkill):
33
30
  "detected_entities": entities,
34
31
  "entity_count": len(detected_entities),
35
32
  "security_level": "local-only",
36
- "model": "arpacorp/micro-f1-mask"
37
- }
33
+ "model": "arpacorp/micro-f1-mask",
34
+ },
38
35
  }
39
36
 
40
37
  def _call_ollama(self, text: str, endpoint: str) -> Tuple[str, List[str]]:
41
38
  try:
42
- response = requests.post(f"{endpoint}/api/generate", json={
43
- "model": "arpacorp/micro-f1-mask",
44
- "prompt": text,
45
- "stream": False
46
- }, timeout=30)
39
+ response = requests.post(
40
+ f"{endpoint}/api/generate",
41
+ json={
42
+ "model": "arpacorp/micro-f1-mask",
43
+ "prompt": text,
44
+ "stream": False,
45
+ },
46
+ timeout=30,
47
+ )
47
48
  if response.status_code == 200:
48
49
  result_text = response.json().get("response", text)
49
50
  else:
@@ -56,7 +57,7 @@ class PIIMaskerSkill(BaseSkill):
56
57
  result_text = text
57
58
 
58
59
  # Detect entities in the response
59
- detected = re.findall(r'\[([A-Z_]+(?:_[0-9]+)?)\]', result_text)
60
+ detected = re.findall(r"\[([A-Z_]+(?:_[0-9]+)?)\]", result_text)
60
61
  return result_text, detected
61
62
 
62
63
  def _apply_mode(self, text: str, mode: str) -> str:
@@ -64,14 +65,14 @@ class PIIMaskerSkill(BaseSkill):
64
65
  return text
65
66
 
66
67
  # Pattern to catch [DOCUMENT], [PERSON_1], etc.
67
- pattern = r'\[[A-Z_]+(?:_[0-9]+)?\]'
68
+ pattern = r"\[[A-Z_]+(?:_[0-9]+)?\]"
68
69
  if mode == "redact":
69
70
  return re.sub(pattern, "XXXX", text)
70
71
  elif mode == "remove":
71
72
  # Replace token and any immediate preceding/following spaces safely
72
73
  # A simple sub is sufficient. Cleaning up double spaces.
73
74
  text = re.sub(pattern, "", text)
74
- text = re.sub(r'\s+', ' ', text).strip()
75
+ text = re.sub(r"\s+", " ", text).strip()
75
76
  return text
76
77
 
77
78
  return text
@@ -0,0 +1,58 @@
1
+ import os
2
+
3
+ import pytest
4
+ import yaml
5
+
6
+ from .skill import PIIMaskerSkill
7
+
8
+
9
+ @pytest.fixture
10
+ def skill():
11
+ return PIIMaskerSkill()
12
+
13
+
14
+ @pytest.fixture
15
+ def manifest():
16
+ manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml")
17
+ with open(manifest_path, "r", encoding="utf-8") as f:
18
+ return yaml.safe_load(f)
19
+
20
+
21
+ def test_skill_manifest_consistency(skill, manifest):
22
+ skill_manifest = skill.manifest
23
+ assert skill_manifest["name"] == manifest["name"]
24
+ assert skill_manifest["version"] == manifest["version"]
25
+
26
+
27
+ def test_pii_masker_modes(mocker, skill):
28
+ mock_response = (
29
+ "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified."
30
+ )
31
+ mocker.patch.object(
32
+ skill,
33
+ "_call_ollama",
34
+ return_value=(mock_response, ["PERSON_1", "CRYPTO_ADDRESS", "EMAIL"]),
35
+ )
36
+
37
+ sample_text = (
38
+ "Hello John Doe, your wallet 0xabc and john@doe.com have been verified."
39
+ )
40
+
41
+ result_mask = skill.execute({"text": sample_text})
42
+ assert (
43
+ result_mask["sanitized_text"]
44
+ == "Hello [PERSON_1], your wallet [CRYPTO_ADDRESS] and [EMAIL] have been verified."
45
+ )
46
+ assert "PERSON" in result_mask["metadata"]["detected_entities"]
47
+ assert "CRYPTO_ADDRESS" in result_mask["metadata"]["detected_entities"]
48
+
49
+ result_redact = skill.execute({"text": sample_text, "mode": "redact"})
50
+ assert (
51
+ result_redact["sanitized_text"]
52
+ == "Hello XXXX, your wallet XXXX and XXXX have been verified."
53
+ )
54
+
55
+ result_remove = skill.execute({"text": sample_text, "mode": "remove"})
56
+ assert (
57
+ result_remove["sanitized_text"] == "Hello , your wallet and have been verified."
58
+ )
@@ -213,7 +213,9 @@ class TOSEvaluatorSkill(BaseSkill):
213
213
  try:
214
214
  response = self.session.get(robots_url, timeout=10)
215
215
  if response.status_code >= 400:
216
- assessment["reason"] = f"robots.txt returned HTTP {response.status_code}."
216
+ assessment["reason"] = (
217
+ f"robots.txt returned HTTP {response.status_code}."
218
+ )
217
219
  return assessment
218
220
 
219
221
  parser = RobotFileParser()
@@ -266,13 +268,20 @@ class TOSEvaluatorSkill(BaseSkill):
266
268
  else:
267
269
  candidates[item["url"]] = item
268
270
 
269
- ordered = sorted(candidates.values(), key=lambda item: item["score"], reverse=True)
271
+ ordered = sorted(
272
+ candidates.values(), key=lambda item: item["score"], reverse=True
273
+ )
270
274
  return {"candidates": ordered[:max_terms_pages]}
271
275
 
272
- def _extract_candidate_links(self, page_url: str, origin: str) -> List[Dict[str, Any]]:
276
+ def _extract_candidate_links(
277
+ self, page_url: str, origin: str
278
+ ) -> List[Dict[str, Any]]:
273
279
  links: List[Dict[str, Any]] = []
274
280
  response = self._safe_get(page_url, timeout=10)
275
- if not response or "html" not in response.headers.get("Content-Type", "").lower():
281
+ if (
282
+ not response
283
+ or "html" not in response.headers.get("Content-Type", "").lower()
284
+ ):
276
285
  return links
277
286
 
278
287
  soup = BeautifulSoup(response.text[:300000], "html.parser")
@@ -449,7 +458,10 @@ class TOSEvaluatorSkill(BaseSkill):
449
458
  return False
450
459
  if robots_assessment.get("can_fetch") is False:
451
460
  return False
452
- return bool(tos_assessment.get("matched_clauses") or tos_assessment["status"] == "caution")
461
+ return bool(
462
+ tos_assessment.get("matched_clauses")
463
+ or tos_assessment["status"] == "caution"
464
+ )
453
465
 
454
466
  def _run_llm_evaluator(
455
467
  self, normalized: Dict[str, Any], tos_assessment: Dict[str, Any]
@@ -512,26 +524,33 @@ class TOSEvaluatorSkill(BaseSkill):
512
524
  verdict = "INSUFFICIENT_EVIDENCE"
513
525
  confidence_score = 0.35
514
526
  reason = "Insufficient policy evidence to safely approve the requested action."
515
- recommended_next_step = "Review the discovered policy pages manually before proceeding."
527
+ recommended_next_step = (
528
+ "Review the discovered policy pages manually before proceeding."
529
+ )
516
530
 
517
531
  if robots_assessment.get("can_fetch") is False:
518
532
  verdict = "UNSAFE"
519
533
  confidence_score = 0.98
520
534
  reason = robots_assessment["reason"]
521
- recommended_next_step = (
522
- "Do not automate access to this path unless you have explicit permission."
523
- )
535
+ recommended_next_step = "Do not automate access to this path unless you have explicit permission."
524
536
  elif tos_assessment["status"] == "blocked":
525
537
  verdict = "UNSAFE"
526
538
  confidence_score = 0.9
527
539
  reason = tos_assessment["summary"]
528
- recommended_next_step = "Avoid the requested action or obtain explicit written permission."
540
+ recommended_next_step = (
541
+ "Avoid the requested action or obtain explicit written permission."
542
+ )
529
543
  elif tos_assessment["status"] == "caution":
530
544
  verdict = "CAUTION"
531
545
  confidence_score = 0.65
532
546
  reason = tos_assessment["summary"]
533
- recommended_next_step = "Prefer an official API or documented integration path if one exists."
534
- elif tos_assessment["status"] == "allowed" and robots_assessment.get("can_fetch") is not False:
547
+ recommended_next_step = (
548
+ "Prefer an official API or documented integration path if one exists."
549
+ )
550
+ elif (
551
+ tos_assessment["status"] == "allowed"
552
+ and robots_assessment.get("can_fetch") is not False
553
+ ):
535
554
  verdict = "SAFE"
536
555
  confidence_score = 0.72
537
556
  reason = tos_assessment["summary"]
@@ -575,7 +594,9 @@ class TOSEvaluatorSkill(BaseSkill):
575
594
  "tos_assessment": tos_assessment,
576
595
  "llm_assessment": llm_assessment or {"status": "not_used"},
577
596
  "discovered_policy_urls": {
578
- "candidates": [item["url"] for item in policy_candidates.get("candidates", [])]
597
+ "candidates": [
598
+ item["url"] for item in policy_candidates.get("candidates", [])
599
+ ]
579
600
  },
580
601
  "evidence": evidence,
581
602
  }
@@ -1,32 +1,66 @@
1
+ import os
2
+ from unittest.mock import MagicMock
3
+
4
+ import numpy as np
1
5
  import pytest
2
6
  import yaml
3
- import os
7
+
4
8
  from .skill import NoveltyExtractor
5
9
 
6
10
 
11
+ def _mock_embed_vectors(texts):
12
+ """Deterministic keyword vectors — no HuggingFace download in CI."""
13
+ vectors = []
14
+ for text in texts:
15
+ t = text.lower()
16
+ if "bitcoin" in t:
17
+ vectors.append(np.array([1.0, 0.0, 0.0], dtype=float))
18
+ elif "sky" in t or "blue" in t:
19
+ vectors.append(np.array([0.0, 1.0, 0.0], dtype=float))
20
+ elif "python" in t:
21
+ vectors.append(np.array([0.0, 0.0, 1.0], dtype=float))
22
+ else:
23
+ vectors.append(np.array([0.1, 0.1, 0.1], dtype=float))
24
+ return vectors
25
+
26
+
27
+ @pytest.fixture(autouse=True)
28
+ def mock_embedding_model(monkeypatch):
29
+ """Bundle tests must stay offline; mock fastembed to avoid HF rate limits in CI."""
30
+ NoveltyExtractor._model = None
31
+ mock_model = MagicMock()
32
+ mock_model.embed.side_effect = _mock_embed_vectors
33
+
34
+ @classmethod
35
+ def _fake_get_model(cls):
36
+ if cls._model is None:
37
+ cls._model = mock_model
38
+ return cls._model
39
+
40
+ monkeypatch.setattr(NoveltyExtractor, "_get_model", _fake_get_model)
41
+ yield
42
+ NoveltyExtractor._model = None
43
+
44
+
7
45
  @pytest.fixture
8
46
  def skill():
9
- """Fixture to initialize the skill."""
10
47
  return NoveltyExtractor()
11
48
 
12
49
 
13
50
  @pytest.fixture
14
51
  def manifest():
15
- """Fixture to load manifest.yaml for validation."""
16
52
  manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml")
17
53
  with open(manifest_path, "r", encoding="utf-8") as f:
18
54
  return yaml.safe_load(f)
19
55
 
20
56
 
21
57
  def test_skill_manifest_consistency(skill, manifest):
22
- """Verify the skill manifest matches manifest.yaml."""
23
58
  skill_manifest = skill.manifest
24
59
  assert skill_manifest["name"] == manifest["name"]
25
60
  assert skill_manifest["version"] == manifest["version"]
26
61
 
27
62
 
28
63
  def test_skill_returns_dict(skill):
29
- """Execution result must be a dictionary."""
30
64
  result = skill.execute(
31
65
  {
32
66
  "dataset_chunk": "Bitcoin is going to rise.\n\nThe sky is blue.",
@@ -37,7 +71,6 @@ def test_skill_returns_dict(skill):
37
71
 
38
72
 
39
73
  def test_skill_output_keys(skill):
40
- """Result must contain the three expected output keys."""
41
74
  result = skill.execute(
42
75
  {
43
76
  "dataset_chunk": "Bitcoin is going to rise.\n\nThe sky is blue.",
@@ -50,7 +83,6 @@ def test_skill_output_keys(skill):
50
83
 
51
84
 
52
85
  def test_skill_filters_redundant_chunks(skill):
53
- """Semantically similar chunks should be filtered out."""
54
86
  result = skill.execute(
55
87
  {
56
88
  "dataset_chunk": (
@@ -67,7 +99,6 @@ def test_skill_filters_redundant_chunks(skill):
67
99
 
68
100
 
69
101
  def test_skill_empty_input(skill):
70
- """Empty input should return empty result without crashing."""
71
102
  result = skill.execute(
72
103
  {
73
104
  "dataset_chunk": "",
@@ -79,7 +110,6 @@ def test_skill_empty_input(skill):
79
110
 
80
111
 
81
112
  def test_skill_baseline_filters_seen_content(skill):
82
- """Chunks already in baseline should be filtered out."""
83
113
  baseline = "Bitcoin is going to rise.\n\nThe sky is blue."
84
114
  result = skill.execute(
85
115
  {
@@ -93,7 +123,6 @@ def test_skill_baseline_filters_seen_content(skill):
93
123
 
94
124
 
95
125
  def test_skill_sentence_strategy(skill):
96
- """Sentence chunking strategy should work without crashing."""
97
126
  result = skill.execute(
98
127
  {
99
128
  "dataset_chunk": "Bitcoin is going to rise. The sky is blue. Python is great.",