haiku.rag/haiku_rag_slim/haiku/rag/tools/analysis.py
2026-02-10 14:38:13 +02:00

117 lines
3.9 KiB
Python

from pydantic import BaseModel
from pydantic_ai import FunctionToolset
from haiku.rag.agents.rlm.agent import create_rlm_agent
from haiku.rag.agents.rlm.dependencies import RLMContext, RLMDeps
from haiku.rag.agents.rlm.docker_sandbox import DockerSandbox
from haiku.rag.client import HaikuRAG
from haiku.rag.config.models import AppConfig
from haiku.rag.tools.context import ToolContext
from haiku.rag.tools.filters import (
build_document_filter,
build_multi_document_filter,
combine_filters,
)
from haiku.rag.tools.models import AnalysisResult
from haiku.rag.tools.session import SESSION_NAMESPACE, SessionState
ANALYSIS_NAMESPACE = "haiku.rag.analysis"
class AnalysisState(BaseModel):
"""State for analysis toolset.
Tracks programs produced across tool invocations.
"""
programs: list[str] = []
def create_analysis_toolset(
client: HaikuRAG,
config: AppConfig,
context: ToolContext | None = None,
base_filter: str | None = None,
tool_name: str = "analyze",
) -> FunctionToolset:
"""Create a toolset with code analysis capabilities via RLM agent.
Args:
client: HaikuRAG client for document operations.
config: Application configuration.
context: Optional ToolContext for state accumulation.
If provided, code executions are tracked in AnalysisState.
If SessionState is registered, it will be used for dynamic
document filtering.
base_filter: Optional base SQL WHERE clause applied to searches.
tool_name: Name for the analyze tool. Defaults to "analyze".
Returns:
FunctionToolset with an analyze tool.
"""
# Get or create analysis state if context provided
state: AnalysisState | None = None
if context is not None:
state = context.get_or_create(ANALYSIS_NAMESPACE, AnalysisState)
async def analyze(
task: str,
document_name: str | None = None,
) -> AnalysisResult:
"""Execute a computational task via code execution.
Uses the RLM (Recursive Language Model) agent to write and execute
Python code to answer the task.
Args:
task: A specific, actionable instruction describing what to compute.
document_name: Optional document name/title to focus on.
Returns:
AnalysisResult with answer and execution metadata.
"""
# Get session filter from session state
session_filter = None
if context is not None:
session_state = context.get_typed(SESSION_NAMESPACE, SessionState)
if session_state is not None and session_state.document_filter:
session_filter = build_multi_document_filter(
session_state.document_filter
)
# Build filter from base_filter, session_filter, and document_name
doc_filter = build_document_filter(document_name) if document_name else None
effective_filter = combine_filters(
combine_filters(base_filter, session_filter), doc_filter
)
# Create RLM context
rlm_context = RLMContext(filter=effective_filter)
# Run RLM agent with Docker sandbox
async with DockerSandbox(
client=client,
config=config.rlm,
context=rlm_context,
image=config.rlm.docker_image,
) as sandbox:
deps = RLMDeps(
sandbox=sandbox,
context=rlm_context,
)
rlm_agent = create_rlm_agent(config)
result = await rlm_agent.run(task, deps=deps)
program = result.output.program
if state is not None and program:
state.programs.append(program)
return AnalysisResult(
answer=result.output.answer,
code_executed=bool(program),
)
toolset = FunctionToolset()
toolset.add_function(analyze, name=tool_name)
return toolset