`ask(sources=["typo"])` reached the model, which discovered the name only if it searched: requests spent on a selection that could never answer, and a run that never searched answered anyway. Checked by name, not by opening: a client covering a set opens a database when a query reaches it, and validating by opening would open every one of them before any search, letting a database nobody asked about fail the run.
162 lines
5.2 KiB
Python
162 lines
5.2 KiB
Python
from collections.abc import Sequence
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from pydantic_ai import Agent
|
|
|
|
if TYPE_CHECKING:
|
|
from pydantic_ai.messages import BinaryContent
|
|
|
|
from haiku.rag.client import HaikuRAG
|
|
from haiku.rag.config.models import ModelConfig
|
|
from haiku.rag.sandbox import AnalysisResult
|
|
from haiku.rag.store.models.citation import Citation
|
|
|
|
|
|
@dataclass
|
|
class _AgentDeps:
|
|
state: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
def _build_user_prompt(
|
|
question: str,
|
|
images: Sequence[bytes] | None,
|
|
model_config: "ModelConfig",
|
|
) -> "str | list[str | BinaryContent]":
|
|
if not images:
|
|
return question
|
|
if not model_config.vision:
|
|
raise ValueError(
|
|
f"Model {model_config.provider}:{model_config.name} is not configured "
|
|
"for vision (set `vision: true` on the model config to pass images)."
|
|
)
|
|
from haiku.rag.utils import image_binary_content
|
|
|
|
return [question, *(image_binary_content(data) for data in images)]
|
|
|
|
|
|
async def ask(
|
|
client: "HaikuRAG",
|
|
question: str,
|
|
filter: str | None = None,
|
|
images: Sequence[bytes] | None = None,
|
|
sources: list[str] | None = None,
|
|
) -> "tuple[str, list[Citation]]":
|
|
"""Ask a question against the knowledge base via the RAG capability.
|
|
|
|
Args:
|
|
client: The HaikuRAG client.
|
|
question: The question to ask.
|
|
filter: SQL WHERE clause to filter documents.
|
|
images: Raw image bytes attached to the question (requires a
|
|
vision-capable QA model).
|
|
sources: Names of the databases to ask across. None asks across every
|
|
configured database.
|
|
|
|
Returns:
|
|
Tuple of (answer text, list of resolved citations).
|
|
"""
|
|
from haiku.rag.capabilities.rag import (
|
|
AGENT_PREAMBLE,
|
|
RAGState,
|
|
create_capability,
|
|
)
|
|
from haiku.rag.utils import get_model
|
|
|
|
# Validate names without opening lazily covered databases.
|
|
client._require_known_sources(sources)
|
|
|
|
# No `db_path`: the lent client is what the capability reads through, and it
|
|
# already knows which databases that is.
|
|
capability = create_capability(
|
|
config=client._config,
|
|
rag=client,
|
|
defer_loading=False,
|
|
)
|
|
deps = _AgentDeps(
|
|
state={
|
|
"rag": RAGState(document_filter=filter, sources=sources).model_dump(
|
|
mode="json"
|
|
)
|
|
}
|
|
)
|
|
user_prompt = _build_user_prompt(question, images, client._config.qa.model)
|
|
model = get_model(client._config.qa.model, client._config)
|
|
agent = Agent(
|
|
model,
|
|
deps_type=_AgentDeps,
|
|
instructions=AGENT_PREAMBLE,
|
|
capabilities=[capability],
|
|
)
|
|
result = await agent.run(user_prompt, deps=deps)
|
|
state = RAGState.model_validate(deps.state["rag"])
|
|
citations = [
|
|
state.citation_index[cid]
|
|
for cid in state.citations
|
|
if cid in state.citation_index
|
|
]
|
|
return result.output, citations
|
|
|
|
|
|
async def analyze(
|
|
client: "HaikuRAG",
|
|
question: str,
|
|
filter: str | None = None,
|
|
images: Sequence[bytes] | None = None,
|
|
sources: list[str] | None = None,
|
|
) -> "AnalysisResult":
|
|
"""Answer a question using the analysis capability.
|
|
|
|
The capability exposes search, code execution, and citation tools.
|
|
The driving model decides when to reach for code (structural traversal,
|
|
computation, aggregation) versus a direct ``search → cite → answer``.
|
|
|
|
Args:
|
|
client: The HaikuRAG client.
|
|
question: The question to answer.
|
|
filter: SQL WHERE clause to filter documents during searches.
|
|
images: Raw image bytes attached to the question (requires a
|
|
vision-capable analysis model).
|
|
sources: Names of the databases to analyze across. None covers every
|
|
configured database.
|
|
|
|
Returns:
|
|
AnalysisResult with the answer and resolved citations.
|
|
"""
|
|
from haiku.rag.capabilities.analysis import AnalysisState, create_capability
|
|
from haiku.rag.sandbox import AnalysisResult
|
|
from haiku.rag.utils import get_model
|
|
|
|
# Validate names without opening lazily covered databases.
|
|
client._require_known_sources(sources)
|
|
|
|
# No `db_path`: the lent client is what the capability reads through, and it
|
|
# already knows which databases that is.
|
|
capability = create_capability(
|
|
config=client._config,
|
|
rag=client,
|
|
defer_loading=False,
|
|
)
|
|
deps = _AgentDeps(
|
|
state={
|
|
"analysis": AnalysisState(
|
|
document_filter=filter, sources=sources
|
|
).model_dump(mode="json")
|
|
}
|
|
)
|
|
model_config = client._config.analysis.model or client._config.qa.model
|
|
user_prompt = _build_user_prompt(question, images, model_config)
|
|
model = get_model(model_config, client._config)
|
|
agent = Agent(
|
|
model,
|
|
deps_type=_AgentDeps,
|
|
capabilities=[capability],
|
|
)
|
|
result = await agent.run(user_prompt, deps=deps)
|
|
state = AnalysisState.model_validate(deps.state["analysis"])
|
|
citations = [
|
|
state.citation_index[cid]
|
|
for cid in state.citations
|
|
if cid in state.citation_index
|
|
]
|
|
return AnalysisResult(answer=result.output, citations=citations)
|