haiku.rag/docs/agents.md

312 lines
11 KiB
Markdown

# Agents
Four agentic flows are provided by haiku.rag:
- **Simple QA Agent** — a focused question answering agent
- **Chat Agent** — multi-turn conversational RAG with session memory
- **Research Graph** — a multi-step research workflow with question decomposition
- **RLM Agent** — complex analytical tasks via sandboxed Python code execution (see [RLM Agent](rlm.md))
See [QA and Research Configuration](configuration/qa-research.md) for configuring model, iterations, concurrency, and other settings.
## Simple QA Agent
The simple QA agent answers a single question using the knowledge base. It retrieves relevant chunks, optionally expands context around them, and asks the model to answer strictly based on that context.
Key points:
- Uses a single `search_documents` tool to fetch relevant chunks
- Can be run with or without inline citations in the prompt
- Returns a plain string answer
**CLI usage:**
```bash
haiku-rag ask "What is climate change?"
# With citations
haiku-rag ask "What is climate change?" --cite
# Deep mode (uses research graph with optimized settings)
haiku-rag ask "What are the main features of haiku.rag?" --deep
```
**Python usage:**
```python
from haiku.rag.client import HaikuRAG
from haiku.rag.agents.qa.agent import QuestionAnswerAgent
async with HaikuRAG(path_to_db) as client:
agent = QuestionAnswerAgent(
client=client,
provider="openai",
model="gpt-4o-mini",
use_citations=False,
)
answer = await agent.answer("What is climate change?")
print(answer)
```
## Chat Agent
The chat agent enables multi-turn conversational RAG. It is built from composable [toolsets](tools.md) and maintains session state to improve follow-up answers.
Key features:
- **Composable toolsets**: Built from reusable `FunctionToolset` factories — see [Toolsets](tools.md)
- **Semantic prior answer recall**: Similar prior Q/A pairs are retrieved and passed to the research planner, which can skip searching when they suffice
- **Background summarization**: After each `ask` call, the QA history is summarized into a compact session context for the next request
- **Document filtering**: Session-level or per-query document filtering
### CLI Usage
```bash
haiku-rag chat
haiku-rag chat --db /path/to/database.lancedb
```
See [Applications](apps.md#chat-tui) for the full TUI interface guide.
### Python Usage
```python
from haiku.rag.client import HaikuRAG
from haiku.rag.agents.chat import create_chat_agent, ChatDeps
from haiku.rag.tools import ToolContext
async with HaikuRAG(path_to_db) as client:
# Create agent with composed toolsets
context = ToolContext()
agent = create_chat_agent(config, client, context)
deps = ChatDeps(config=config, tool_context=context)
# First question
result = await agent.run("What is haiku.rag?", deps=deps)
print(result.output)
# Follow-up (uses session context)
result = await agent.run("How does it handle PDFs?", deps=deps)
print(result.output)
```
### Feature Selection
By default, `create_chat_agent` enables search, documents, and QA toolsets. You can customize which capabilities the agent has via the `features` parameter:
```python
from haiku.rag.agents.chat import (
create_chat_agent,
FEATURE_SEARCH,
FEATURE_DOCUMENTS,
FEATURE_QA,
FEATURE_ANALYSIS,
)
# Search-only agent
agent = create_chat_agent(config, client, context, features=[FEATURE_SEARCH])
# All features including code analysis
agent = create_chat_agent(
config, client, context,
features=[FEATURE_SEARCH, FEATURE_DOCUMENTS, FEATURE_QA, FEATURE_ANALYSIS],
)
```
Available features:
| Feature | Constant | Tools added |
|---------|----------|-------------|
| Search | `FEATURE_SEARCH` | `search` |
| Documents | `FEATURE_DOCUMENTS` | `list_documents`, `get_document`, `summarize_document` |
| QA | `FEATURE_QA` | `ask` |
| Analysis | `FEATURE_ANALYSIS` | `analyze` |
The system prompt is automatically composed to match the selected features. See [Toolsets](tools.md) for details on each toolset's parameters and behavior.
### Session State
The `ChatSessionState` maintains:
- `session_id` — Unique identifier for the session
- `qa_history` — List of previous Q/A pairs
- `session_context` — Automatically maintained session context summary
- `document_filter` — List of document titles/URIs to restrict searches
- `citation_registry` — Stable mapping of chunk IDs to citation indices
**Citation Registry**: Citation indices persist across tool calls within a session. The same `chunk_id` always returns the same citation index (first-occurrence-wins). This ensures consistent citation numbering in multi-turn conversations — `[1]` always refers to the same source.
### Conversational Memory
The chat agent maintains two layers of conversational memory:
**1. Semantic prior answer recall**
When the `ask` tool receives a question, it embeds the question and compares it against prior Q/A embeddings. Sufficiently similar prior answers are passed to the research planner, which can skip searching entirely if they already cover the question.
**2. Background session summarization**
After each `ask` call, a background task summarizes the full QA history into a compact session context. This summary is injected into the research planner on the next request, allowing it to resolve ambiguous references ("Tell me more about the authentication part") without having seen the full conversation.
## Research Graph
The research workflow is implemented as a typed pydantic-graph. It uses an iterative feedback loop where the planner proposes one question at a time, sees the answer, then decides whether to continue or synthesize.
```mermaid
---
title: Research graph
---
stateDiagram-v2
state plan_next_decision <<choice>>
[*] --> plan_next
plan_next --> plan_next_decision
plan_next_decision --> search_one: Has next question
plan_next_decision --> synthesize: Complete or max iterations
search_one --> plan_next: Answer added to context
synthesize --> [*]
note right of plan_next
Receives session_context as background
and prior_answers from conversation history.
Uses a different prompt when prior answers exist.
end note
```
The graph receives a `ResearchContext` containing:
- `original_question` — the user's question
- `session_context` — summary of conversation history (injected as `<background>` XML)
- `qa_responses` — prior answers from semantic matching or previous iterations (injected as `<prior_answers>` XML)
When prior answers are provided, the planner uses a context-aware prompt that evaluates whether existing evidence is sufficient. If it is, the planner marks `is_complete=True` and the graph skips directly to synthesis without any searches.
**Key nodes:**
- **plan_next**: Evaluates gathered evidence and either proposes the next question to investigate or marks research as complete. Uses a context-aware prompt when prior answers exist, allowing it to skip research entirely.
- **search_one**: Answers a single question using the knowledge base (up to 3 search calls per question). Each answer is added to `ResearchContext.qa_responses` for the next planning iteration.
- **synthesize**: Generates the final output from all gathered evidence.
**Output modes:**
The graph supports two output modes via `build_research_graph(output_mode=...)`:
| Mode | Output type | Used by |
|------|-------------|---------|
| `"report"` | `ResearchReport` (title, executive summary, findings, conclusions, recommendations) | CLI `haiku-rag research`, Python API |
| `"conversational"` | `ConversationalAnswer` (answer, citations, confidence) | Chat agent's `ask` tool |
**Iterative flow:**
- Each iteration: planner evaluates context → proposes one question → search answers it → loop back
- Planner can decompose complex questions (e.g., "benefits and drawbacks" → start with "benefits")
- Session context resolves ambiguous references and informs planning
- Prior answers let the planner skip redundant searches
- Loop terminates when planner marks `is_complete=True` or `max_iterations` is reached
### CLI Usage
```bash
# Basic usage
haiku-rag research "How does haiku.rag organize and query documents?"
# With document filter
haiku-rag research "What are the key findings?" --filter "uri LIKE '%report%'"
```
### Python Usage
**Basic example:**
```python
from haiku.rag.client import HaikuRAG
from haiku.rag.config import Config
from haiku.rag.agents.research.dependencies import ResearchContext
from haiku.rag.agents.research.graph import build_research_graph
from haiku.rag.agents.research.state import ResearchDeps, ResearchState
async with HaikuRAG(path_to_db) as client:
graph = build_research_graph(config=Config)
context = ResearchContext(original_question="What are the main features?")
state = ResearchState.from_config(context=context, config=Config)
deps = ResearchDeps(client=client)
report = await graph.run(state=state, deps=deps)
print(report.title)
print(report.executive_summary)
```
**With custom config:**
```python
from haiku.rag.client import HaikuRAG
from haiku.rag.config.models import AppConfig, ResearchConfig
from haiku.rag.agents.research.dependencies import ResearchContext
from haiku.rag.agents.research.graph import build_research_graph
from haiku.rag.agents.research.state import ResearchDeps, ResearchState
custom_config = AppConfig(
research=ResearchConfig(
provider="openai",
model="gpt-4o-mini",
max_iterations=5,
max_concurrency=3,
)
)
async with HaikuRAG(path_to_db) as client:
graph = build_research_graph(config=custom_config)
context = ResearchContext(original_question="What are the main features?")
state = ResearchState.from_config(context=context, config=custom_config)
deps = ResearchDeps(client=client)
report = await graph.run(state=state, deps=deps)
```
**Conversational mode with prior answers:**
```python
from haiku.rag.agents.research.dependencies import ResearchContext
from haiku.rag.agents.research.graph import build_research_graph
from haiku.rag.agents.research.models import SearchAnswer
from haiku.rag.agents.research.state import ResearchDeps, ResearchState
# Conversational mode returns ConversationalAnswer instead of ResearchReport
graph = build_research_graph(config=Config, output_mode="conversational")
# Pass session context and prior answers from conversation history
context = ResearchContext(
original_question="How does it handle authentication?",
session_context="User is building a Python web app with FastAPI.",
qa_responses=[
SearchAnswer(
query="What authentication methods are supported?",
answer="JWT and OAuth2 are supported.",
confidence=0.95,
cited_chunks=["chunk-1"],
)
],
)
state = ResearchState.from_config(context=context, config=Config)
deps = ResearchDeps(client=client)
result = await graph.run(state=state, deps=deps)
print(result.answer) # Direct conversational answer
print(result.confidence) # 0.0-1.0
print(result.citations) # Deduplicated citations from all searches
```
### Filtering Documents
Restrict searches to specific documents via the `search_filter` parameter:
```python
# Set filter before running the graph
state = ResearchState.from_config(context=context, config=Config)
state.search_filter = "id IN ('doc-123', 'doc-456')"
report = await graph.run(state=state, deps=deps)
```
The filter applies to all search operations in the graph. See [Filtering Search Results](python.md#filtering-search-results) for available filter columns and syntax.