diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a8e526..4a0910f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,7 @@ ### Added -- **`create-skill` CLI command**: Generate standalone skill packages with embedded LanceDB databases. Supports tool selection, custom preamble/description, and optional config embedding. Generated packages register as `haiku.skills` entry points. -- **`haiku.rag.skill_generator`**: Programmatic API for skill generation (`generate_skill()`, `render_templates()`) -- **`haiku.rag.skills._tools`**: Reusable tool implementations and `create_skill_tools()` factory shared by built-in and generated skills -- **Jinja2 dependency**: Added for skill template rendering +- **`create-skill` CLI command**: Generate standalone skill packages with embedded LanceDB databases. Generated packages register as `haiku.skills` entry points. ## [0.35.0] - 2026-03-24 diff --git a/haiku_rag_slim/haiku/rag/skill_generator/__init__.py b/haiku_rag_slim/haiku/rag/skill_generator/__init__.py index ca4319fb..241925d4 100644 --- a/haiku_rag_slim/haiku/rag/skill_generator/__init__.py +++ b/haiku_rag_slim/haiku/rag/skill_generator/__init__.py @@ -36,13 +36,12 @@ def _get_env() -> Environment: def validate_metadata(name: str, description: str) -> None: + if not name.isidentifier(): + raise ValueError(f"{name!r} is not a valid Python identifier") + from haiku.skills import SkillMetadata SkillMetadata(name=name, description=description) - if not name.isidentifier(): - raise ValueError(f"{name!r} is not a valid Python identifier") - if not name.islower(): - raise ValueError(f"{name!r} must be lowercase") def validate_tools(tools: list[str]) -> None: diff --git a/haiku_rag_slim/haiku/rag/skills/_tools.py b/haiku_rag_slim/haiku/rag/skills/_tools.py index 337d44b1..4cdd1710 100644 --- a/haiku_rag_slim/haiku/rag/skills/_tools.py +++ b/haiku_rag_slim/haiku/rag/skills/_tools.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from pydantic_ai import RunContext from haiku.rag.agents.research.models import Citation +from haiku.rag.config.models import AppConfig from haiku.rag.tools.document import DocumentInfo from haiku.rag.tools.qa import QAHistoryEntry from haiku.skills.state import SkillRunDeps @@ -25,7 +26,7 @@ class AnalysisEntry(BaseModel): async def find_relevant_prior_qa( qa_history: list[QAHistoryEntry], query: str, - config: Any, + config: AppConfig, ) -> list[QAHistoryEntry]: from haiku.rag.embeddings import get_embedder from haiku.rag.tools.qa import PRIOR_ANSWER_RELEVANCE_THRESHOLD @@ -61,7 +62,7 @@ async def find_relevant_prior_qa( async def skill_search( db_path: Path, - config: Any, + config: AppConfig, query: str, limit: int | None = None, document_filter: str | None = None, @@ -85,7 +86,7 @@ async def skill_search( async def skill_list_documents( db_path: Path, - config: Any, + config: AppConfig, limit: int | None = None, offset: int | None = None, ) -> list[dict[str, Any]]: @@ -108,7 +109,7 @@ async def skill_list_documents( async def skill_get_document( db_path: Path, - config: Any, + config: AppConfig, query: str, ) -> dict[str, Any] | None: from haiku.rag.client import HaikuRAG @@ -130,7 +131,7 @@ async def skill_get_document( async def skill_ask( db_path: Path, - config: Any, + config: AppConfig, question: str, qa_history: list[QAHistoryEntry] | None = None, document_filter: str | None = None, @@ -166,7 +167,7 @@ async def skill_ask( async def skill_research( db_path: Path, - config: Any, + config: AppConfig, question: str, document_filter: str | None = None, ) -> tuple[str, str, str]: @@ -203,7 +204,7 @@ async def skill_research( async def skill_analyze( db_path: Path, - config: Any, + config: AppConfig, question: str, document: str | None = None, filter: str | None = None, @@ -235,7 +236,7 @@ def update_documents_state( documents_state.append(doc_info) -def _get_state(ctx: RunContext[SkillRunDeps], state_type: type) -> Any: +def _get_state(ctx: RunContext[SkillRunDeps], state_type: type[BaseModel]) -> Any: if ctx.deps and ctx.deps.state and isinstance(ctx.deps.state, state_type): return ctx.deps.state return None @@ -243,8 +244,8 @@ def _get_state(ctx: RunContext[SkillRunDeps], state_type: type) -> Any: def create_skill_tools( db_path: Path, - config: Any, - state_type: type, + config: AppConfig, + state_type: type[BaseModel], tool_names: list[str], ) -> dict[str, Any]: """Create tool closures for a skill. diff --git a/tests/test_skill_generator.py b/tests/test_skill_generator.py index 743eaa0f..db79217f 100644 --- a/tests/test_skill_generator.py +++ b/tests/test_skill_generator.py @@ -43,7 +43,7 @@ class TestValidateMetadata: validate_metadata("Recipes", "A skill.") def test_rejects_empty_name(self): - with pytest.raises(ValueError, match="name"): + with pytest.raises(ValueError, match="identifier"): validate_metadata("", "A skill.") def test_rejects_not_identifier(self): @@ -51,11 +51,11 @@ class TestValidateMetadata: validate_metadata("123abc", "A skill.") def test_rejects_spaces_in_name(self): - with pytest.raises(ValueError, match="name"): + with pytest.raises(ValueError, match="identifier"): validate_metadata("my recipes", "A skill.") def test_rejects_special_chars(self): - with pytest.raises(ValueError, match="name"): + with pytest.raises(ValueError, match="identifier"): validate_metadata("my@recipes", "A skill.") def test_rejects_empty_description(self): @@ -313,7 +313,7 @@ class TestGenerateSkill: def test_rejects_invalid_name(self, tmp_path): db_path = _make_fake_lancedb(tmp_path / "test.lancedb") - with pytest.raises(ValueError, match="name"): + with pytest.raises(ValueError, match="identifier"): generate_skill( db_path=db_path, output_dir=tmp_path,