From 771ac9c96c7679db1972663e55274f5cacd3bb40 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Thu, 13 Aug 2026 15:04:05 +0300 Subject: [PATCH] Register the optional capabilities where agents are composed The README feature list and the overview stopped at the analysis capability. Both examples and the app backend composed agents without the capabilities the documentation recommends alongside an evidence capability. custom_agent.py ran each input as an independent agent run, so it needed a state dict and a carried history before compaction could mean anything there: without state the evidence record is empty, and earlier evidence would reduce to receipts retaining nothing. --- README.md | 2 ++ app/backend/main.py | 8 ++++++-- docs/overview.md | 2 +- examples/custom_agent.py | 33 +++++++++++++++++++++++++++------ examples/custom_agent_agui.py | 6 +++++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8680d460..869b3bfd 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Agentic RAG built on [LanceDB](https://lancedb.com/), [Pydantic AI](https://ai.p - **Vision QA** — Vision-capable models receive figure bytes alongside chunk text; attach your own images to questions in `ask`, `analyze`, MCP, and the chat TUI - **Reranking** — local cross-encoders, Cohere, Zero Entropy, or vLLM - **Analysis capability** — Complex analytical tasks via sandboxed Python code execution (aggregation, computation, multi-document analysis) +- **Evidence compaction** — Optional capability that replaces earlier questions' search results on the request with the evidence they cited, so long conversations stop resending everything they retrieved +- **Citation policy** — Optional capability that requires every answer to declare what grounds it, including declaring that nothing does - **Conversational RAG** — Chat TUI and web application for multi-turn conversations with session memory - **Document structure** — Stores full [DoclingDocument](https://docling-project.github.io/docling/concepts/docling_document/), enabling structure-aware context expansion - **Multiple providers** — Embeddings: Ollama, OpenAI, VoyageAI, Cohere, LM Studio, vLLM (multimodal via `multimodal: true` on vLLM/VoyageAI/Cohere). QA: any model supported by Pydantic AI diff --git a/app/backend/main.py b/app/backend/main.py index 252e938c..109fb659 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -21,6 +21,9 @@ from starlette.routing import Route from haiku.rag.capabilities.compaction import ( create_capability as create_compaction, ) +from haiku.rag.capabilities.policy import ( + create_capability as create_citation_policy, +) from haiku.rag.capabilities.rag import AGENT_PREAMBLE, RAGState, create_capability from haiku.rag.client import HaikuRAG from haiku.rag.config import load_yaml_config @@ -85,8 +88,9 @@ agent = Agent( get_model(Config.qa.model, Config), instructions=AGENT_PREAMBLE, # Conversations here are multi-turn, so earlier questions are reduced to the - # evidence they cited rather than carried whole. - capabilities=[capability, create_compaction()], + # evidence they cited rather than carried whole, and every answer declares + # what grounds it so the UI can show citations for all of them. + capabilities=[capability, create_compaction(), create_citation_policy()], deps_type=AppDeps, ) diff --git a/docs/overview.md b/docs/overview.md index 83170fb9..8655a6aa 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -27,7 +27,7 @@ The chat TUI is one way to interact with the database. `haiku-rag ask` and `haik **Search.** Hybrid retrieval (vector + full-text with reciprocal rank fusion), optional cross-encoder reranking, structure-aware context expansion. Image-as-query and cross-modal retrieval when configured with a multimodal embedder. -**Answer.** RAG capability with citations including page numbers, section headings, and visual grounding. Vision-capable models receive figure bytes alongside chunk text. Analysis capability with a sandboxed Python interpreter for aggregation and computation across documents. +**Answer.** RAG capability with citations including page numbers, section headings, and visual grounding. Vision-capable models receive figure bytes alongside chunk text. Analysis capability with a sandboxed Python interpreter for aggregation and computation across documents. Optional capabilities compact a long conversation down to the evidence it cited, and require every answer to declare its grounding. **Integrate.** Use it from Python, the CLI, the [MCP server](mcp.md), or through composable native Pydantic AI [capabilities](capabilities/index.md). diff --git a/examples/custom_agent.py b/examples/custom_agent.py index 99ecadf8..0bf2adb7 100644 --- a/examples/custom_agent.py +++ b/examples/custom_agent.py @@ -1,6 +1,7 @@ """Custom agent using the native haiku.rag RAG capability. -Demonstrates composing a native Pydantic AI capability into an agent. +Demonstrates composing native Pydantic AI capabilities into an agent, and what a +multi-turn conversation needs to carry between runs. Requirements: - An Ollama instance running locally (default embedder) @@ -13,21 +14,40 @@ Usage: import asyncio import sys +from dataclasses import dataclass, field from pathlib import Path +from typing import Any from pydantic_ai import Agent +from pydantic_ai.messages import ModelMessage -from haiku.rag.capabilities.rag import create_capability +from haiku.rag.capabilities.compaction import create_capability as compaction +from haiku.rag.capabilities.policy import create_capability as citation_policy +from haiku.rag.capabilities.rag import create_capability as rag + + +@dataclass +class Deps: + state: dict[str, Any] = field(default_factory=dict) async def main(db_path: str) -> None: - capability = create_capability(db_path=Path(db_path), defer_loading=False) - agent = Agent( "anthropic:claude-haiku-4-5-20251001", - capabilities=[capability], + capabilities=[ + rag(db_path=Path(db_path), defer_loading=False), + compaction(), + citation_policy(), + ], + deps_type=Deps, ) + # One state dict and one history for the whole session. The capabilities read + # both: the state holds what was retrieved and cited, and the message counts + # are how they tell one question from the next. + deps = Deps() + messages: list[ModelMessage] = [] + print("Custom agent ready. Ctrl+C to exit.\n") while True: try: @@ -38,7 +58,8 @@ async def main(db_path: str) -> None: if not user_input: continue - result = await agent.run(user_input) + result = await agent.run(user_input, deps=deps, message_history=messages) + messages = list(result.all_messages()) print(f"\nAgent: {result.output}\n") diff --git a/examples/custom_agent_agui.py b/examples/custom_agent_agui.py index 850c2f7f..78f7b08e 100644 --- a/examples/custom_agent_agui.py +++ b/examples/custom_agent_agui.py @@ -26,6 +26,8 @@ from starlette.requests import Request from starlette.responses import JSONResponse, Response, StreamingResponse from starlette.routing import Route +from haiku.rag.capabilities.compaction import create_capability as compaction +from haiku.rag.capabilities.policy import create_capability as citation_policy from haiku.rag.capabilities.rag import RAGState, create_capability db_path = os.environ.get("DB_PATH") @@ -45,7 +47,9 @@ class AppDeps: agent = Agent( "anthropic:claude-haiku-4-5-20251001", - capabilities=[capability], + # The client returns the state snapshot with every run, so earlier questions are + # reduced to the evidence they cited and every answer declares its grounding. + capabilities=[capability, compaction(), citation_policy()], deps_type=AppDeps, )