diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17321e3..7302a90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,13 +75,12 @@ This is the most critical file. It is the "driver" for the LLM. *Wait for approval/feedback before writing code.* 2. **Fork** the repository. 3. **Create** your skill folder: `skills///`. -4. **Implement** the 4 required files (`manifest.yaml`, `skill.py`, `instructions.md`, `card.json`). -5. **Add** a test script in `examples/`. -6. **Verify**: Run linting and tests. See [TESTING.md](docs/TESTING.md) for details. +4. **Implement** the 5 required files (`manifest.yaml`, `skill.py`, `instructions.md`, `card.json`, `test_skill.py`). +5. **Verify**: Run linting and tests locally. + * `pytest skills///test_skill.py` * `python -m black .` * `python -m flake8 .` - * `python -m pytest tests/` -7. **Submit** PR. +6. **Submit** PR. --- diff --git a/README.md b/README.md index cace344..7980981 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ Skillware/ │ └── skill_name/ # The Skill bundle │ ├── manifest.yaml # Definition, schema, and constitution │ ├── skill.py # Executable Python logic -│ └── instructions.md # Cognitive map for the LLM +│ ├── instructions.md # Cognitive map for the LLM +│ └── test_skill.py # Unit tests & schema validation ├── skillware/ # Core Framework Package │ └── core/ │ ├── base_skill.py # Abstract Base Class for skills diff --git a/docs/TESTING.md b/docs/TESTING.md index 6bbc832..2f326e6 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -59,9 +59,16 @@ Run the full test suite: python -m pytest tests/ ``` +### Testing Individual Skills +Every skill now comes with a `test_skill.py` boilerplate. You can run tests for a specific skill without running the entire suite: + +```bash +python -m pytest skills///test_skill.py +``` + ### Writing Tests -- Place tests in the `tests/` directory. -- Mimic the structure of the `skills/` or `skillware/` directory you are testing. +- **Global Tests**: Place core framework tests in the `tests/` directory. +- **Skill Tests**: Place skill-specific logic tests in a `test_skill.py` file within the skill's own directory. - Use `conftest.py` for shared fixtures (e.g., mocking LLM clients). ## Pre-Commit Checklist @@ -70,4 +77,5 @@ Before pushing your code, run the following commands to ensure your changes are 1. `python -m black .` (Format code) 2. `python -m flake8 .` (Check quality) -3. `python -m pytest tests/` (Verify functionality) +3. `python -m pytest tests/` (Verify framework functionality) +4. `python -m pytest skills/` (Verify all skills pass their local tests) diff --git a/templates/python_skill/instructions.md b/templates/python_skill/instructions.md new file mode 100644 index 0000000..72c85bb --- /dev/null +++ b/templates/python_skill/instructions.md @@ -0,0 +1,14 @@ +# Instructions: My Awesome Skill + +You are an agent equipped with the **My Awesome Skill**. + +### When to use this tool +- Use this tool when the user asks for [describe primary use case]. +- Do not use this tool for [describe anti-patterns]. + +### How to interpret the output +- The tool returns a `result` field containing [describe result]. +- If you see an error message, explain to the user that [describe error handling]. + +### Examples +- User: "Run my awesome skill with value X" -> Call `my-awesome-skill(param1="X")` diff --git a/templates/python_skill/manifest.yaml b/templates/python_skill/manifest.yaml index 8daddba..2f2cdeb 100644 --- a/templates/python_skill/manifest.yaml +++ b/templates/python_skill/manifest.yaml @@ -2,14 +2,22 @@ name: "my-awesome-skill" version: "0.1.0" description: "A short description of what this skill does." category: "utility" -inputs: - param1: - type: "string" - description: "Description of the first parameter" +parameters: + type: object + properties: + param1: + type: string + description: "Description of the first parameter" + required: + - param1 outputs: result: - type: "string" + type: string description: "Description of the output" +requirements: [] +constitution: | + 1. Always provide helpful and accurate results. + 2. Do not include sensitive information in the output. presentation: icon: "star" color: "#3498DB" diff --git a/templates/python_skill/skill.py b/templates/python_skill/skill.py index 8adfc82..ce865ad 100644 --- a/templates/python_skill/skill.py +++ b/templates/python_skill/skill.py @@ -5,10 +5,23 @@ class MyAwesomeSkill(BaseSkill): @property def manifest(self) -> Dict[str, Any]: - # You can load this from manifest.yaml or define it here - return {"name": "my-awesome-skill", "version": "0.1.0"} + """ + Returns the skill's manifest. In a production skill, + you can load this from manifest.yaml using SkillLoader. + """ + return { + "name": "my-awesome-skill", + "version": "0.1.0", + "description": "A short description of what this skill does.", + } + + def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: + """ + The main execution logic for the skill. + Expects 'param1' in params as defined in manifest.yaml. + """ + param1 = params.get("param1", "default") + + # Implement your logic here - def execute(self, params: Dict[str, Any]) -> Any: - # Your skill logic goes here - param1 = params.get("param1") return {"result": f"Executed with {param1}"} diff --git a/templates/python_skill/test_skill.py b/templates/python_skill/test_skill.py new file mode 100644 index 0000000..2b4a3a2 --- /dev/null +++ b/templates/python_skill/test_skill.py @@ -0,0 +1,51 @@ +import pytest +import yaml +import os +from .skill import MyAwesomeSkill + + +@pytest.fixture +def skill(): + """Fixture to initialize the skill class.""" + return MyAwesomeSkill() + + +@pytest.fixture +def manifest(): + """Fixture to load the manifest.yaml for validation.""" + manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml") + with open(manifest_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + + +def test_skill_manifest_consistency(skill, manifest): + """Verify the skill's internal manifest matches the manifest.yaml file basics.""" + skill_manifest = skill.manifest + assert skill_manifest["name"] == manifest["name"] + assert skill_manifest["version"] == manifest["version"] + + +def test_skill_execution(skill, manifest): + """Test the skill execution and validate output schema.""" + # 1. Prepare dummy input + params = {"param1": "test-value"} + + # 2. Execute + result = skill.execute(params) + + # 3. Validate result is a dictionary (JSON serializable) + assert isinstance(result, dict), "Execution result must be a dictionary" + + # 4. Validate against 'outputs' defined in manifest.yaml + expected_outputs = manifest.get("outputs", {}) + for key, spec in expected_outputs.items(): + assert key in result, f"Missing expected output key: '{key}'" + + # Optional: Basic type checking based on manifest + expected_type = spec.get("type") + if expected_type == "string": + assert isinstance(result[key], str), f"Output '{key}' should be a string" + elif expected_type == "integer": + assert isinstance(result[key], int), f"Output '{key}' should be an integer" + elif expected_type == "boolean": + assert isinstance(result[key], bool), f"Output '{key}' should be a boolean"