Fix validate_metadata layering and type annotations in _tools.py

This commit is contained in:
Yiorgis Gozadinos 2026-03-24 17:09:14 +02:00
parent 7309d13317
commit 7d50edea86
No known key found for this signature in database
4 changed files with 19 additions and 22 deletions

View file

@ -3,10 +3,7 @@
### Added ### 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. - **`create-skill` CLI command**: Generate standalone skill packages with embedded LanceDB databases. 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
## [0.35.0] - 2026-03-24 ## [0.35.0] - 2026-03-24

View file

@ -36,13 +36,12 @@ def _get_env() -> Environment:
def validate_metadata(name: str, description: str) -> None: 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 from haiku.skills import SkillMetadata
SkillMetadata(name=name, description=description) 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: def validate_tools(tools: list[str]) -> None:

View file

@ -5,6 +5,7 @@ from pydantic import BaseModel
from pydantic_ai import RunContext from pydantic_ai import RunContext
from haiku.rag.agents.research.models import Citation 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.document import DocumentInfo
from haiku.rag.tools.qa import QAHistoryEntry from haiku.rag.tools.qa import QAHistoryEntry
from haiku.skills.state import SkillRunDeps from haiku.skills.state import SkillRunDeps
@ -25,7 +26,7 @@ class AnalysisEntry(BaseModel):
async def find_relevant_prior_qa( async def find_relevant_prior_qa(
qa_history: list[QAHistoryEntry], qa_history: list[QAHistoryEntry],
query: str, query: str,
config: Any, config: AppConfig,
) -> list[QAHistoryEntry]: ) -> list[QAHistoryEntry]:
from haiku.rag.embeddings import get_embedder from haiku.rag.embeddings import get_embedder
from haiku.rag.tools.qa import PRIOR_ANSWER_RELEVANCE_THRESHOLD from haiku.rag.tools.qa import PRIOR_ANSWER_RELEVANCE_THRESHOLD
@ -61,7 +62,7 @@ async def find_relevant_prior_qa(
async def skill_search( async def skill_search(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
query: str, query: str,
limit: int | None = None, limit: int | None = None,
document_filter: str | None = None, document_filter: str | None = None,
@ -85,7 +86,7 @@ async def skill_search(
async def skill_list_documents( async def skill_list_documents(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
limit: int | None = None, limit: int | None = None,
offset: int | None = None, offset: int | None = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
@ -108,7 +109,7 @@ async def skill_list_documents(
async def skill_get_document( async def skill_get_document(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
query: str, query: str,
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
@ -130,7 +131,7 @@ async def skill_get_document(
async def skill_ask( async def skill_ask(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
question: str, question: str,
qa_history: list[QAHistoryEntry] | None = None, qa_history: list[QAHistoryEntry] | None = None,
document_filter: str | None = None, document_filter: str | None = None,
@ -166,7 +167,7 @@ async def skill_ask(
async def skill_research( async def skill_research(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
question: str, question: str,
document_filter: str | None = None, document_filter: str | None = None,
) -> tuple[str, str, str]: ) -> tuple[str, str, str]:
@ -203,7 +204,7 @@ async def skill_research(
async def skill_analyze( async def skill_analyze(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
question: str, question: str,
document: str | None = None, document: str | None = None,
filter: str | None = None, filter: str | None = None,
@ -235,7 +236,7 @@ def update_documents_state(
documents_state.append(doc_info) 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): if ctx.deps and ctx.deps.state and isinstance(ctx.deps.state, state_type):
return ctx.deps.state return ctx.deps.state
return None return None
@ -243,8 +244,8 @@ def _get_state(ctx: RunContext[SkillRunDeps], state_type: type) -> Any:
def create_skill_tools( def create_skill_tools(
db_path: Path, db_path: Path,
config: Any, config: AppConfig,
state_type: type, state_type: type[BaseModel],
tool_names: list[str], tool_names: list[str],
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Create tool closures for a skill. """Create tool closures for a skill.

View file

@ -43,7 +43,7 @@ class TestValidateMetadata:
validate_metadata("Recipes", "A skill.") validate_metadata("Recipes", "A skill.")
def test_rejects_empty_name(self): def test_rejects_empty_name(self):
with pytest.raises(ValueError, match="name"): with pytest.raises(ValueError, match="identifier"):
validate_metadata("", "A skill.") validate_metadata("", "A skill.")
def test_rejects_not_identifier(self): def test_rejects_not_identifier(self):
@ -51,11 +51,11 @@ class TestValidateMetadata:
validate_metadata("123abc", "A skill.") validate_metadata("123abc", "A skill.")
def test_rejects_spaces_in_name(self): 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.") validate_metadata("my recipes", "A skill.")
def test_rejects_special_chars(self): def test_rejects_special_chars(self):
with pytest.raises(ValueError, match="name"): with pytest.raises(ValueError, match="identifier"):
validate_metadata("my@recipes", "A skill.") validate_metadata("my@recipes", "A skill.")
def test_rejects_empty_description(self): def test_rejects_empty_description(self):
@ -313,7 +313,7 @@ class TestGenerateSkill:
def test_rejects_invalid_name(self, tmp_path): def test_rejects_invalid_name(self, tmp_path):
db_path = _make_fake_lancedb(tmp_path / "test.lancedb") db_path = _make_fake_lancedb(tmp_path / "test.lancedb")
with pytest.raises(ValueError, match="name"): with pytest.raises(ValueError, match="identifier"):
generate_skill( generate_skill(
db_path=db_path, db_path=db_path,
output_dir=tmp_path, output_dir=tmp_path,