From 3f02abbc70c6cdf1015b8b44a4742874776f199a Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Wed, 4 Mar 2026 11:36:20 +0200 Subject: [PATCH] Expose skill state metadata as module-level API --- CHANGELOG.md | 8 +++++ haiku_rag_slim/haiku/rag/skills/rag.py | 42 +++++++++++++++++++------ haiku_rag_slim/haiku/rag/skills/rlm.py | 42 +++++++++++++++++++------ haiku_rag_slim/pyproject.toml | 2 +- tests/skills/test_rag.py | 43 ++++++++++++++++++++++++++ tests/skills/test_rlm.py | 43 ++++++++++++++++++++++++++ uv.lock | 8 ++--- 7 files changed, 165 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54369116..b97904b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog ## [Unreleased] +### Added + +- **Module-level skill introspection API**: `STATE_TYPE`, `STATE_NAMESPACE`, `skill_metadata()`, `instructions()`, and `state_metadata()` on `haiku.rag.skills.rag` and `haiku.rag.skills.rlm` — allows introspecting skill configuration without calling `create_skill()` + +### Changed + +- **`haiku.skills` dependency**: Bumped to `>=0.7.0` for `StateMetadata` dataclass + ## [0.32.3] - 2026-03-03 ### Changed diff --git a/haiku_rag_slim/haiku/rag/skills/rag.py b/haiku_rag_slim/haiku/rag/skills/rag.py index fd8d0308..01283b4d 100644 --- a/haiku_rag_slim/haiku/rag/skills/rag.py +++ b/haiku_rag_slim/haiku/rag/skills/rag.py @@ -1,4 +1,5 @@ import os +from functools import cache from pathlib import Path from typing import Any @@ -9,7 +10,7 @@ from haiku.rag.agents.research.models import Citation from haiku.rag.store.models.chunk import SearchResult from haiku.rag.tools.document import DocumentInfo from haiku.rag.tools.qa import QAHistoryEntry -from haiku.skills.models import Skill, SkillSource +from haiku.skills.models import Skill, SkillMetadata, SkillSource, StateMetadata from haiku.skills.parser import parse_skill_md from haiku.skills.state import SkillRunDeps @@ -37,6 +38,32 @@ class RAGState(BaseModel): reports: list[ResearchEntry] = Field(default_factory=list) +STATE_TYPE = RAGState +STATE_NAMESPACE = "rag" + +_skill_path = Path(__file__).parent / "rag" + + +@cache +def skill_metadata() -> SkillMetadata: + metadata, _ = parse_skill_md(_skill_path / "SKILL.md") + return metadata + + +@cache +def instructions() -> str | None: + _, instr = parse_skill_md(_skill_path / "SKILL.md") + return instr + + +def state_metadata() -> StateMetadata: + return StateMetadata( + namespace=STATE_NAMESPACE, + type=STATE_TYPE, + schema=STATE_TYPE.model_json_schema(), + ) + + def create_skill( db_path: Path | None = None, config: Any = None, @@ -62,9 +89,6 @@ def create_skill( else: db_path = config.storage.data_dir / "haiku.rag.lancedb" - path = Path(__file__).parent / "rag" - metadata, instructions = parse_skill_md(path / "SKILL.md") - async def _find_relevant_prior_qa( state: RAGState, query: str ) -> list[QAHistoryEntry]: @@ -323,10 +347,10 @@ def create_skill( return "\n".join(parts) return Skill( - metadata=metadata, + metadata=skill_metadata(), source=SkillSource.ENTRYPOINT, - path=path, - instructions=instructions, + path=_skill_path, + instructions=instructions(), tools=[ search, list_documents, @@ -334,6 +358,6 @@ def create_skill( ask, research, ], - state_type=RAGState, - state_namespace="rag", + state_type=STATE_TYPE, + state_namespace=STATE_NAMESPACE, ) diff --git a/haiku_rag_slim/haiku/rag/skills/rlm.py b/haiku_rag_slim/haiku/rag/skills/rlm.py index 60f75e00..daa8c7cd 100644 --- a/haiku_rag_slim/haiku/rag/skills/rlm.py +++ b/haiku_rag_slim/haiku/rag/skills/rlm.py @@ -1,11 +1,12 @@ import os +from functools import cache from pathlib import Path from typing import Any from pydantic import BaseModel from pydantic_ai import RunContext -from haiku.skills.models import Skill, SkillSource +from haiku.skills.models import Skill, SkillMetadata, SkillSource, StateMetadata from haiku.skills.parser import parse_skill_md from haiku.skills.state import SkillRunDeps @@ -20,6 +21,32 @@ class RLMState(BaseModel): analyses: list[AnalysisEntry] = [] +STATE_TYPE = RLMState +STATE_NAMESPACE = "rlm" + +_skill_path = Path(__file__).parent / "rag-rlm" + + +@cache +def skill_metadata() -> SkillMetadata: + metadata, _ = parse_skill_md(_skill_path / "SKILL.md") + return metadata + + +@cache +def instructions() -> str | None: + _, instr = parse_skill_md(_skill_path / "SKILL.md") + return instr + + +def state_metadata() -> StateMetadata: + return StateMetadata( + namespace=STATE_NAMESPACE, + type=STATE_TYPE, + schema=STATE_TYPE.model_json_schema(), + ) + + def create_skill( db_path: Path | None = None, config: Any = None, @@ -45,9 +72,6 @@ def create_skill( else: db_path = config.storage.data_dir / "haiku.rag.lancedb" - path = Path(__file__).parent / "rag-rlm" - metadata, instructions = parse_skill_md(path / "SKILL.md") - async def analyze( ctx: RunContext[SkillRunDeps], question: str, @@ -85,11 +109,11 @@ def create_skill( return output return Skill( - metadata=metadata, + metadata=skill_metadata(), source=SkillSource.ENTRYPOINT, - path=path, - instructions=instructions, + path=_skill_path, + instructions=instructions(), tools=[analyze], - state_type=RLMState, - state_namespace="rlm", + state_type=STATE_TYPE, + state_namespace=STATE_NAMESPACE, ) diff --git a/haiku_rag_slim/pyproject.toml b/haiku_rag_slim/pyproject.toml index 53e2d642..3021d6b0 100644 --- a/haiku_rag_slim/pyproject.toml +++ b/haiku_rag_slim/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ dependencies = [ "cachetools>=5.5.0", "docling-core==2.65.1", - "haiku.skills>=0.6.0", + "haiku.skills>=0.7.0", "httpx>=0.28.1", "jsonpatch>=1.33", "lancedb==0.29.2", diff --git a/tests/skills/test_rag.py b/tests/skills/test_rag.py index a53f9da5..ce2b1b32 100644 --- a/tests/skills/test_rag.py +++ b/tests/skills/test_rag.py @@ -2,13 +2,56 @@ from unittest.mock import AsyncMock from haiku.rag.agents.research.models import Citation, ResearchReport from haiku.rag.client import HaikuRAG +from haiku.rag.skills.rag import ( + STATE_NAMESPACE, + STATE_TYPE, + RAGState, + instructions, + skill_metadata, + state_metadata, +) from haiku.rag.store.models.chunk import SearchResult from haiku.rag.tools.document import DocumentInfo from haiku.rag.tools.qa import QAHistoryEntry +from haiku.skills.models import SkillMetadata, StateMetadata from .conftest import _get_tool, _make_ctx +class TestRAGModuleAPI: + def test_state_type_is_rag_state(self): + assert STATE_TYPE is RAGState + + def test_state_namespace(self): + assert STATE_NAMESPACE == "rag" + + def test_state_metadata_returns_state_metadata(self): + result = state_metadata() + assert isinstance(result, StateMetadata) + assert result.namespace == "rag" + assert result.type is RAGState + assert result.schema == RAGState.model_json_schema() + + def test_skill_metadata_returns_skill_metadata(self): + result = skill_metadata() + assert isinstance(result, SkillMetadata) + assert result.name == "rag" + + def test_instructions_returns_string(self): + result = instructions() + assert isinstance(result, str) + assert len(result) > 0 + + def test_constants_match_create_skill(self, temp_db_path): + from haiku.rag.skills.rag import create_skill + + skill = create_skill(db_path=temp_db_path) + assert skill.state_type is STATE_TYPE + assert skill.state_namespace == STATE_NAMESPACE + assert skill.metadata == skill_metadata() + assert skill.instructions == instructions() + + class TestRAGSkillCreation: def test_create_skill_returns_valid_skill(self, temp_db_path): from haiku.rag.skills.rag import create_skill diff --git a/tests/skills/test_rlm.py b/tests/skills/test_rlm.py index 03b60e13..6e3317d6 100644 --- a/tests/skills/test_rlm.py +++ b/tests/skills/test_rlm.py @@ -2,10 +2,53 @@ from unittest.mock import AsyncMock from haiku.rag.agents.rlm.models import RLMResult from haiku.rag.client import HaikuRAG +from haiku.rag.skills.rlm import ( + STATE_NAMESPACE, + STATE_TYPE, + RLMState, + instructions, + skill_metadata, + state_metadata, +) +from haiku.skills.models import SkillMetadata, StateMetadata from .conftest import _get_tool, _make_ctx +class TestRLMModuleAPI: + def test_state_type_is_rlm_state(self): + assert STATE_TYPE is RLMState + + def test_state_namespace(self): + assert STATE_NAMESPACE == "rlm" + + def test_state_metadata_returns_state_metadata(self): + result = state_metadata() + assert isinstance(result, StateMetadata) + assert result.namespace == "rlm" + assert result.type is RLMState + assert result.schema == RLMState.model_json_schema() + + def test_skill_metadata_returns_skill_metadata(self): + result = skill_metadata() + assert isinstance(result, SkillMetadata) + assert result.name == "rag-rlm" + + def test_instructions_returns_string(self): + result = instructions() + assert isinstance(result, str) + assert len(result) > 0 + + def test_constants_match_create_skill(self, temp_db_path): + from haiku.rag.skills.rlm import create_skill + + skill = create_skill(db_path=temp_db_path) + assert skill.state_type is STATE_TYPE + assert skill.state_namespace == STATE_NAMESPACE + assert skill.metadata == skill_metadata() + assert skill.instructions == instructions() + + class TestRLMSkillCreation: def test_create_skill_returns_valid_skill(self, temp_db_path): from haiku.rag.skills.rlm import create_skill diff --git a/uv.lock b/uv.lock index 71b705b5..8da54baa 100644 --- a/uv.lock +++ b/uv.lock @@ -1490,7 +1490,7 @@ requires-dist = [ { name = "cohere", marker = "extra == 'cohere'", specifier = ">=5.20.1" }, { name = "docling", marker = "extra == 'docling'", specifier = "==2.73.1" }, { name = "docling-core", specifier = "==2.65.1" }, - { name = "haiku-skills", specifier = ">=0.6.0" }, + { name = "haiku-skills", specifier = ">=0.7.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "jsonpatch", specifier = ">=1.33" }, { name = "lancedb", specifier = "==0.29.2" }, @@ -1522,7 +1522,7 @@ provides-extras = ["docling", "voyageai", "mxbai", "cohere", "zeroentropy", "jin [[package]] name = "haiku-skills" -version = "0.6.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -1530,9 +1530,9 @@ dependencies = [ { name = "pyyaml" }, { name = "skills-ref" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/0f/97d95a6814cec171d97ca7eec6365468f6cbdbfc9fbb2d6346494035aa8a/haiku_skills-0.6.0.tar.gz", hash = "sha256:8352c9157260742b475315f92191fe0c9149aa0faa8c9d20af8c201f1bc71e87", size = 132772, upload-time = "2026-03-03T08:55:59.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/4fbdfe95e6ff6a574088b9f3ed7543a6e01b4b6c1740440ce07c1cf4893a/haiku_skills-0.7.0.tar.gz", hash = "sha256:ec5c5176f8feab09cc6aa4cda9a64e2446074f3ed111cb7e2587f7b524711364", size = 133420, upload-time = "2026-03-04T09:25:52.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/c4/f9f8892da06bdc2defb12191e765486dd8dd87155c63dc6ca045f75c8211/haiku_skills-0.6.0-py3-none-any.whl", hash = "sha256:adfbe2206eb238abb0dfe376ffede669860ffd45a2f4647090dfbd787842cf7f", size = 24631, upload-time = "2026-03-03T08:55:58.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/38/5048252c0b68bc77dd0fcf6b5c4e29074d90b3f6888eb282a0fcae967227/haiku_skills-0.7.0-py3-none-any.whl", hash = "sha256:7b7791890cc230fd53c5a9a7d93129e7901fecc1d64153d0386a83603e5b7b25", size = 25022, upload-time = "2026-03-04T09:25:51.872Z" }, ] [[package]]