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, )