haiku.rag/tests/skills/test_rag.py
2026-04-20 09:48:18 +03:00

301 lines
11 KiB
Python

from haiku.rag.config.models import AppConfig
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.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, test_app_config, temp_db_path):
from haiku.rag.skills.rag import create_skill
skill = create_skill(config=test_app_config, 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 TestGetAgentPreamble:
def test_without_domain_preamble(self):
from haiku.rag.skills.rag import AGENT_PREAMBLE, get_agent_preamble
config = AppConfig()
assert get_agent_preamble(config) == AGENT_PREAMBLE
def test_with_domain_preamble(self):
from haiku.rag.config.models import PromptsConfig
from haiku.rag.skills.rag import AGENT_PREAMBLE, get_agent_preamble
config = AppConfig(
prompts=PromptsConfig(
domain_preamble="This knowledge base contains Helios solar panel documentation."
)
)
result = get_agent_preamble(config)
assert result.startswith(
"This knowledge base contains Helios solar panel documentation."
)
assert AGENT_PREAMBLE in result
class TestDomainPreambleInSkillInstructions:
def test_create_skill_without_domain_preamble(self, test_app_config, temp_db_path):
from haiku.rag.skills.rag import create_skill, instructions
skill = create_skill(config=test_app_config, db_path=temp_db_path)
assert skill.instructions == instructions()
def test_create_skill_with_domain_preamble(self, temp_db_path):
from haiku.rag.config.models import PromptsConfig
from haiku.rag.skills.rag import create_skill, instructions
config = AppConfig(
prompts=PromptsConfig(
domain_preamble="This knowledge base contains Helios solar panel documentation."
)
)
skill = create_skill(config=config, db_path=temp_db_path)
assert skill.instructions is not None
assert skill.instructions.startswith(
"This knowledge base contains Helios solar panel documentation."
)
base_instructions = instructions()
assert base_instructions is not None
assert base_instructions in skill.instructions
class TestRAGSkillCreation:
def test_create_skill_returns_valid_skill(self, test_app_config, temp_db_path):
from haiku.rag.skills.rag import create_skill
skill = create_skill(config=test_app_config, db_path=temp_db_path)
assert skill.metadata.name == "rag"
assert skill.metadata.description
assert skill.instructions
def test_create_skill_has_expected_tools(self, test_app_config, temp_db_path):
from haiku.rag.skills.rag import create_skill
skill = create_skill(config=test_app_config, db_path=temp_db_path)
tool_names = {getattr(t, "__name__") for t in skill.tools if callable(t)}
assert tool_names == {"search", "list_documents", "get_document", "cite"}
def test_create_skill_has_state(self, test_app_config, temp_db_path):
from haiku.rag.skills.rag import RAGState, create_skill
skill = create_skill(config=test_app_config, db_path=temp_db_path)
assert skill._state_type is RAGState
assert skill._state_namespace == "rag"
def test_create_skill_has_extras(self, test_app_config, temp_db_path):
from haiku.rag.skills.rag import create_skill
skill = create_skill(config=test_app_config, db_path=temp_db_path)
assert skill.extras["config"] is test_app_config
assert skill.extras["db_path"] is temp_db_path
assert "visualize_chunk" in skill.extras
assert "list_documents" in skill.extras
def test_create_skill_from_env(self, monkeypatch, temp_db_path):
monkeypatch.setenv("HAIKU_RAG_DB", str(temp_db_path))
from haiku.rag.skills.rag import create_skill
skill = create_skill()
assert skill.metadata.name == "rag"
class TestSkillExtras:
async def test_list_documents_returns_all(self, test_app_config, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(config=test_app_config, db_path=rag_db)
list_docs = skill.extras["list_documents"]
results = await list_docs()
assert len(results) == 2
assert all(k in results[0] for k in ("id", "title", "uri", "metadata"))
async def test_list_documents_with_filter(self, test_app_config, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(config=test_app_config, db_path=rag_db)
list_docs = skill.extras["list_documents"]
results = await list_docs(filter="title = 'AI Overview'")
assert len(results) == 1
assert results[0]["title"] == "AI Overview"
class TestSearchTool:
async def test_search_returns_formatted_string(self, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(db_path=rag_db)
search = _get_tool(skill, "search")
ctx = _make_ctx()
result = await search(ctx, query="artificial intelligence")
assert isinstance(result, str)
assert len(result) > 0
async def test_search_updates_state(self, rag_db):
from haiku.rag.skills.rag import RAGState, create_skill
skill = create_skill(db_path=rag_db)
search = _get_tool(skill, "search")
state = RAGState()
ctx = _make_ctx(state)
await search(ctx, query="artificial intelligence")
assert "artificial intelligence" in state.searches
results = state.searches["artificial intelligence"]
assert len(results) > 0
assert isinstance(results[0], SearchResult)
async def test_search_applies_document_filter_from_state(self, rag_db):
from haiku.rag.skills.rag import RAGState, create_skill
skill = create_skill(db_path=rag_db)
search = _get_tool(skill, "search")
state = RAGState(document_filter="title = 'AI Overview'")
ctx = _make_ctx(state)
result = await search(ctx, query="artificial intelligence")
assert "AI Overview" in result
assert "ML Basics" not in result
async def test_search_without_state(self, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(db_path=rag_db)
search = _get_tool(skill, "search")
ctx = _make_ctx(state=None)
result = await search(ctx, query="artificial intelligence")
assert isinstance(result, str)
class TestListDocumentsTool:
async def test_list_documents_returns_results(self, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(db_path=rag_db)
list_docs = _get_tool(skill, "list_documents")
ctx = _make_ctx()
results = await list_docs(ctx)
assert isinstance(results, list)
assert len(results) == 2
async def test_list_documents_applies_document_filter_from_state(self, rag_db):
from haiku.rag.skills.rag import RAGState, create_skill
skill = create_skill(db_path=rag_db)
list_docs = _get_tool(skill, "list_documents")
state = RAGState(document_filter="title = 'AI Overview'")
ctx = _make_ctx(state)
results = await list_docs(ctx)
assert len(results) == 1
assert results[0]["title"] == "AI Overview"
class TestGetDocumentTool:
async def test_get_document_by_title(self, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(db_path=rag_db)
get_doc = _get_tool(skill, "get_document")
ctx = _make_ctx()
result = await get_doc(ctx, query="AI Overview")
assert result is not None
assert result["title"] == "AI Overview"
async def test_get_document_not_found(self, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(db_path=rag_db)
get_doc = _get_tool(skill, "get_document")
ctx = _make_ctx()
result = await get_doc(ctx, query="nonexistent document xyz")
assert result is None
class TestCiteTool:
async def test_cite_registers_citations(self, rag_db):
from haiku.rag.skills.rag import RAGState, create_skill
skill = create_skill(db_path=rag_db)
search = _get_tool(skill, "search")
cite = _get_tool(skill, "cite")
state = RAGState()
ctx = _make_ctx(state)
await search(ctx, query="artificial intelligence")
chunk_ids = [
sr.chunk_id
for results in state.searches.values()
for sr in results
if sr.chunk_id
][:2]
result = await cite(ctx, chunk_ids=chunk_ids)
assert "Registered" in result
assert len(state.citations) == 1
assert len(state.citations[0]) == 2
assert all(cid in state.citation_index for cid in chunk_ids)
async def test_cite_deduplicates_in_index(self, rag_db):
from haiku.rag.skills.rag import RAGState, create_skill
skill = create_skill(db_path=rag_db)
search = _get_tool(skill, "search")
cite = _get_tool(skill, "cite")
state = RAGState()
ctx = _make_ctx(state)
await search(ctx, query="artificial intelligence")
chunk_ids = [
sr.chunk_id
for results in state.searches.values()
for sr in results
if sr.chunk_id
][:1]
await cite(ctx, chunk_ids=chunk_ids)
await cite(ctx, chunk_ids=chunk_ids)
assert len(state.citation_index) == 1
assert len(state.citations) == 2
async def test_cite_without_state(self, rag_db):
from haiku.rag.skills.rag import create_skill
skill = create_skill(db_path=rag_db)
cite = _get_tool(skill, "cite")
ctx = _make_ctx(state=None)
result = await cite(ctx, chunk_ids=["nonexistent"])
assert "No state" in result