Merge pull request #299 from ggozad/feat/skill-meta

Expose skill state metadata as module-level API
This commit is contained in:
Yiorgis Gozadinos 2026-03-04 13:17:52 +02:00 committed by GitHub
commit 2358c14028
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 165 additions and 23 deletions

View file

@ -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

View file

@ -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,
)

View file

@ -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,
)

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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]]