7.2 KiB
7.2 KiB
Tools Extraction Refactoring Plan
Goal
Extract tools from haiku.rag agents into a reusable tools/ module, enabling users to create pydantic-ai agents outside haiku.rag and compose toolsets as needed.
Target API
from pydantic_ai import Agent
from haiku.rag import HaikuRAG
from haiku.rag.tools import ToolContext, create_search_toolset, create_document_toolset
async with HaikuRAG(db_path) as client:
context = ToolContext()
search_tools = create_search_toolset(client, config, context)
doc_tools = create_document_toolset(client, config, context)
agent = Agent(
'anthropic:claude-sonnet',
toolsets=[search_tools, doc_tools]
)
result = await agent.run("Find documents about X")
# Access accumulated state after run
search_state = context.get("haiku.rag.search")
for result in search_state.results:
print(f"{result.document_title}")
Design Principles
-
ToolContext is a pure generic container - No special-cased fields. Toolsets register their own Pydantic model state under namespaces.
-
Shared state via same namespace - Multiple toolsets can share state (e.g., citations, filters) by registering under the same namespace.
-
App manages identity - ToolContext has no session/user identity. The app layer manages
session_id -> ToolContextmapping. -
Toolsets are stateless factories -
create_*_toolset()returns aFunctionToolset. State lives in the context they're given.
ToolContext Design
class ToolContext(BaseModel):
"""Generic state container for toolsets.
Toolsets register Pydantic model state under namespaces.
Multiple toolsets can share state via the same namespace.
"""
_namespaces: dict[str, BaseModel] = PrivateAttr(default_factory=dict)
def register(self, namespace: str, state: BaseModel) -> None: ...
def get(self, namespace: str) -> BaseModel | None: ...
def get_or_create(self, namespace: str, factory: Callable[[], T]) -> T: ...
def clear_namespace(self, namespace: str) -> None: ...
def clear_all(self) -> None: ...
def dump_namespaces(self) -> dict[str, dict[str, Any]]: ...
def load_namespace(self, namespace: str, state_type: type[T], data: dict) -> T: ...
Toolset State Examples
Each toolset defines its own state model:
# Search toolset state
class SearchState(BaseModel):
results: list[SearchResult] = []
filter: str | None = None
SEARCH_NAMESPACE = "haiku.rag.search"
# QA toolset state
class QAState(BaseModel):
history: list[QAResult] = []
QA_NAMESPACE = "haiku.rag.qa"
# Shared citation state (used by multiple toolsets)
class CitationState(BaseModel):
registry: dict[str, int] = {}
def get_or_assign_index(self, chunk_id: str) -> int:
if chunk_id in self.registry:
return self.registry[chunk_id]
new_index = len(self.registry) + 1
self.registry[chunk_id] = new_index
return new_index
CITATION_NAMESPACE = "haiku.rag.citations"
Multi-User/Session Management
App layer manages context routing:
# App maintains context per session
contexts: dict[str, ToolContext] = {}
def get_context(session_id: str) -> ToolContext:
if session_id not in contexts:
contexts[session_id] = ToolContext()
return contexts[session_id]
# When running agent
context = get_context(user_session_id)
toolsets = [create_search_toolset(client, config, context)]
await agent.run(prompt, toolsets=toolsets)
New Module Structure
haiku_rag_slim/haiku/rag/
├── tools/ # NEW
│ ├── __init__.py # Public exports
│ ├── context.py # ToolContext (generic state container)
│ ├── models.py # QAResult, AnalysisResult
│ ├── filters.py # build_document_filter, combine_filters
│ ├── search.py # create_search_toolset()
│ ├── document.py # create_document_toolset()
│ ├── qa.py # create_qa_toolset()
│ └── analysis.py # create_analysis_toolset()
├── agents/ # REFACTORED to use tools/
Implementation Chunks
Chunk 1: Create tools module foundation ✅ DONE
- Created
tools/__init__.py,tools/context.py,tools/models.py,tools/filters.py - Created
ToolContextas generic namespace-based Pydantic model - Moved filter utilities from
agents/chat/state.pytotools/filters.py - Created result models (
QAResult,AnalysisResult) - Added tests for ToolContext and filters
Chunk 2: Create SearchToolset ✅ DONE
- Created
tools/search.pywithcreate_search_toolset() - Defined
SearchStatemodel for accumulating search results - Core search logic:
client.search()→client.expand_context()→format_for_agent() - Results accumulated in
SearchStateunderSEARCH_NAMESPACE - Added 13 tests for SearchToolset
Chunk 3: Refactor QA Agent to use SearchToolset ✅ DONE
- Updated
agents/qa/agent.pyto usecreate_search_toolset() - Added
base_filterandtool_nameparameters tocreate_search_toolset() - QA agent now uses ToolContext + SearchState for result accumulation
- Public interface (
answer(question, filter)) unchanged - All 5 QA tests pass
Chunk 4: Create DocumentToolset ✅ DONE
- Created
tools/document.pywithcreate_document_toolset() - Defined
DocumentState,DocumentInfo,DocumentListResponsemodels - Extracted
list_documents,get_document,summarize_documenttools - Moved
find_documenthelper (now public) - Added 13 tests
Chunk 5: Create QAToolset ✅ DONE
- Created
tools/qa.pywithcreate_qa_toolset() - Defined
QAStatemodel (tracks QA history) - Runs research graph, returns structured
QAResult - Supports
base_filter,tool_name,session_context,prior_answersparams - Added 7 tests
Chunk 6: Create AnalysisToolset ✅ DONE
- Created
tools/analysis.pywithcreate_analysis_toolset() - Defined
AnalysisStatemodel (tracks CodeExecution history) - Extracted
analyzetool (RLM delegation with filter support) - Fixed circular import by using direct submodule imports
- Added 6 tests
Chunk 7: Refactor Chat Agent ✅ DONE
- Removed
analyzetool from chat agent (kept hardcoded, not composing toolsets) - Reverted system prompt to pre-analyze version
- Removed
test_analyze_tooltest and cassette file - All 47 chat agent tests pass
Chunk 8: Refactor Research Graph
- Update
_search_one_step_logicto use search toolset - Verify research tests pass
Chunk 9: Public API and Documentation
- Export from
haiku.rag.toolsandhaiku.rag - Update CLAUDE.md
- Add usage examples
Verification
- Run
pytestafter each chunk - Run
ty checkandruff check - Test with existing agents (QA, Chat, Research)
- Test with external agent using new toolsets
Critical Files
haiku_rag_slim/haiku/rag/agents/chat/agent.py- largest tool collectionhaiku_rag_slim/haiku/rag/agents/qa/agent.py- simplest, good starting pointhaiku_rag_slim/haiku/rag/agents/chat/state.py- filter utilities (now moved)haiku_rag_slim/haiku/rag/agents/research/graph.py- search tool inside stephaiku_rag_slim/haiku/rag/store/models/chunk.py- SearchResult.format_for_agent()