179 lines
6.5 KiB
Markdown
179 lines
6.5 KiB
Markdown
# RAG Skill
|
||
|
||
The `rag` skill answers questions over a knowledge base with hybrid search, structure-aware context expansion, and explicit citations. `client.ask`, `haiku-rag ask`, the MCP `ask_question` tool, and the chat TUI all run through this skill.
|
||
|
||
## When to use it
|
||
|
||
- The model needs to find and quote evidence from a document corpus.
|
||
- You want citations under every answer.
|
||
- You're building a Q&A agent, a documentation chatbot, or any RAG-style integration.
|
||
|
||
If the question requires *computation* over the corpus (counts, aggregates, comparisons, section-scoped reading), reach for the [Analysis skill](analysis.md) instead, or attach both.
|
||
|
||
## Tools
|
||
|
||
| Tool | Purpose |
|
||
|------|---------|
|
||
| `search(query, limit?)` | Hybrid search (vector + full-text) with section-aware context expansion. Returns `chunk_id`, content, `doc_item_refs`, `picture_refs`, `picture_captions`, source metadata. |
|
||
| `cite(chunk_ids)` | Register chunk IDs as citations for the current answer. The agent calls this before writing the final response. |
|
||
|
||
For corpus enumeration or full-document reads, reach for the [Analysis skill](analysis.md), which exposes `await list_documents()` and a `/documents/{id}/content.txt` virtual filesystem inside `execute_code`. Both are also available as opt-in tools when building a [custom skill](custom.md).
|
||
|
||
## State
|
||
|
||
The skill manages a `RAGState` under the `"rag"` namespace:
|
||
|
||
```python
|
||
class RAGState(BaseModel):
|
||
citation_index: dict[str, Citation] = {}
|
||
citations: list[str] = []
|
||
document_filter: str | None = None
|
||
searches: dict[str, list[SearchResult]] = {}
|
||
```
|
||
|
||
- **citation_index** — All citations indexed by chunk ID. Accumulates across invocations so historical chunk IDs stay resolvable in UI scrollback.
|
||
- **citations** — Chunk IDs registered via `cite` during the current invocation. Deduplicated, cleared at the start of each invocation.
|
||
- **document_filter** — SQL WHERE clause applied to `search`. Persists across invocations.
|
||
- **searches** — Search results keyed by query string. Cleared at the start of each invocation.
|
||
|
||
## `create_skill(db_path?, config?)`
|
||
|
||
```python
|
||
from haiku.rag.skills.rag import create_skill
|
||
|
||
skill = create_skill(db_path="my.lancedb")
|
||
```
|
||
|
||
| Parameter | Default | Description |
|
||
|-----------|---------|-------------|
|
||
| `db_path` | `None` | Path to LanceDB database. Falls back to `HAIKU_RAG_DB` env var, then config default. |
|
||
| `config` | `None` | `AppConfig` instance. Falls back to `get_config()`. |
|
||
|
||
## Examples
|
||
|
||
### Minimal agent
|
||
|
||
```python
|
||
from haiku.rag.skills.rag import create_skill
|
||
from haiku.skills.agent import SkillToolset
|
||
from haiku.skills.prompts import build_system_prompt
|
||
from pydantic_ai import Agent
|
||
|
||
rag = create_skill(db_path="my.lancedb")
|
||
toolset = SkillToolset(skills=[rag])
|
||
|
||
agent = Agent(
|
||
"openai-chat:gpt-4o",
|
||
instructions=build_system_prompt(toolset.skill_catalog),
|
||
toolsets=[toolset],
|
||
)
|
||
|
||
result = await agent.run("What does the manual say about safety procedures?")
|
||
print(result.output)
|
||
|
||
# Inspect what the model cited
|
||
state = toolset.get_namespace("rag")
|
||
for chunk_id in state.citations:
|
||
citation = state.citation_index[chunk_id]
|
||
print(f"- {citation.document_title}: {citation.content[:100]}…")
|
||
```
|
||
|
||
### Domain customization
|
||
|
||
Set a domain preamble in `haiku.rag.yaml` and the skill picks it up:
|
||
|
||
```yaml
|
||
prompts:
|
||
domain_preamble: |
|
||
The knowledge base contains the operations manual for the Helios solar array.
|
||
"The array" or unqualified specs refer to Helios. Terminology like "string"
|
||
refers to a series-connected panel chain, not text.
|
||
```
|
||
|
||
To scope a session to a subset of documents, set the filter on the namespace state:
|
||
|
||
```python
|
||
state = toolset.get_namespace("rag")
|
||
state.document_filter = "uri LIKE '%helios/v4/%'"
|
||
|
||
result = await agent.run("What's the maintenance interval for the inverters?")
|
||
```
|
||
|
||
The filter applies to every `search` call for the rest of the session, and the model can't bypass it from inside.
|
||
|
||
### Combining with the analysis skill
|
||
|
||
Attach both skills and the agent routes between them:
|
||
|
||
```python
|
||
from haiku.rag.skills.rag import create_skill as create_rag_skill
|
||
from haiku.rag.skills.analysis import create_skill as create_analysis_skill
|
||
|
||
rag = create_rag_skill(db_path="my.lancedb")
|
||
analysis = create_analysis_skill(db_path="my.lancedb")
|
||
toolset = SkillToolset(skills=[rag, analysis])
|
||
|
||
agent = Agent(
|
||
"openai-chat:gpt-4o",
|
||
instructions=build_system_prompt(toolset.skill_catalog),
|
||
toolsets=[toolset],
|
||
)
|
||
|
||
# Q&A → uses rag
|
||
await agent.run("What safety equipment is required on-site?")
|
||
|
||
# Computational question → uses rag-analysis
|
||
await agent.run("How many checklists mention torque specifications?")
|
||
```
|
||
|
||
### Streaming to a web frontend
|
||
|
||
Wrap the agent with `AGUIAdapter` to stream tool calls, text deltas, and state changes to a CopilotKit-style frontend:
|
||
|
||
```python
|
||
from pydantic_ai.ui.ag_ui import AGUIAdapter
|
||
|
||
adapter = AGUIAdapter(agent=agent, run_input=run_input)
|
||
sse_stream = adapter.encode_stream(adapter.run_stream())
|
||
```
|
||
|
||
See the [Web application](../apps.md) reference implementation for the full Starlette + Next.js setup.
|
||
|
||
### Exposing via MCP
|
||
|
||
To call the skill from Claude Desktop (or any MCP client), run the MCP server:
|
||
|
||
```bash
|
||
haiku-rag serve --mcp --stdio
|
||
```
|
||
|
||
The exposed `ask_question` tool runs this skill. See [MCP](../mcp.md) for the configuration block.
|
||
|
||
## Configuration
|
||
|
||
The skill picks up its model and search behavior from the standard config sections:
|
||
|
||
```yaml
|
||
qa:
|
||
model:
|
||
provider: ollama
|
||
name: gpt-oss
|
||
enable_thinking: true
|
||
temperature: 0.3
|
||
vision: false # set true for vision-capable QA models
|
||
max_searches: 3
|
||
|
||
search:
|
||
limit: 5
|
||
max_context_chars: 10000
|
||
```
|
||
|
||
See [Search and question answering](../configuration/qa.md) for every knob.
|
||
|
||
## Vision support
|
||
|
||
When `qa.model.vision: true` is set, the skill's `search` tool attaches picture bytes to its tool returns as `BinaryContent`. The model can then read figures, diagrams, and screenshots directly alongside the surrounding text. Requires `processing.pictures != none` so the bytes exist on disk. See the [pictures × embedder × QA model matrix](../configuration/processing.md#picture-handling) for the combinations that make sense.
|
||
|
||
## Customizing the skill prompt
|
||
|
||
The skill's instruction prompt lives in `SKILL.md` inside the package. For behavior changes (different phrasing, refusal style, additional rules), the supported path is to fork the skill with `haiku-rag create-skill` and edit the generated `SKILL.md`. The `domain_preamble` field above is for *what the corpus is about*, not for *how the agent should behave*. See [Custom skills](custom.md).
|