diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe12990..45f1e802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,28 +17,28 @@ - `AnalysisResult.program`. The per-execution programs are still tracked on `AnalysisState.executions` (the analysis skill's `execute_code` tool populates it); consumers that need the executed code should pull it from the skill state instead of the function return value. - `--cite` flag on `haiku-rag ask`. Citations always render after the answer now. - `system_prompt` kwarg on `client.ask`. No production caller used it; `config.prompts.domain_preamble` already covers the preamble use case. -- `evaluations optimize` subcommand and the GEPA prompt-optimization module. Drops the `gepa` dependency. Hand-tuning SKILL.md / `prompts.qa` against `evaluations run` is the loop we actually iterate; the auto-mutation surface was unused and conflicted with the project's no-prompt-string-tests rule. -- `--target qa` from `evaluations run`. The skill path supersedes the standalone QA agent — use `--target rag-skill` (now the default) or `--target analysis-skill`. - -### Changed - -- `search.limit` default lowered from `10` to `5`. Reduces text + binary noise in vision-tool returns (picture count tracks result count after expansion + dedup); the cite path still selects from all returned chunks. -- Search result formatter surfaces picture captions on a labelled line when a chunk's expanded refs include pictures. The OpenAI vision API has no identifier field for binary parts, so the caption is the only signal a model can use to map a description to the figure it sees. -- `AnalysisConfig.model` defaults to `None` (was `ollama:gpt-oss/no-thinking/temp=0`). Consumers resolve via `config.analysis.model or config.qa.model`, so the analysis skill inherits the QA driving model when no `analysis` block is in YAML. Set `analysis.model` explicitly to keep the analysis pipeline on a different model from QA. **Note**: the type is now `ModelConfig | None`; external code reading `cfg.analysis.model.X` directly will need to handle the `None` case. -- `client.ask` and `client.analyze` now route through the rag and rag-analysis skills internally (via `haiku.skills.run_skill`). Return shapes preserved: `client.ask` still returns `tuple[str, list[Citation]]`; `client.analyze` returns `AnalysisResult(answer, citations)`. The standalone QA and analysis agents under `agents/qa/` and `agents/analysis/agent.py` remain for now but are scheduled for removal once GEPA optimisation and the `--target qa` eval CLI move to the skill path. - -### Fixed - -- Chat TUI's state-edit screen now syntax-highlights JSON instead of falling back to plain text. Adds `tree-sitter` + `tree-sitter-json` to the `[tui]` extra. +- Standalone QA agent (`haiku.rag.agents.qa.*`) and analysis agent module (`haiku.rag.agents.analysis.agent`, `haiku.rag.agents.analysis.prompts`). Also drops `RawAnalysisResult`, `CodeExecution`, `AnalysisDeps`, and the dead `documents=` preload path in `Sandbox`. +- `prompts.qa` config field. +- `evaluations optimize` subcommand and GEPA prompt-optimization. Drops `gepa` dep. +- `--target qa` from `evaluations run`. Default is now `rag-skill`. ### Changed +- `haiku.rag.agents.analysis` moved to `haiku.rag.sandbox`. Public surface: `from haiku.rag.sandbox import Sandbox, SandboxResult, AnalysisContext, AnalysisResult`. +- `search.limit` default lowered from `10` to `5`. +- Search result formatter surfaces picture captions on a labelled line when a chunk's expanded refs include pictures. +- `AnalysisConfig.model` defaults to `None` (was `ollama:gpt-oss/no-thinking/temp=0`). Resolves via `config.analysis.model or config.qa.model`. +- `client.ask` and `client.analyze` route through the rag and rag-analysis skills internally. - Bump `docling>=2.93.0` and `docling-core>=2.75.0`. - Bump `pydantic-ai-slim>=1.96.0`. Migrate off deprecated APIs: AG-UI imports use `pydantic_ai.ui.ag_ui`, docs/CLI examples use the explicit `openai-chat:` model prefix, and `Agent(retries=)` is split into `tool_retries=` + `output_retries=`. - Bump `pydantic-monty>=0.0.17`. Migrate off deprecated `pydantic_monty.run_repl_async(repl, ...)` to `repl.feed_run_async(...)`. - Cap `transformers<5.0.0` in the `mxbai` extra: `mxbai-rerank>=0.1.6` calls `tokenizer.prepare_for_model` which transformers 5 removed. - Refresh the rest of the lockfile to latest within current constraints (pydantic, pydantic-ai, rich, ruff, ty, pytest, torch, textual, textual-image, watchfiles, pre-commit, datasets, and transitives). +### Fixed + +- Chat TUI's state-edit screen syntax-highlights JSON instead of falling back to plain text. Adds `tree-sitter` + `tree-sitter-json` to the `[tui]` extra. + ## [0.47.0] - 2026-05-14 ### Added diff --git a/README.md b/README.md index bc59f450..ad3b45ce 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Agentic RAG built on [LanceDB](https://lancedb.com/), [Pydantic AI](https://ai.p - **Hybrid search** — Vector + full-text with Reciprocal Rank Fusion - **Multimodal & cross-modal search** — Multimodal embedders (vLLM) put picture vectors in the same space as text; supports text-as-query → figure hits and image-as-query -- **Question answering** — QA agents with citations (page numbers, section headings) +- **Question answering** — RAG skill with citations (page numbers, section headings) - **Vision QA** — Vision-capable models receive figure bytes alongside chunk text - **Reranking** — MxBAI, Cohere, Zero Entropy, or vLLM -- **Research agents** — Multi-agent workflows via pydantic-graph: plan, search, evaluate, synthesize -- **Analysis agent** — Complex analytical tasks via sandboxed Python code execution (aggregation, computation, multi-document analysis) +- **Research workflow** — Multi-agent pydantic-graph: plan, search, evaluate, synthesize +- **Analysis skill** — Complex analytical tasks via sandboxed Python code execution (aggregation, computation, multi-document analysis) - **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, LM Studio, vLLM (multimodal). QA/Research: any model supported by Pydantic AI diff --git a/docs/agents/analysis.md b/docs/agents/analysis.md index 5f04f7f7..bfe6c280 100644 --- a/docs/agents/analysis.md +++ b/docs/agents/analysis.md @@ -1,6 +1,6 @@ -# Analysis Agent +# Analysis -The analysis agent enables complex analytical tasks by writing and executing Python code in a sandboxed environment. It solves problems that traditional RAG struggles with: +The analysis skill enables complex analytical tasks by writing and executing Python code in a sandboxed environment. It solves problems that traditional RAG struggles with: - **Aggregation**: "How many documents mention security vulnerabilities?" - **Computation**: "What's the average revenue across all quarterly reports?" @@ -9,10 +9,10 @@ The analysis agent enables complex analytical tasks by writing and executing Pyt ## How It Works -1. The agent receives a question +1. The skill receives a question 2. It writes Python code to explore the knowledge base -3. Code executes in a sandboxed Python interpreter with access to search, LLM, and a virtual filesystem of documents -4. The agent iterates: run code, examine results, refine approach +3. Code executes in a sandboxed Python interpreter with access to search and a virtual filesystem of documents +4. The skill iterates: run code, examine results, refine approach 5. Final answer is synthesized from the gathered data ## CLI Usage @@ -21,11 +21,8 @@ The analysis agent enables complex analytical tasks by writing and executing Pyt # Basic usage haiku-rag analyze "How many documents are in the database?" -# With document filter (restricts what the agent can access) +# With document filter (restricts what the skill can access) haiku-rag analyze "Summarize the key points" --filter "uri LIKE '%report%'" - -# Pre-load specific documents -haiku-rag analyze "Compare these two reports" --document "Q1 Report" --document "Q2 Report" ``` ## Python Usage @@ -34,24 +31,20 @@ haiku-rag analyze "Compare these two reports" --document "Q1 Report" --document from haiku.rag.client import HaikuRAG async with HaikuRAG(path_to_db) as client: - # Basic question result = await client.analyze("How many documents mention 'security'?") - print(result.answer) # The answer - print(result.program) # The final consolidated program + print(result.answer) + for citation in result.citations: + print(citation.uri, citation.title) - # With filter (agent can only see filtered documents) + # With filter (skill can only see filtered documents) result = await client.analyze( "What is the total revenue?", filter="title LIKE '%Financial%'" ) - - # Pre-load specific documents - result = await client.analyze( - "Compare the conclusions", - documents=["Report A", "Report B"] - ) ``` +The executed Python program(s) for each turn are not on `AnalysisResult` itself; they live on `AnalysisState.executions` while the skill runs. + ## Sandbox Capabilities The agent's code runs in a sandboxed Python interpreter ([pydantic-monty](https://github.com/pydantic/monty)) with: @@ -60,9 +53,8 @@ The agent's code runs in a sandboxed Python interpreter ([pydantic-monty](https: | Function | Description | |----------|-------------| -| `search(query, limit)` | Hybrid search (vector + full-text) with automatic context expansion. Returns `doc_item_refs` for cross-referencing with `items.jsonl` | +| `search(query, limit)` | Hybrid search (vector + full-text) with automatic context expansion. Returns `doc_item_refs` and `picture_refs` for cross-referencing with `items.jsonl` | | `list_documents()` | List all documents in the knowledge base | -| `llm(prompt)` | Call an LLM for classification, summarization, or extraction | ### Document Filesystem @@ -81,13 +73,11 @@ All documents are mounted as a virtual filesystem at `/documents/`. The agent us Search results include `doc_item_refs` (e.g. `["#/texts/5", "#/tables/0"]`) that match `self_ref` values in `items.jsonl`, enabling navigation from search hits to document structure. -When documents are pre-loaded via the `documents` parameter, they are also injected as a `documents` variable accessible in the sandbox code. - ### Python Features The interpreter supports a subset of Python: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `filter()`, `getattr()`, try/except, file I/O via `pathlib.Path`, and the `json`, `re`, `math` modules. -Not supported: most imports (only `json`, `re`, `math`, `pathlib` are available), class definitions, generators/yield, match statements, decorators, `with` statements. For pattern matching, the agent can use `import re`, string methods, or the `llm()` function. +Not supported: most imports (only `json`, `re`, `math`, `pathlib` are available), class definitions, generators/yield, match statements, decorators, `with` statements. For pattern matching, use `import re` or string methods. ### Security @@ -101,10 +91,10 @@ Code executes in an isolated interpreter with: ## Context Filter -The `filter` parameter restricts what documents the agent can access. Unlike tool parameters, the filter is applied automatically and cannot be bypassed by the LLM — both the VFS and search results are scoped to the filter: +The `filter` parameter restricts what documents the skill can access. Unlike tool parameters, the filter is applied automatically and cannot be bypassed by the LLM — both the VFS and search results are scoped to the filter: ```python -# Agent can only see documents with "confidential" in the URI +# Skill can only see documents with "confidential" in the URI result = await client.analyze( "Summarize all findings", filter="uri LIKE '%confidential%'" diff --git a/docs/agents/index.md b/docs/agents/index.md index fb95c234..e12d9c43 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -1,51 +1,30 @@ # Agents -Three agentic flows are provided by haiku.rag: +haiku.rag provides: -- **Simple QA Agent** — a focused question answering agent -- **Research Graph** — a multi-step research workflow with question decomposition -- **Analysis Agent** — complex analytical tasks via sandboxed Python code execution (see [Analysis Agent](analysis.md)) +- **Question Answering** via `client.ask` and the [RAG skill](../skills/index.md) — search + cite over the knowledge base. +- **Analysis** via `client.analyze` and the [analysis skill](../skills/index.md) — sandboxed Python code execution (see [Analysis](analysis.md)). +- **Research Graph** — a multi-step research workflow with question decomposition (this page). -For multi-turn conversational RAG, haiku.rag provides [skills](../skills/index.md) built on [haiku.skills](https://github.com/ggozad/haiku.skills). The skills bundle search, Q&A, analysis, and research tools with session state management. +`client.ask` and `client.analyze` are thin wrappers over the rag and rag-analysis skills built on [haiku.skills](https://github.com/ggozad/haiku.skills). For multi-turn conversational RAG with the same primitives, use the skills directly via `SkillToolset`. 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:** +## Question Answering ```bash haiku-rag ask "What is climate change?" - -# With citations -haiku-rag ask "What is climate change?" --cite ``` -**Python usage:** - ```python from haiku.rag.client import HaikuRAG -from haiku.rag.config.models import ModelConfig -from haiku.rag.agents.qa.agent import QuestionAnswerAgent async with HaikuRAG(path_to_db) as client: - agent = QuestionAnswerAgent( - client=client, - model_config=ModelConfig(provider="openai", name="gpt-4o-mini"), - ) - - answer, citations = await agent.answer("What is climate change?") - print(answer) + answer, citations = await client.ask("What is climate change?") ``` +Citations are always returned in the second element of the tuple and rendered after the answer on the CLI. + ## 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. diff --git a/docs/apps.md b/docs/apps.md index 4da9dcdf..14902883 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -185,7 +185,7 @@ Search uses hybrid (vector + full-text) search across all chunks. ### Context Expansion -Press `c` while viewing a chunk to see the expanded context that would be provided to the QA agent: +Press `c` while viewing a chunk to see the expanded context that would be provided to the rag skill: - Section-aware expansion: expands to fill the current document section - Noise filtering: footnotes, page headers/footers excluded from structured documents diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 4f686a10..a8a8ace9 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -61,9 +61,9 @@ evaluations run repliqa --config /path/to/haiku.rag.yaml --db /path/to/custom.la - `--skip-qa` - Skip QA benchmark - `--limit N` - Limit number of test cases - `--name NAME` - Override the evaluation name -- `--judge-model PROVIDER:NAME` - Override the LLM judge model. Defaults to `ollama:qwen3.6` so the judge stays stable when the QA / skill model changes. -- `--target {qa,rag-skill,analysis-skill}` - Choose what to benchmark (default: `qa`). `rag-skill` and `analysis-skill` run the corresponding [skill](skills/index.md) end-to-end against the same datasets and judge as the QA agent. -- `--skill-model PROVIDER:NAME` - Override the skill model independently from the judge (default: `config.qa.model`). Only valid with skill targets. +- `--judge-model PROVIDER:NAME` - Override the LLM judge model. Defaults to `ollama:qwen3.6` so the judge stays stable when the answering model changes. +- `--target {rag-skill,analysis-skill}` - Choose which [skill](skills/index.md) to benchmark end-to-end against the same datasets and judge (default: `rag-skill`). +- `--skill-model PROVIDER:NAME` - Override the skill model independently from the judge (default: `config.qa.model`, or `config.analysis.model` when set for `--target analysis-skill`). If no config file is specified, the script searches standard locations: `./haiku.rag.yaml`, user config directory, then falls back to defaults. @@ -88,7 +88,7 @@ If no config file is specified, the script searches standard locations: `./haiku ### QA Accuracy -For question-answering evaluation, `pydantic-evals` coordinates an LLM judge to determine whether answers are correct. The default judge is `ollama:qwen3.6` — pinned so changes to the QA or skill model don't change the judge underneath. Override per run with `--judge-model provider:name`. Accuracy is the fraction of correctly answered questions. +For question-answering evaluation, `pydantic-evals` coordinates an LLM judge to determine whether answers are correct. The default judge is `ollama:qwen3.6` — pinned so changes to the skill model don't change the judge underneath. Override per run with `--judge-model provider:name`. Accuracy is the fraction of correctly answered questions. We picked `qwen3.6` over the previously-pinned `gpt-oss` after a 4-cell calibration (gpt-oss / qwen3.6 as both answerer and judge, with Claude Opus 4.7 as a reference). `qwen3.6` had κ ≥ 0.66 vs the reference on both same-family and cross-family answerers (vs ~0.39–0.55 for `gpt-oss`) and showed no measurable self-preference bias, while `gpt-oss` was ~10 pp more lenient on its own outputs. diff --git a/docs/cli.md b/docs/cli.md index 86cfd69a..2c28cfd1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -171,21 +171,15 @@ Ask questions about your documents: haiku-rag ask "Who is the author of haiku.rag?" ``` -Ask questions with citations showing source documents: -```bash -haiku-rag ask "Who is the author of haiku.rag?" --cite -``` - Filter to specific documents: ```bash haiku-rag ask "What are the main findings?" --filter "uri LIKE '%paper%'" ``` -The QA agent searches your documents for relevant information and provides a comprehensive answer. When available, citations use the document title; otherwise they fall back to the URI. +`ask` runs the [rag skill](skills/index.md) and always renders citations under the answer. When available, citations use the document title; otherwise they fall back to the URI. Flags: -- `--cite`: Include citations showing which documents were used - `--filter` / `-f`: Restrict searches to documents matching the filter (see [Filtering Search Results](python.md#filtering-search-results)) ## Chat @@ -271,18 +265,11 @@ Filter to specific documents: haiku-rag analyze "What is the total revenue?" --filter "title LIKE '%Financial%'" ``` -Pre-load specific documents for comparison: - -```bash -haiku-rag analyze "Compare the conclusions" --document "Report A" --document "Report B" -``` - Flags: - `--filter` / `-f`: SQL WHERE clause to restrict document access -- `--document` / `-d`: Pre-load a document by title or ID (can repeat) -See [Analysis Agent](agents/analysis.md) for details on capabilities and configuration. +See [Analysis](agents/analysis.md) for details on capabilities and configuration. ## Create Skill diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 2ce3f5df..418dbdf8 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -106,8 +106,7 @@ search: vector_refine_factor: 30 prompts: - domain_preamble: "" # Prepended to all agent prompts - qa: null # Custom QA agent prompt (null = use default) + domain_preamble: "" # Prepended to skill instructions and research prompts synthesis: null # Custom research synthesis prompt (null = use default) processing: diff --git a/docs/configuration/prompts.md b/docs/configuration/prompts.md index 6cac3824..ad2d7268 100644 --- a/docs/configuration/prompts.md +++ b/docs/configuration/prompts.md @@ -1,20 +1,17 @@ # Prompt Customization -Customize the prompts used by haiku.rag's AI agents to better match your domain and use case. +Customize the prompts used by haiku.rag's skills and research workflow to better match your domain and use case. ## Configuration ```yaml prompts: - # Domain context prepended to all agent prompts + # Domain context prepended to skill instructions and research prompts domain_preamble: | This knowledge base contains technical documentation for the Helios solar panel system, including installation manuals, maintenance procedures, and safety guidelines. Questions about "the system" or unqualified specs refer to the Helios panel. - # Full replacement for QA agent prompt (optional) - qa: null - # Full replacement for research synthesis prompt (optional) synthesis: null @@ -24,13 +21,13 @@ prompts: ## Domain Preamble -The `domain_preamble` field provides **domain context** that is prepended to all agent prompts — the main agent, skill subagents, and internal agents (QA, research planning, search, evaluation, and synthesis). Use this to: +The `domain_preamble` field provides **domain context** prepended to the rag and rag-analysis skill instructions and to the research planner/search/synthesis prompts. Use this to: - Describe what the knowledge base contains - Clarify domain-specific terminology -- Provide context that helps agents interpret ambiguous queries +- Provide context that helps the model interpret ambiguous queries -**Important:** `domain_preamble` is for domain context, not behavioral instructions. Descriptions of subject matter, terminology, and content scope belong here. Behavioral guidance (tone, response style, formatting rules) belongs in the agent's system prompt or custom `prompts.qa`. +**Important:** `domain_preamble` is for domain context, not behavioral instructions. Descriptions of subject matter, terminology, and content scope belong here. Behavioral guidance (tone, response style, formatting rules) lives in the skill's SKILL.md — fork the skill via `haiku-rag create-skill` to customize behavior. **Example:** @@ -42,34 +39,6 @@ prompts: "Deployment" refers to Acme's managed deployment service, not general CI/CD. ``` -## Custom QA Prompt - -Replace the default QA agent prompt entirely by setting `prompts.qa`. The prompt should instruct the agent how to: - -1. Use the `search_documents` tool to find relevant content -2. Interpret search results with scores and metadata -3. Cite sources using chunk IDs -4. Handle insufficient information - -**Example:** - -```yaml -prompts: - qa: | - You are a concise technical assistant. Answer questions using only the knowledge base. - - Process: - 1. Search for relevant documents using the search_documents tool - 2. Review results ordered by relevance (rank 1 = most relevant) - 3. Provide a brief, direct answer based on retrieved content - - Guidelines: - - Use only information from search results - - Include chunk IDs in cited_chunks for sources you use - - If information is insufficient, say so clearly - - Be concise - avoid unnecessary elaboration -``` - ## Custom Synthesis Prompt Replace the research report synthesis prompt by setting `prompts.synthesis`. This controls how the multi-agent research workflow generates its final report. @@ -130,7 +99,6 @@ from haiku.rag.config.models import PromptsConfig config = AppConfig( prompts=PromptsConfig( domain_preamble="This knowledge base contains Acme Corp product documentation and API references.", - qa=None, # Use default QA prompt synthesis=None, # Use default synthesis prompt picture_description="Describe this image for search indexing.", ) diff --git a/docs/configuration/qa-research.md b/docs/configuration/qa-research.md index 49bf4dc9..daf1a82b 100644 --- a/docs/configuration/qa-research.md +++ b/docs/configuration/qa-research.md @@ -20,7 +20,7 @@ Context expansion is automatic and section-aware. For structured documents (with ## Question Answering Configuration -Configure the QA workflow: +Configure the rag skill (used by `client.ask`, `haiku-rag ask`, and the MCP `ask_question` tool): ```yaml qa: @@ -29,13 +29,13 @@ qa: name: gpt-oss enable_thinking: true temperature: 0.3 # Default: 0.3 - vision: false # Set true for vision-capable QA models + vision: false # Set true for vision-capable models max_searches: 3 # Maximum search tool calls per question ``` - **model**: LLM configuration (see [Providers](providers.md#model-settings)) -- **model.vision**: Set to `true` for vision-capable QA models (`qwen2.5vl`, `qwen3.6`, `gpt-4o`, `claude-sonnet`, …). The agent's `search` tool only attaches picture bytes (`BinaryContent`) to its `ToolReturn` when this is `true`; otherwise picture bytes are withheld. See [Pictures × embedder × QA model](processing.md#pictures--embedder--qa-model-how-the-pieces-compose) for the full matrix. -- **max_searches**: Maximum number of search tool calls the QA agent can make per question (default: 3) +- **model.vision**: Set to `true` for vision-capable models (`qwen2.5vl`, `qwen3.6`, `gpt-4o`, `claude-sonnet`, …). The skill's `search` tool only attaches picture bytes (`BinaryContent`) to its `ToolReturn` when this is `true`; otherwise picture bytes are withheld. See [Pictures × embedder × QA model](processing.md#pictures--embedder--qa-model-how-the-pieces-compose) for the full matrix. +- **max_searches**: Maximum number of search tool calls the rag skill can make per question (default: 3) ## Research Configuration @@ -60,7 +60,7 @@ The research workflow uses an iterative feedback loop: the planner proposes one ## Analysis Configuration -Configure the analysis agent: +Configure the analysis skill: ```yaml analysis: @@ -72,8 +72,8 @@ analysis: max_output_chars: 50000 # Truncate output after this many chars ``` -- **model**: LLM configuration (see [Providers](providers.md#model-settings)) +- **model**: LLM configuration (see [Providers](providers.md#model-settings)). When unset, falls back to `qa.model`. - **code_timeout**: Maximum seconds for each code execution (default: 60) - **max_output_chars**: Truncate code output after this many characters (default: 50000) -See [Analysis Agent](../agents/analysis.md) for usage details. +See [Analysis](../agents/analysis.md) for usage details. diff --git a/docs/index.md b/docs/index.md index 9ad17be2..ef53a5ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,11 +8,11 @@ Agentic RAG built on [LanceDB](https://lancedb.com/), [Pydantic AI](https://ai.p - **Hybrid search** — Vector + full-text with Reciprocal Rank Fusion - **Multimodal & cross-modal search** — Multimodal embedders (vLLM) put picture vectors in the same space as text; supports text-as-query → figure hits and image-as-query -- **Question answering** — QA agents with citations (page numbers, section headings) +- **Question answering** — RAG skill with citations (page numbers, section headings) - **Vision QA** — Vision-capable models receive figure bytes alongside chunk text via pydantic-ai `BinaryContent` when `qa.model.vision = true` - **Reranking** — MxBAI, Cohere, Zero Entropy, or vLLM -- **Research agents** — Multi-agent workflows via pydantic-graph: plan, search, evaluate, synthesize -- **Analysis agent** — Complex analytical tasks via sandboxed Python code execution (aggregation, computation, multi-document analysis) +- **Research workflow** — Multi-agent pydantic-graph: plan, search, evaluate, synthesize +- **Analysis skill** — Complex analytical tasks via sandboxed Python code execution (aggregation, computation, multi-document analysis) - **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, LM Studio, vLLM (multimodal). QA/Research: any model supported by Pydantic AI diff --git a/docs/python.md b/docs/python.md index 749f91ec..649e6d24 100644 --- a/docs/python.md +++ b/docs/python.md @@ -421,19 +421,6 @@ for cite in citations: print(f" [{cite.chunk_id}] {cite.document_title or cite.document_uri}") ``` -Customize the QA agent's behavior with a custom system prompt: - -```python -custom_prompt = """You are a technical support expert for WIX. -Answer questions based on the knowledge base documents provided. -Be concise and helpful.""" - -answer, citations = await client.ask( - "How do I create a blog?", - system_prompt=custom_prompt -) -``` - Filter to specific documents: ```python @@ -443,11 +430,11 @@ answer, citations = await client.ask( ) ``` -The QA agent searches your documents for relevant information and uses the configured LLM to generate an answer. The method returns a tuple of `(answer_text, list[Citation])`. Citations include page numbers, section headings, and document references. +`client.ask` runs the [rag skill](skills/index.md) under the hood and returns `(answer_text, list[Citation])`. Citations include page numbers, section headings, and document references. The QA provider and model are configured in `haiku.rag.yaml` or can be passed directly to the client (see [Configuration](configuration/index.md)). -See also: [Agents](agents/index.md) for details on the QA agent and the multi‑agent research workflow. +See also: [Agents](agents/index.md) for details on question answering and the multi‑agent research workflow. ## Analysis @@ -456,25 +443,20 @@ Answer complex analytical questions via code execution: ```python # Aggregation across documents result = await client.analyze("Which quarter had the highest revenue?") -print(result.answer) # The answer -print(result.program) # The final consolidated program +print(result.answer) +for citation in result.citations: + print(citation.uri, citation.title) # Computation within a document set result = await client.analyze( "What is the average deal size mentioned in these contracts?", filter="uri LIKE '%contracts%'" ) - -# Multi-document comparison -result = await client.analyze( - "What changed between these two versions of the policy?", - documents=["Policy v1.0", "Policy v2.0"] -) ``` -The analysis agent writes and executes Python code in a sandboxed environment to solve problems that traditional RAG struggles with: aggregation, computation, and multi-document analysis. +`client.analyze` runs the [analysis skill](skills/index.md), which writes and executes Python code in a sandboxed environment to solve problems that traditional RAG struggles with: aggregation, computation, and multi-document analysis. -See [Analysis Agent](agents/analysis.md) for details on capabilities and configuration. +See [Analysis](agents/analysis.md) for details on capabilities and configuration. ## Building Custom Agents diff --git a/docs/tools.md b/docs/tools.md index f6aca748..38bbef88 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -6,7 +6,7 @@ For lower-level access, `haiku.rag.tools` provides individual `FunctionToolset` ## Low-Level Toolsets -For advanced use cases, individual toolset factories are available in `haiku.rag.tools`. These are used internally by the QA agent and can be composed into custom agents. +For advanced use cases, individual toolset factories are available in `haiku.rag.tools`. These are the same primitives the rag and rag-analysis skills compose, and can be reused to build custom agents. ### RAGDeps Protocol diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/__init__.py b/haiku_rag_slim/haiku/rag/agents/analysis/__init__.py deleted file mode 100644 index f7507d7d..00000000 --- a/haiku_rag_slim/haiku/rag/agents/analysis/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from haiku.rag.agents.analysis.agent import create_analysis_agent -from haiku.rag.agents.analysis.dependencies import AnalysisContext, AnalysisDeps -from haiku.rag.agents.analysis.models import ( - AnalysisResult, - CodeExecution, - RawAnalysisResult, -) -from haiku.rag.agents.analysis.prompts import ANALYSIS_SYSTEM_PROMPT -from haiku.rag.agents.analysis.sandbox import Sandbox, SandboxResult - -__all__ = [ - "ANALYSIS_SYSTEM_PROMPT", - "AnalysisContext", - "AnalysisDeps", - "RawAnalysisResult", - "AnalysisResult", - "CodeExecution", - "Sandbox", - "SandboxResult", - "create_analysis_agent", -] diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/agent.py b/haiku_rag_slim/haiku/rag/agents/analysis/agent.py deleted file mode 100644 index da656299..00000000 --- a/haiku_rag_slim/haiku/rag/agents/analysis/agent.py +++ /dev/null @@ -1,56 +0,0 @@ -from pydantic_ai import Agent, RunContext - -from haiku.rag.agents.analysis.dependencies import AnalysisDeps -from haiku.rag.agents.analysis.models import CodeExecution, RawAnalysisResult -from haiku.rag.agents.analysis.prompts import ANALYSIS_SYSTEM_PROMPT -from haiku.rag.config.models import AppConfig -from haiku.rag.utils import get_model - - -def create_analysis_agent(config: AppConfig) -> Agent[AnalysisDeps, RawAnalysisResult]: - """Create an analysis agent with code execution capability. - - The analysis agent can write and execute Python code in a sandboxed - environment to solve problems that require computation, aggregation, - or complex traversal across documents. - - Args: - config: Application configuration. - - Returns: - A pydantic-ai Agent configured for analysis execution. - """ - model = get_model(config.analysis.model or config.qa.model, config) - - agent: Agent[AnalysisDeps, RawAnalysisResult] = Agent( # type: ignore[assignment] # ty: ignore[invalid-assignment] - model, - deps_type=AnalysisDeps, - output_type=RawAnalysisResult, - instructions=ANALYSIS_SYSTEM_PROMPT, - tool_retries=3, - output_retries=3, - ) - - @agent.tool - async def execute_code(ctx: RunContext[AnalysisDeps], code: str) -> CodeExecution: - """Execute Python code in a sandboxed interpreter. - - The code has access to search() and list_documents() external - functions, plus a virtual filesystem at /documents/ with document - content and structure. Use print() to output results. - - Args: - code: Python code to execute. - - Returns: - Structured result with success status, stdout, and stderr. - """ - result = await ctx.deps.sandbox.execute(code) - return CodeExecution( - code=code, - stdout=result.stdout, - stderr=result.stderr, - success=result.success, - ) - - return agent diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/dependencies.py b/haiku_rag_slim/haiku/rag/agents/analysis/dependencies.py deleted file mode 100644 index c1d5a4e2..00000000 --- a/haiku_rag_slim/haiku/rag/agents/analysis/dependencies.py +++ /dev/null @@ -1,23 +0,0 @@ -from dataclasses import dataclass, field -from typing import TYPE_CHECKING - -from haiku.rag.store.models import Document - -if TYPE_CHECKING: - from haiku.rag.agents.analysis.sandbox import Sandbox - - -@dataclass -class AnalysisContext: - """Mutable context accumulating data during analysis execution.""" - - documents: list[Document] | None = None - filter: str | None = None - - -@dataclass -class AnalysisDeps: - """Dependencies for analysis agent.""" - - sandbox: "Sandbox" - context: AnalysisContext = field(default_factory=AnalysisContext) diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/models.py b/haiku_rag_slim/haiku/rag/agents/analysis/models.py deleted file mode 100644 index 74ad9397..00000000 --- a/haiku_rag_slim/haiku/rag/agents/analysis/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from pydantic import BaseModel, Field - -from haiku.rag.agents.research.models import Citation - - -class CodeExecution(BaseModel): - """Result of executing a code block in the analysis sandbox.""" - - code: str = Field(description="The Python code that was executed") - stdout: str = Field(description="Standard output captured during execution") - stderr: str = Field(description="Standard error captured during execution") - success: bool = Field(description="Whether execution completed without error") - - -class RawAnalysisResult(BaseModel): - """Raw result from the analysis agent (LLM output).""" - - answer: str = Field(description="The answer to the user's question") - program: str = Field(description="The final consolidated program") - - -class AnalysisResult(BaseModel): - """Result from analysis execution with resolved citations. - - The per-execution program(s) are tracked on ``AnalysisState.executions`` - (populated by the analysis skill's ``execute_code`` tool), not on this - return value. Consumers that need the executed code should pull it from - the skill state.""" - - answer: str - citations: list[Citation] = Field(default_factory=list) diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py b/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py deleted file mode 100644 index f962460b..00000000 --- a/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py +++ /dev/null @@ -1,167 +0,0 @@ -ANALYSIS_SYSTEM_PROMPT = """You are an analysis agent that solves complex research questions by writing and executing Python code. - -You MUST use the `execute_code` tool to run Python code. The functions and filesystem described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - -## Available Functions - -Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: -- results = await search("query") ✓ CORRECT -- import search ✗ WRONG - will fail -- results = search("query") ✗ WRONG - must use await - -### await search(query, limit=10) -> list[dict] -Search the knowledge base using hybrid search (vector + full-text). -Results are automatically expanded with surrounding context (adjacent paragraphs, complete tables, section content). -Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings, doc_item_refs, labels, picture_refs. -`picture_refs` is the subset of `doc_item_refs` whose label is `picture` — use it to spot results that contain figures. - -### await list_documents() -> list[dict] -List all documents in the knowledge base. -Returns list of dicts with keys: id, title, uri, created_at - -## Document Filesystem - -All documents in the knowledge base are available as files under `/documents/`. Use `from pathlib import Path` and standard file I/O to access them. - -### Directory structure -``` -/documents/ - {document_id}/ - metadata.json # {"id", "title", "uri", "created_at"} - content.txt # Full document text - items.jsonl # Structured document items (one JSON object per line) - toc.json # Section tree (nested or flat depending on source) -``` - -### metadata.json -Small file with document metadata. Use to discover and identify documents. -```python -from pathlib import Path -import json -for doc_dir in Path('/documents').iterdir(): - meta = json.loads((doc_dir / 'metadata.json').read_text()) - print(meta['title'], meta['uri']) -``` - -### content.txt -Full text content of the document. Use for regex, keyword search, or full-text analysis. -```python -content = Path(f'/documents/{doc_id}/content.txt').read_text() -``` - -### items.jsonl -Structured document items as JSONL. Each line is a JSON object with: -- `position`: sequential position in the document -- `self_ref`: item reference (e.g. "#/texts/5", "#/tables/0") -- `label`: item type — "section_header", "text", "table", "list_item", "caption", "formula", "picture", "code", "footnote", etc. -- `text`: rendered content (tables are markdown with `|` columns) -- `page_numbers`: list of page numbers where the item appears -- `heading_level`: H-level (1–6) on `section_header` items, `0` otherwise. Often `1` for everything when the source is a PDF (docling can't infer heading hierarchy from PDFs) — see `toc.json` for the derived tree. -- `tree_depth`: DOM nesting depth from docling's structure. Useful for HTML where it varies meaningfully (sidebars, captions, nested lists); near-uniform on PDFs. - -Use items.jsonl to find tables, section headers, or specific structural elements: -```python -import json -items_text = Path(f'/documents/{doc_id}/items.jsonl').read_text() -for line in items_text.strip().split(chr(10)): - item = json.loads(line) - if item['label'] == 'table': - print(f"Table on page {item['page_numbers']}: {item['text'][:100]}") -``` - -### toc.json -Per-document section tree derived from `heading_level`. Shape: -```json -{"doc_id": "...", "title": "...", "tree": [ - {"self_ref": "#/texts/0", "level": 1, "title": "Intro", - "position": 0, "page_numbers": [1], "item_range": [0, 18], - "children": [ - {"self_ref": "#/texts/8", "level": 2, "title": "Background", - "position": 8, "page_numbers": [2], "item_range": [8, 13], - "children": []} - ]} -]} -``` -- `item_range = [start, end_exclusive]` over the same `position` ints used in items.jsonl. Slice items.jsonl by this range to read a whole section. -- PDF-derived docs typically produce a flat list of level-1 siblings (docling collapses heading levels). HTML/markdown produce a real nested tree. -- `tree: []` when the doc has no section_headers at all. - -```python -import json -toc = json.loads(Path(f'/documents/{doc_id}/toc.json').read_text()) -items = [json.loads(line) for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split(chr(10))] - -# Read the contents of one section -def items_in(node): - start, end = node['item_range'] - return [it for it in items if start <= it['position'] < end] - -# From a search hit's doc_item_refs, find the deepest TOC node containing it -def find_containing_section(tree, position): - best = None - def walk(nodes): - nonlocal best - for n in nodes: - s, e = n['item_range'] - if s <= position < e: - best = n - walk(n['children']) - walk(tree) - return best -``` - -## Cross-referencing search results with items - -Search results include `doc_item_refs` (e.g. `["#/texts/48", "#/tables/0"]`) that correspond to `self_ref` values in items.jsonl. Use this to navigate from a search hit to the surrounding document structure: -```python -results = await search("revenue", limit=5) -r = results[0] -doc_id = r['document_id'] -refs = set(r['doc_item_refs']) - -import json -items_text = Path(f'/documents/{doc_id}/items.jsonl').read_text() -for line in items_text.strip().split(chr(10)): - item = json.loads(line) - if item['self_ref'] in refs: - print(f"Matched: {item['label']} on page {item['page_numbers']}") -``` - -## Pre-loaded Documents Variable - -If documents were pre-loaded for this session, a `documents` variable is available: -```python -# documents is a list of dicts with keys: id, title, uri, content -for doc in documents: - print(doc['title'], len(doc['content'])) -``` -Check if it exists with: `try: documents ... except NameError: ...` - -## Available Python Features - -The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `filter()`, `getattr()`, `sorted()`/`.sort(key=...)`, try/except, and the `json`, `re`, `math` modules. File I/O via `pathlib.Path` is supported for the `/documents/` filesystem. - -Not supported: most imports (only `json`, `re`, `math`, `pathlib` are available), class definitions, generators/yield, match statements, decorators, `with` statements. - -## Strategy Guide - -1. **Search First**: Start with `search()` to find relevant content. Results include expanded context and `doc_item_refs` for cross-referencing. -2. **Discover Documents**: Use `list_documents()` to see what's in the knowledge base. -3. **Navigate Structure**: Use `items.jsonl` to find tables, section headers, or specific elements by label and page number (tables are pre-rendered as markdown). When a question is scoped to a section, open `toc.json`, find the matching node, and slice `items.jsonl` by its `item_range` instead of streaming `content.txt`. For PDFs where the tree is flat, the sibling list is still useful as a TOC. -4. **Use content.txt for Full Text**: When you need the complete document text (e.g., for regex across the whole document). -5. **Iterate**: Run code, examine results, refine your approach. Don't try to solve everything in one execution. -6. **Cite picture chunks for figure-driven questions**: When a question is about a figure or diagram, find the picture chunk (search results with non-empty `picture_refs`) and cite its chunk_id. The driving model already sees figures from search hits; the citation makes the picture visible in the user's UI as well. - -## Output Format - -Your final response MUST be valid JSON matching this exact schema: -```json -{"answer": "Your answer here", "program": "Your final program here"} -``` - -- `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. -- `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. - -Do NOT return arbitrary JSON structures. Always use the exact format above. - -You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first.""" diff --git a/haiku_rag_slim/haiku/rag/agents/qa/__init__.py b/haiku_rag_slim/haiku/rag/agents/qa/__init__.py deleted file mode 100644 index 6af77f02..00000000 --- a/haiku_rag_slim/haiku/rag/agents/qa/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from haiku.rag.agents.qa.agent import QuestionAnswerAgent -from haiku.rag.agents.qa.prompts import QA_SYSTEM_PROMPT -from haiku.rag.client import HaikuRAG -from haiku.rag.config import AppConfig, Config -from haiku.rag.utils import build_prompt - - -def get_qa_agent( - client: HaikuRAG, - config: AppConfig = Config, - system_prompt: str | None = None, -) -> QuestionAnswerAgent: - """Factory function to get a QA agent based on the configuration. - - Args: - client: HaikuRAG client instance. - config: Configuration to use. Defaults to global Config. - system_prompt: Optional custom system prompt (overrides config). - - Returns: - A configured QuestionAnswerAgent instance. - """ - # Determine the base prompt: explicit > config > default - if system_prompt is None: - system_prompt = config.prompts.qa or QA_SYSTEM_PROMPT - - # Prepend system_context if configured - system_prompt = build_prompt(system_prompt, config) - - return QuestionAnswerAgent( - client=client, - model_config=config.qa.model, - config=config, - system_prompt=system_prompt, - ) diff --git a/haiku_rag_slim/haiku/rag/agents/qa/agent.py b/haiku_rag_slim/haiku/rag/agents/qa/agent.py deleted file mode 100644 index f3c050a7..00000000 --- a/haiku_rag_slim/haiku/rag/agents/qa/agent.py +++ /dev/null @@ -1,81 +0,0 @@ -from dataclasses import dataclass - -from pydantic_ai import Agent - -from haiku.rag.agents.qa.prompts import QA_SYSTEM_PROMPT -from haiku.rag.agents.research.models import ( - Citation, - RawSearchAnswer, - resolve_citations, -) -from haiku.rag.client import HaikuRAG -from haiku.rag.config import Config -from haiku.rag.config.models import AppConfig, ModelConfig -from haiku.rag.store.models import SearchResult -from haiku.rag.tools.search import create_search_toolset -from haiku.rag.utils import get_model - - -@dataclass -class _QARunDeps: - client: HaikuRAG - - -class QuestionAnswerAgent: - def __init__( - self, - client: HaikuRAG, - model_config: ModelConfig, - config: AppConfig | None = None, - system_prompt: str | None = None, - ): - self._client = client - self._config = config or Config - self._model_config = model_config - self._system_prompt = system_prompt or QA_SYSTEM_PROMPT - - async def answer( - self, question: str, filter: str | None = None - ) -> tuple[str, list[Citation]]: - """Answer a question using the RAG system. - - Args: - question: The question to answer - filter: SQL WHERE clause to filter documents - - Returns: - Tuple of (answer text, list of resolved citations) - """ - accumulated_results: list[SearchResult] = [] - max_searches = self._config.qa.max_searches - search_toolset = create_search_toolset( - self._config, - base_filter=filter, - tool_name="search", - on_results=accumulated_results.extend, - max_searches=max_searches, - ) - - # Agent created per-call: toolset varies with filter, and Agent - # construction is pure Python (no IO). - model = get_model(self._model_config, self._config) - try: - system_prompt = self._system_prompt.format(max_searches=max_searches) - except KeyError: - system_prompt = self._system_prompt - agent: Agent[_QARunDeps, RawSearchAnswer] = Agent( # ty: ignore[invalid-assignment] - model=model, - deps_type=_QARunDeps, - output_type=RawSearchAnswer, - instructions=system_prompt, - toolsets=[search_toolset], - tool_retries=3, - output_retries=3, - ) - - deps = _QARunDeps(client=self._client) - result = await agent.run(question, deps=deps) - output = result.output - - citations = resolve_citations(output.cited_chunks, accumulated_results) - return output.answer, citations diff --git a/haiku_rag_slim/haiku/rag/agents/qa/prompts.py b/haiku_rag_slim/haiku/rag/agents/qa/prompts.py deleted file mode 100644 index 5403ccaf..00000000 --- a/haiku_rag_slim/haiku/rag/agents/qa/prompts.py +++ /dev/null @@ -1,41 +0,0 @@ -QA_SYSTEM_PROMPT = """You are a knowledgeable assistant that answers questions using a document knowledge base. - -Process: -1. Call search with relevant keywords from the question -2. Review the results ordered by relevance -3. If needed, perform follow-up searches with different keywords (max {max_searches} total) -4. Provide a concise answer based strictly on the retrieved content - -The search tool returns results like: -[chunk_abc123] [rank 1 of 5] -Source: "Document Title" > Section > Subsection -Type: paragraph -Content: -The actual text content here... - -[chunk_def456] [rank 2 of 5] -Source: "Another Document" -Type: table -Content: -| Column 1 | Column 2 | -... - -Each result includes: -- chunk_id in brackets and rank position (rank 1 = most relevant) -- Source: document title and section hierarchy (when available) -- Type: content type like paragraph, table, code, list_item (when available) -- Content: the actual text - -When a result is of `Type: picture` (a figure or diagram), the search tool may also attach the picture itself as image content alongside the text — use it for visual reasoning when answering. Reference it by its chunk_id like any other source. - -IMPORTANT: You MUST include in cited_chunks the COMPLETE IDs of every chunk you reference. Copy the full ID string without brackets — e.g. "5ae52166-5329-42e9-b6a5-756fc0cb7200" not "[5ae52166]" or "5ae52166". Never truncate IDs. Never leave cited_chunks empty if you found relevant content. - -Guidelines: -- Base answers strictly on retrieved content - do not use external knowledge -- Use the Source and Type metadata to understand context -- If multiple results are relevant, synthesize them coherently -- Be concise and direct - avoid elaboration unless asked -- Results are ordered by relevance, with rank 1 being most relevant -- If the search tool tells you the search limit is reached, stop searching immediately and answer with what you have -- If the retrieved documents do not directly address the question, say: "I cannot find enough information in the knowledge base to answer this question." Do not guess or infer an answer from tangentially related content. -""" diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index fc805576..15ac5432 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -30,11 +30,11 @@ if TYPE_CHECKING: from docling_core.types.doc.document import DoclingDocument from PIL import Image as PILImage - from haiku.rag.agents.analysis.models import AnalysisResult from haiku.rag.agents.research.models import ( Citation, ResearchReport, ) + from haiku.rag.sandbox import AnalysisResult logger = logging.getLogger(__name__) diff --git a/haiku_rag_slim/haiku/rag/client/agents.py b/haiku_rag_slim/haiku/rag/client/agents.py index ff9f9a8a..ba9cf3e2 100644 --- a/haiku_rag_slim/haiku/rag/client/agents.py +++ b/haiku_rag_slim/haiku/rag/client/agents.py @@ -1,9 +1,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from haiku.rag.agents.analysis.models import AnalysisResult from haiku.rag.agents.research.models import Citation, ResearchReport from haiku.rag.client import HaikuRAG + from haiku.rag.sandbox import AnalysisResult async def ask( @@ -89,7 +89,7 @@ async def analyze( Returns: AnalysisResult with the answer and resolved citations. """ - from haiku.rag.agents.analysis.models import AnalysisResult + from haiku.rag.sandbox import AnalysisResult from haiku.rag.skills.analysis import AnalysisState, create_skill from haiku.rag.utils import get_model from haiku.skills import run_skill diff --git a/haiku_rag_slim/haiku/rag/config/models.py b/haiku_rag_slim/haiku/rag/config/models.py index f263e027..a12091fb 100644 --- a/haiku_rag_slim/haiku/rag/config/models.py +++ b/haiku_rag_slim/haiku/rag/config/models.py @@ -241,7 +241,6 @@ class ProvidersConfig(BaseModel): class PromptsConfig(BaseModel): domain_preamble: str = "" - qa: str | None = None synthesis: str | None = None picture_description: str = ( "Describe this image for a blind user. " diff --git a/haiku_rag_slim/haiku/rag/sandbox/__init__.py b/haiku_rag_slim/haiku/rag/sandbox/__init__.py new file mode 100644 index 00000000..1ca4b1de --- /dev/null +++ b/haiku_rag_slim/haiku/rag/sandbox/__init__.py @@ -0,0 +1,10 @@ +from haiku.rag.sandbox.dependencies import AnalysisContext +from haiku.rag.sandbox.models import AnalysisResult +from haiku.rag.sandbox.sandbox import Sandbox, SandboxResult + +__all__ = [ + "AnalysisContext", + "AnalysisResult", + "Sandbox", + "SandboxResult", +] diff --git a/haiku_rag_slim/haiku/rag/sandbox/dependencies.py b/haiku_rag_slim/haiku/rag/sandbox/dependencies.py new file mode 100644 index 00000000..7800dbd2 --- /dev/null +++ b/haiku_rag_slim/haiku/rag/sandbox/dependencies.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class AnalysisContext: + """Mutable context accumulating data during analysis execution.""" + + filter: str | None = None diff --git a/haiku_rag_slim/haiku/rag/sandbox/models.py b/haiku_rag_slim/haiku/rag/sandbox/models.py new file mode 100644 index 00000000..5c988418 --- /dev/null +++ b/haiku_rag_slim/haiku/rag/sandbox/models.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field + +from haiku.rag.agents.research.models import Citation + + +class AnalysisResult(BaseModel): + """Result from analysis execution with resolved citations. + + Executed code is tracked on ``AnalysisState.executions`` (populated by the + analysis skill's ``execute_code`` tool). Consumers that need the program + should pull it from the skill state.""" + + answer: str + citations: list[Citation] = Field(default_factory=list) diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py similarity index 95% rename from haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py rename to haiku_rag_slim/haiku/rag/sandbox/sandbox.py index 060c5dbc..f42753ce 100644 --- a/haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py +++ b/haiku_rag_slim/haiku/rag/sandbox/sandbox.py @@ -10,8 +10,8 @@ from typing import TYPE_CHECKING, Any, Literal import pydantic_monty from pydantic_monty import CallbackFile, MemoryFile, MontyRepl, OSAccess -from haiku.rag.agents.analysis.dependencies import AnalysisContext from haiku.rag.config.models import AppConfig +from haiku.rag.sandbox.dependencies import AnalysisContext from haiku.rag.store.models.chunk import SearchResult from haiku.rag.store.models.document_item import PICTURE_REF_PREFIX, DocumentItem @@ -351,23 +351,6 @@ class Sandbox: "max_duration_secs": self._config.analysis.code_timeout, }, ) - if self._context.documents: - await self._repl.feed_run_async( - "pass", - inputs={ - "documents": [ - { - "id": d.id, - "title": d.title, - "uri": d.uri, - "content": d.content, - } - for d in self._context.documents - ] - }, - external_functions=self._build_external_functions(), - os=self._vfs, - ) assert self._repl is not None and self._vfs is not None return self._repl, self._vfs diff --git a/haiku_rag_slim/haiku/rag/skills/_deps.py b/haiku_rag_slim/haiku/rag/skills/_deps.py index fdad98a8..3fba9463 100644 --- a/haiku_rag_slim/haiku/rag/skills/_deps.py +++ b/haiku_rag_slim/haiku/rag/skills/_deps.py @@ -8,8 +8,8 @@ from haiku.rag.config.models import AppConfig from haiku.skills.state import SkillRunDeps if TYPE_CHECKING: - from haiku.rag.agents.analysis.sandbox import Sandbox from haiku.rag.client import HaikuRAG + from haiku.rag.sandbox import Sandbox @dataclass @@ -60,9 +60,8 @@ def make_rag_lifespan(db_path: Path, config: AppConfig): def make_analysis_lifespan(db_path: Path, config: AppConfig): @asynccontextmanager async def lifespan(deps: AnalysisRunDeps) -> AsyncIterator[None]: - from haiku.rag.agents.analysis.dependencies import AnalysisContext - from haiku.rag.agents.analysis.sandbox import Sandbox from haiku.rag.client import HaikuRAG + from haiku.rag.sandbox import AnalysisContext, Sandbox doc_filter = getattr(deps.state, "document_filter", None) async with HaikuRAG(db_path, config=config, read_only=True) as rag: diff --git a/mkdocs.yml b/mkdocs.yml index 3a282ca8..69dbff75 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,7 +72,7 @@ nav: - Tuning: tuning.md - Agents: - agents/index.md - - Analysis Agent: agents/analysis.md + - Analysis: agents/analysis.md - Skills: - skills/index.md - RAG: skills/rag.md diff --git a/tests/agents/analysis/test_models.py b/tests/agents/analysis/test_models.py deleted file mode 100644 index 192d0f45..00000000 --- a/tests/agents/analysis/test_models.py +++ /dev/null @@ -1,32 +0,0 @@ -from haiku.rag.agents.analysis.models import AnalysisResult, CodeExecution - - -class TestCodeExecution: - def test_create_successful_execution(self): - execution = CodeExecution( - code="print('hello')", - stdout="hello\n", - stderr="", - success=True, - ) - assert execution.code == "print('hello')" - assert execution.stdout == "hello\n" - assert execution.stderr == "" - assert execution.success is True - - def test_create_failed_execution(self): - execution = CodeExecution( - code="1/0", - stdout="", - stderr="ZeroDivisionError: division by zero", - success=False, - ) - assert execution.success is False - assert "ZeroDivisionError" in execution.stderr - - -class TestAnalysisResult: - def test_create_result(self): - result = AnalysisResult(answer="The answer is 42") - assert result.answer == "The answer is 42" - assert result.citations == [] diff --git a/tests/agents/qa/test_qa.py b/tests/agents/qa/test_qa.py deleted file mode 100644 index 19faebb0..00000000 --- a/tests/agents/qa/test_qa.py +++ /dev/null @@ -1,69 +0,0 @@ -from pathlib import Path - -import pytest -from datasets import Dataset -from evaluations.evaluators import LLMJudge - -from haiku.rag.agents.qa.agent import QuestionAnswerAgent -from haiku.rag.client import HaikuRAG -from haiku.rag.config import Config -from haiku.rag.config.models import ModelConfig - - -@pytest.fixture(scope="module") -def vcr_cassette_dir(): - return str(Path(__file__).parent.parent.parent / "cassettes" / "test_qa") - - -@pytest.mark.asyncio -async def test_get_qa_agent_factory(temp_db_path): - """Test get_qa_agent factory function creates a properly configured agent.""" - from haiku.rag.agents.qa import get_qa_agent - - async with HaikuRAG(temp_db_path, create=True) as client: - agent = get_qa_agent(client, Config) - - assert agent is not None - assert isinstance(agent, QuestionAnswerAgent) - # Verify internal client is set correctly - assert agent._client is client - - -@pytest.mark.asyncio -async def test_get_qa_agent_with_custom_prompt(temp_db_path): - """Test get_qa_agent factory with custom system prompt.""" - from haiku.rag.agents.qa import get_qa_agent - - async with HaikuRAG(temp_db_path, create=True) as client: - custom_prompt = "You are a custom QA assistant." - agent = get_qa_agent(client, Config, system_prompt=custom_prompt) - - assert agent is not None - assert isinstance(agent, QuestionAnswerAgent) - assert agent._system_prompt == custom_prompt - - -@pytest.mark.vcr() -async def test_qa_ollama(allow_model_requests, qa_corpus: Dataset, temp_db_path): - """Test Ollama QA with LLM judge (VCR recorded).""" - async with HaikuRAG(temp_db_path, create=True) as client: - qa = QuestionAnswerAgent( - client, - ModelConfig(provider="ollama", name="gpt-oss", enable_thinking=True), - ) - llm_judge = LLMJudge() - - doc = qa_corpus[1] - await client.create_document( - content=doc["document_extracted"], uri=doc["document_id"] - ) - - question = doc["question"] - expected_answer = doc["answer"] - - answer, _ = await qa.answer(question) - is_equivalent = await llm_judge.judge_answers(question, answer, expected_answer) - - assert is_equivalent, ( - f"Generated answer not equivalent to expected answer.\nQuestion: {question}\nGenerated: {answer}\nExpected: {expected_answer}" - ) diff --git a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_aggregation.yaml b/tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_aggregation.yaml similarity index 100% rename from tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_aggregation.yaml rename to tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_aggregation.yaml diff --git a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_count_documents.yaml b/tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_count_documents.yaml similarity index 100% rename from tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_count_documents.yaml rename to tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_count_documents.yaml diff --git a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_search_and_extract.yaml b/tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_search_and_extract.yaml similarity index 100% rename from tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_search_and_extract.yaml rename to tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_search_and_extract.yaml diff --git a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_search_and_identify_source.yaml b/tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_search_and_identify_source.yaml similarity index 100% rename from tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_search_and_identify_source.yaml rename to tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_search_and_identify_source.yaml diff --git a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_with_filter.yaml b/tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_with_filter.yaml similarity index 100% rename from tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_with_filter.yaml rename to tests/cassettes/test_client_analyze/TestClientAnalysisIntegration.test_analyze_with_filter.yaml diff --git a/tests/cassettes/test_qa/test_qa_ollama.yaml b/tests/cassettes/test_qa/test_qa_ollama.yaml deleted file mode 100644 index e0af991c..00000000 --- a/tests/cassettes/test_qa/test_qa_ollama.yaml +++ /dev/null @@ -1,585 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '4851' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - |- - Jakarta Election Campaigns Heat Up: Here's How to Understand the System - As election day in Jakarta draws nearer, candidates are out in force using various strategies to woo voters and prepare to exercise their democratic rights. Citizens make their voices heard as the city vibrates with life. This coverage offers valuable insight into campaign activities while offering an in-depth guide for understanding electoral processes in Jakarta. - Initial Launch of Candidates' Campaign Plans on September 1 - After September 1st, when campaign season officially kicked off, candidates have moved swiftly to engage their bases. Amira Bintang, an upstart candidate with extensive social activism experience and promising urban development and public transportation reform as key platforms of her candidacy speech in Jakarta; incumbent Rizal Harahap relies heavily on his track record and highlights all infrastructure projects completed during his term. - Campaign Strategies: From Digital Battlegrounds to Door-toDoor Outreach - Campaign strategies have taken an advanced turn as candidates leverage digital media to reach a wider audience. Hashtags, viral videos, targeted ads and targeted messaging can all play an influential role in changing public opinion with just a tweet or meme. At the grassroots level candidates engage in door-to-door campaigns personalized for individual voters in an attempt to connect. - - |- - Bintang's interactive app for gathering real-time feedback from citizens about daily commute challenges was applauded as an innovative form of civic engagement, while Harahap launched a series of webinars featuring experts discussing economic growth under his administration. - Rallies and Persuasion - Jakartan politicians know the power of an impassioned speech cannot be underrated, and candidates have been taking full advantage of its effectiveness at rallies. Rallies feature vibrant colors, banners and impassioned discourse in an attempt to win converts over. At one high-spirited rally on October 22, Bintang outlined her policy plans for improving education and healthcare to an appreciative crowd while Harahap's rallies often consist of shows of solidarity from various political allies united behind his plea for continuity and stability. - Debates: Clashes Between Visions and Policies - - |- - Debates are one of the highlights of Jakarta election campaigns, allowing candidates to outline their platforms and discuss critical issues. On November 5th, citizens witnessed an exhilarating debate between candidates Bintang and Harahap over whether the city was prepared for digital transformation in public services; Bintang advocated an aggressive move toward smart city model while Harahap advocated a more measured approach so as not to alienate less tech-savvy residents. - Voter Engagement: Making Every Vote Count - Ensuring every eligible voter is engaged and informed remains an ongoing challenge for Jakartans. Civil society groups and independent bodies host workshops and publish voter guides to inform voters of their rights and choices, while an annual democracy festival such as that held on November 20 featured interactive exhibits on Jakarta's electoral history as well as mock voting booths for first-time voters. - Campaign Financing: Transparency and Accountability - - |- - Campaign financing has always been a contentious topic in elections, and this election cycle is no exception. Bintang's campaign, funded largely through crowdfunders online supporters, stands in stark contrast with Harahap's sophisticated machine backed by both private donors and party funds. To protect democratic decision making processes from any - undue influences on decision-making processes, Jakarta Election Commission mandated strict reporting and transparency measures during campaign financing decisions. - Before Election Day: Submit Final Appeals Now - As election day nears, candidates make their last appeals to voters. Bintang plans a visit through key neighborhoods while Harahap plans a final rally scheduled for late November. Both camps are honing their messaging and policy proposals while encouraging supporters to make an appearance at polling booths on November 8th. - Polling Day: The Final Act of Campaign Activities - On December 6th, voting booths across Jakarta will open their doors, signalling the culmination of weeks of intense campaigning. Voters will cast their vote and candidates await results that depend on how effective their strategies, speeches and outreach initiatives have been. - - Jakarta's vibrant election campaign offers an insight into its flourishing democracy. As the city looks ahead to an - exciting new chapter in its political history, electoral processes demonstrate the significance of people-power in - shaping our collective futures. - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - - embedding: 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 - index: 1 - object: embedding - - embedding: 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 - index: 2 - object: embedding - - embedding: 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 - index: 3 - object: embedding - - embedding: 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 - index: 4 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 858 - total_tokens: 858 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '2953' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: |- - You are a knowledgeable assistant that answers questions using a document knowledge base. - - Process: - 1. Call search_documents with relevant keywords from the question - 2. Review the results ordered by relevance - 3. If needed, perform follow-up searches with different keywords (max 3 total) - 4. Provide a concise answer based strictly on the retrieved content - - The search tool returns results like: - [chunk_abc123] [rank 1 of 5] - Source: "Document Title" > Section > Subsection - Type: paragraph - Content: - The actual text content here... - - [chunk_def456] [rank 2 of 5] - Source: "Another Document" - Type: table - Content: - | Column 1 | Column 2 | - ... - - Each result includes: - - chunk_id in brackets and rank position (rank 1 = most relevant) - - Source: document title and section hierarchy (when available) - - Type: content type like paragraph, table, code, list_item (when available) - - Content: the actual text - - In your response, include the chunk IDs you used in cited_chunks. - - Guidelines: - - Base answers strictly on retrieved content - do not use external knowledge - - Use the Source and Type metadata to understand context - - If multiple results are relevant, synthesize them coherently - - If information is insufficient, say: "I cannot find enough information in the knowledge base to answer this question." - - Be concise and direct - avoid elaboration unless asked - - Results are ordered by relevance, with rank 1 being most relevant - role: system - - content: What innovative form of civic engagement did Bintang introduce for gathering feedback from citizens during - Jakarta's election? - role: user - model: gpt-oss - reasoning_effort: high - response_format: - json_schema: - description: Answer to a search query with chunk references. - name: RawSearchAnswer - schema: - additionalProperties: false - properties: - answer: - description: The answer to the question - type: string - cited_chunks: - description: IDs of chunks used to form the answer - items: - type: string - type: array - confidence: - default: 1.0 - description: Confidence score for this answer (0-1) - maximum: 1.0 - minimum: 0.0 - type: number - query: - description: The question that was answered - type: string - required: - - query - - answer - type: object - strict: false - type: json_schema - stream: false - tool_choice: auto - tools: - - function: - description: |- - Search the knowledge base for relevant documents. - - Formatted search results with content and metadata. - - name: search_documents - parameters: - additionalProperties: false - properties: - limit: - anyOf: - - type: integer - - type: 'null' - default: null - description: 'Number of results to return (default: from config).' - query: - description: The search query (what to search for). - type: string - required: - - query - type: object - type: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '865' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: 'We need to answer: "What innovative form of civic engagement did Bintang introduce for gathering feedback - from citizens during Jakarta''s election?" This asks about a specific innovative form of civic engagement introduced - by Bintang during Jakarta''s election. We need to look into the knowledge base. Let''s search.' - role: assistant - tool_calls: - - function: - arguments: '{"query":"Bintang innovative form of civic engagement gathering feedback citizens during Jakarta - election","limit":10}' - name: search_documents - id: call_zzsyoh9m - index: 0 - type: function - created: 1772626873 - id: chatcmpl-900 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 97 - prompt_tokens: 510 - total_tokens: 607 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '165' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - Bintang innovative form of civic engagement gathering feedback citizens during Jakarta election - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 15 - total_tokens: 15 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '8507' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: |- - You are a knowledgeable assistant that answers questions using a document knowledge base. - - Process: - 1. Call search_documents with relevant keywords from the question - 2. Review the results ordered by relevance - 3. If needed, perform follow-up searches with different keywords (max 3 total) - 4. Provide a concise answer based strictly on the retrieved content - - The search tool returns results like: - [chunk_abc123] [rank 1 of 5] - Source: "Document Title" > Section > Subsection - Type: paragraph - Content: - The actual text content here... - - [chunk_def456] [rank 2 of 5] - Source: "Another Document" - Type: table - Content: - | Column 1 | Column 2 | - ... - - Each result includes: - - chunk_id in brackets and rank position (rank 1 = most relevant) - - Source: document title and section hierarchy (when available) - - Type: content type like paragraph, table, code, list_item (when available) - - Content: the actual text - - In your response, include the chunk IDs you used in cited_chunks. - - Guidelines: - - Base answers strictly on retrieved content - do not use external knowledge - - Use the Source and Type metadata to understand context - - If multiple results are relevant, synthesize them coherently - - If information is insufficient, say: "I cannot find enough information in the knowledge base to answer this question." - - Be concise and direct - avoid elaboration unless asked - - Results are ordered by relevance, with rank 1 being most relevant - role: system - - content: What innovative form of civic engagement did Bintang introduce for gathering feedback from citizens during - Jakarta's election? - role: user - - content: null - reasoning: 'We need to answer: "What innovative form of civic engagement did Bintang introduce for gathering feedback - from citizens during Jakarta''s election?" This asks about a specific innovative form of civic engagement introduced - by Bintang during Jakarta''s election. We need to look into the knowledge base. Let''s search.' - role: assistant - tool_calls: - - function: - arguments: '{"query":"Bintang innovative form of civic engagement gathering feedback citizens during Jakarta election","limit":10}' - name: search_documents - id: call_zzsyoh9m - type: function - - content: |- - [2a4d127c-da6d-40dc-b02f-4c012653b40f] [rank 1 of 1] - Type: text - Content: - Jakarta Election Campaigns Heat Up: Here's How to Understand the System - - As election day in Jakarta draws nearer, candidates are out in force using various strategies to woo voters and prepare to exercise their democratic rights. Citizens make their voices heard as the city vibrates with life. This coverage offers valuable insight into campaign activities while offering an in-depth guide for understanding electoral processes in Jakarta. - - Initial Launch of Candidates' Campaign Plans on September 1 - - After September 1st, when campaign season officially kicked off, candidates have moved swiftly to engage their bases. Amira Bintang, an upstart candidate with extensive social activism experience and promising urban development and public transportation reform as key platforms of her candidacy speech in Jakarta; incumbent Rizal Harahap relies heavily on his track record and highlights all infrastructure projects completed during his term. - - Campaign Strategies: From Digital Battlegrounds to Door-toDoor Outreach - - Campaign strategies have taken an advanced turn as candidates leverage digital media to reach a wider audience. Hashtags, viral videos, targeted ads and targeted messaging can all play an influential role in changing public opinion with just a tweet or meme. At the grassroots level candidates engage in door-to-door campaigns personalized for individual voters in an attempt to connect. - - Bintang's interactive app for gathering real-time feedback from citizens about daily commute challenges was applauded as an innovative form of civic engagement, while Harahap launched a series of webinars featuring experts discussing economic growth under his administration. - - Rallies and Persuasion - - Jakartan politicians know the power of an impassioned speech cannot be underrated, and candidates have been taking full advantage of its effectiveness at rallies. Rallies feature vibrant colors, banners and impassioned discourse in an attempt to win converts over. At one high-spirited rally on October 22, Bintang outlined her policy plans for improving education and healthcare to an appreciative crowd while Harahap's rallies often consist of shows of solidarity from various political allies united behind his plea for continuity and stability. - - Debates: Clashes Between Visions and Policies - - Debates are one of the highlights of Jakarta election campaigns, allowing candidates to outline their platforms and discuss critical issues. On November 5th, citizens witnessed an exhilarating debate between candidates Bintang and Harahap over whether the city was prepared for digital transformation in public services; Bintang advocated an aggressive move toward smart city model while Harahap advocated a more measured approach so as not to alienate less tech-savvy residents. - - Voter Engagement: Making Every Vote Count - - Ensuring every eligible voter is engaged and informed remains an ongoing challenge for Jakartans. Civil society groups and independent bodies host workshops and publish voter guides to inform voters of their rights and choices, while an annual democracy festival such as that held on November 20 featured interactive exhibits on Jakarta's electoral history as well as mock voting booths for first-time voters. - - Campaign Financing: Transparency and Accountability - - Campaign financing has always been a contentious topic in elections, and this election cycle is no exception. Bintang's campaign, funded largely through crowdfunders online supporters, stands in stark contrast with Harahap's sophisticated machine backed by both private donors and party funds. To protect democratic decision making processes from any - - undue influences on decision-making processes, Jakarta Election Commission mandated strict reporting and transparency measures during campaign financing decisions. - - Before Election Day: Submit Final Appeals Now - - As election day nears, candidates make their last appeals to voters. Bintang plans a visit through key neighborhoods while Harahap plans a final rally scheduled for late November. Both camps are honing their messaging and policy proposals while encouraging supporters to make an appearance at polling booths on November 8th. - - Polling Day: The Final Act of Campaign Activities - - On December 6th, voting booths across Jakarta will open their doors, signalling the culmination of weeks of intense campaigning. Voters will cast their vote and candidates await results that depend on how effective their strategies, speeches and outreach initiatives have been. - - Jakarta's vibrant election campaign offers an insight into its flourishing democracy. As the city looks ahead to an exciting new chapter in its political history, electoral processes demonstrate the significance of people-power in shaping our collective futures. - role: tool - tool_call_id: call_zzsyoh9m - model: gpt-oss - reasoning_effort: high - response_format: - json_schema: - description: Answer to a search query with chunk references. - name: RawSearchAnswer - schema: - additionalProperties: false - properties: - answer: - description: The answer to the question - type: string - cited_chunks: - description: IDs of chunks used to form the answer - items: - type: string - type: array - confidence: - default: 1.0 - description: Confidence score for this answer (0-1) - maximum: 1.0 - minimum: 0.0 - type: number - query: - description: The question that was answered - type: string - required: - - query - - answer - type: object - strict: false - type: json_schema - stream: false - tool_choice: auto - tools: - - function: - description: |- - Search the knowledge base for relevant documents. - - Formatted search results with content and metadata. - - name: search_documents - parameters: - additionalProperties: false - properties: - limit: - anyOf: - - type: integer - - type: 'null' - default: null - description: 'Number of results to return (default: from config).' - query: - description: The search query (what to search for). - type: string - required: - - query - type: object - type: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '1785' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: '{"query":"Jakarta election Bintang interactive app","answer":"Bintang introduced an interactive app that - let citizens give real‑time feedback on their daily commute challenges, which was praised as an innovative civic‑engagement - tool.","cited_chunks":["2a4d127c-da6d-40dc-b02f-4c012653b40f"]}' - reasoning: |- - Search result shows content that indicates Bintang's interactive app for gathering real-time feedback from citizens about daily commute challenges as an innovative form of civic engagement. The question: "What innovative form of civic engagement did Bintang introduce for gathering feedback from citizens during Jakarta's election?" The answer: Bintang introduced an interactive app for gathering real-time feedback from citizens about daily commute challenges. According to the snippet: "Bintang's interactive app for gathering real-time feedback from citizens about daily commute challenges was applauded as an innovative form of civic engagement". So that is the answer. Provide with citation. - - But we must cite chunk IDs. The chunk ID is [2a4d127c-da6d-40dc-b02f-4c012653b40f]. That is the relevant chunk. I can only include that chunk ID. Let's use that. The instruction: "In your response, include the chunk IDs you used in cited_chunks." - - Hence answer: "Bintang introduced an interactive app for gathering real-time feedback on daily commute challenges". - - Add citation. Also maybe mention "interactive app" or "digital platform". I'll keep it concise. - role: assistant - created: 1772626882 - id: chatcmpl-570 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 76 - prompt_tokens: 1735 - total_tokens: 1811 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '1856' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: | - You are evaluating whether two answers to the same question are semantically equivalent. - - EVALUATION CRITERIA: - Rate as EQUIVALENT if: - ✓ Both answers contain the same core factual information - ✓ Both directly address the question asked - ✓ The key claims and conclusions are consistent - ✓ Any additional detail in one answer doesn't contradict the other - - Rate as NOT EQUIVALENT if: - ✗ Factual contradictions exist between the answers - ✗ One answer fails to address the core question - ✗ Key information is missing that changes the meaning - ✗ The answers lead to different conclusions or implications - - GUIDELINES: - - Ignore minor differences in phrasing, style, or formatting - - Focus on semantic meaning rather than exact wording - - Consider both answers correct if they convey the same essential information - - Be tolerant of different levels of detail if the core answer is preserved - - Evaluate based on what a person asking this question would need to know - role: system - - content: |- - QUESTION: What innovative form of civic engagement did Bintang introduce for gathering feedback from citizens during Jakarta's election? - - GENERATED ANSWER: Bintang introduced an interactive app that let citizens give real‑time feedback on their daily commute challenges, which was praised as an innovative civic‑engagement tool. - - EXPECTED ANSWER: Bintang introduced an interactive app for real-time feedback on daily commute challenges. - role: user - model: gpt-oss - reasoning_effort: low - stream: false - tool_choice: auto - tools: - - function: - description: The final response which ends this conversation - name: final_result - parameters: - additionalProperties: false - properties: - equivalent: - type: boolean - required: - - equivalent - type: object - strict: true - type: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '556' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: We compare. Both mention interactive app, real-time feedback, daily commute challenges. They match. So - equivalent. - role: assistant - tool_calls: - - function: - arguments: '{"equivalent":true}' - name: final_result - id: call_3tgu31go - index: 0 - type: function - created: 1772626883 - id: chatcmpl-801 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 47 - prompt_tokens: 395 - total_tokens: 442 - status: - code: 200 - message: OK -version: 1 diff --git a/tests/agents/analysis/__init__.py b/tests/sandbox/__init__.py similarity index 100% rename from tests/agents/analysis/__init__.py rename to tests/sandbox/__init__.py diff --git a/tests/agents/analysis/conftest.py b/tests/sandbox/conftest.py similarity index 54% rename from tests/agents/analysis/conftest.py rename to tests/sandbox/conftest.py index ca2f3712..00d214fa 100644 --- a/tests/agents/analysis/conftest.py +++ b/tests/sandbox/conftest.py @@ -1,16 +1,8 @@ import pytest -from haiku.rag.agents.analysis.dependencies import AnalysisContext -from haiku.rag.agents.analysis.sandbox import Sandbox from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig - - -@pytest.fixture -async def empty_client(temp_db_path): - """Create an empty HaikuRAG client without documents.""" - async with HaikuRAG(temp_db_path, create=True) as client: - yield client +from haiku.rag.sandbox import AnalysisContext, Sandbox @pytest.fixture diff --git a/tests/sandbox/test_models.py b/tests/sandbox/test_models.py new file mode 100644 index 00000000..e9eb1892 --- /dev/null +++ b/tests/sandbox/test_models.py @@ -0,0 +1,8 @@ +from haiku.rag.sandbox import AnalysisResult + + +class TestAnalysisResult: + def test_create_result(self): + result = AnalysisResult(answer="The answer is 42") + assert result.answer == "The answer is 42" + assert result.citations == [] diff --git a/tests/agents/analysis/test_sandbox.py b/tests/sandbox/test_sandbox.py similarity index 90% rename from tests/agents/analysis/test_sandbox.py rename to tests/sandbox/test_sandbox.py index 3809f880..2e8cba91 100644 --- a/tests/agents/analysis/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -2,16 +2,14 @@ from pathlib import Path import pytest -from haiku.rag.agents.analysis.dependencies import AnalysisContext -from haiku.rag.agents.analysis.sandbox import Sandbox, SandboxResult from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig -from haiku.rag.store.models import Document +from haiku.rag.sandbox import AnalysisContext, Sandbox, SandboxResult @pytest.fixture(scope="module") def vcr_cassette_dir(): - return str(Path(__file__).parent.parent.parent / "cassettes" / "test_sandbox") + return str(Path(__file__).parent.parent / "cassettes" / "test_sandbox") class TestSandboxBasics: @@ -414,35 +412,3 @@ class TestSandboxVFS: assert "1" in result.stdout assert "Public Doc" in result.stdout assert "Private Doc" not in result.stdout - - -class TestSandboxPreloadedDocuments: - """Test pre-loaded documents context variable.""" - - @pytest.mark.asyncio - async def test_documents_variable_not_available_without_preload(self, sandbox): - """documents variable is not available when context.documents is None.""" - result = await sandbox.execute("print(documents)") - assert not result.success - assert "NameError" in result.stderr - - @pytest.mark.asyncio - async def test_documents_variable_available_with_preload(self, temp_db_path): - """documents variable is available when context.documents is set.""" - async with HaikuRAG(temp_db_path, create=True): - config = AppConfig() - docs = [ - Document(id="1", content="Content A", title="Doc A", uri="a://1"), - Document(id="2", content="Content B", title="Doc B", uri="b://2"), - ] - context = AnalysisContext(documents=docs) - sb = Sandbox(db_path=temp_db_path, config=config, context=context) - result = await sb.execute( - "print(len(documents))\n" - "print(documents[0]['title'])\n" - "print(documents[1]['title'])" - ) - assert result.success - assert "2" in result.stdout - assert "Doc A" in result.stdout - assert "Doc B" in result.stdout diff --git a/tests/agents/analysis/test_sandbox_multimodal.py b/tests/sandbox/test_sandbox_multimodal.py similarity index 96% rename from tests/agents/analysis/test_sandbox_multimodal.py rename to tests/sandbox/test_sandbox_multimodal.py index 145f6c08..bf20324b 100644 --- a/tests/agents/analysis/test_sandbox_multimodal.py +++ b/tests/sandbox/test_sandbox_multimodal.py @@ -4,10 +4,9 @@ and the set of external functions exposed to the interpreter. import pytest -from haiku.rag.agents.analysis.dependencies import AnalysisContext -from haiku.rag.agents.analysis.sandbox import Sandbox from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig +from haiku.rag.sandbox import AnalysisContext, Sandbox from haiku.rag.store.models.chunk import SearchResult diff --git a/tests/agents/analysis/test_sandbox_toc.py b/tests/sandbox/test_sandbox_toc.py similarity index 98% rename from tests/agents/analysis/test_sandbox_toc.py rename to tests/sandbox/test_sandbox_toc.py index ea7b35c9..c68d17e5 100644 --- a/tests/agents/analysis/test_sandbox_toc.py +++ b/tests/sandbox/test_sandbox_toc.py @@ -12,10 +12,9 @@ from pathlib import PurePosixPath import pytest -from haiku.rag.agents.analysis.dependencies import AnalysisContext -from haiku.rag.agents.analysis.sandbox import Sandbox from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig +from haiku.rag.sandbox import AnalysisContext, Sandbox from haiku.rag.store.models.document import Document from haiku.rag.store.models.document_item import DocumentItem diff --git a/tests/skills/conftest.py b/tests/skills/conftest.py index 25a419b3..94776924 100644 --- a/tests/skills/conftest.py +++ b/tests/skills/conftest.py @@ -85,8 +85,7 @@ async def rag_client(rag_db): @pytest.fixture def sandbox_factory(rag_db, test_app_config): """Build Sandbox instances bound to the sample db, optionally with a doc filter.""" - from haiku.rag.agents.analysis.dependencies import AnalysisContext - from haiku.rag.agents.analysis.sandbox import Sandbox + from haiku.rag.sandbox import AnalysisContext, Sandbox def _make(filter: str | None = None) -> Sandbox: return Sandbox( diff --git a/tests/skills/test_analysis.py b/tests/skills/test_analysis.py index 024df494..108ab11e 100644 --- a/tests/skills/test_analysis.py +++ b/tests/skills/test_analysis.py @@ -285,7 +285,7 @@ class TestExecuteCodeTool: class TestAnalysisLifespan: async def test_opens_client_and_sandbox_per_invocation(self, rag_db): - from haiku.rag.agents.analysis.sandbox import Sandbox + from haiku.rag.sandbox import Sandbox from haiku.rag.skills._deps import AnalysisRunDeps, make_analysis_lifespan config = AppConfig() diff --git a/tests/agents/analysis/test_agent.py b/tests/test_client_analyze.py similarity index 54% rename from tests/agents/analysis/test_agent.py rename to tests/test_client_analyze.py index f0869541..3c4c3521 100644 --- a/tests/agents/analysis/test_agent.py +++ b/tests/test_client_analyze.py @@ -1,61 +1,22 @@ from pathlib import Path import pytest -from pydantic_ai import Agent -from haiku.rag.agents.analysis.agent import create_analysis_agent -from haiku.rag.agents.analysis.dependencies import AnalysisDeps -from haiku.rag.agents.analysis.models import CodeExecution, RawAnalysisResult -from haiku.rag.config import AppConfig, Config +from haiku.rag.client import HaikuRAG +from haiku.rag.config import AppConfig @pytest.fixture(scope="module") def vcr_cassette_dir(): - return str(Path(__file__).parent.parent.parent / "cassettes" / "test_analysis") - - -class TestCreateAnalysisAgent: - def test_creates_agent(self): - agent = create_analysis_agent(Config) - assert isinstance(agent, Agent) - assert agent.deps_type is AnalysisDeps - assert agent.output_type is RawAnalysisResult - - def test_agent_has_execute_code_tool(self): - agent = create_analysis_agent(Config) - tool_names = list(agent._function_toolset.tools.keys()) - assert "execute_code" in tool_names - - -class TestCodeExecutionModel: - def test_code_execution_has_correct_fields(self): - """Test that CodeExecution has all expected fields.""" - execution = CodeExecution( - code="print('hello')", - stdout="hello\n", - stderr="", - success=True, - ) - assert execution.code == "print('hello')" - assert execution.stdout == "hello\n" - assert execution.stderr == "" - assert execution.success is True + return str(Path(__file__).parent / "cassettes" / "test_client_analyze") class TestClientAnalysisIntegration: - """Integration tests for client.analyze() method.""" + """Integration tests for client.analyze() through the rag-analysis skill.""" @pytest.mark.asyncio @pytest.mark.vcr() async def test_analyze_count_documents(self, allow_model_requests, temp_db_path): - """Test analysis agent can count documents. - - Agent program: - docs = list_documents(limit=1000) - print(len(docs)) - """ - from haiku.rag.client import HaikuRAG - config = AppConfig() async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -70,26 +31,6 @@ class TestClientAnalysisIntegration: @pytest.mark.asyncio @pytest.mark.vcr() async def test_analyze_aggregation(self, allow_model_requests, temp_db_path): - """Test analysis agent can perform aggregation across documents. - - Agent program: - import re - revs = {} - for d in ['Q1 Report', 'Q2 Report', 'Q3 Report']: - content = get_document(d) - if content: - vals = re.findall(r'\\$([\\d,]+)', content) - if vals: - rev = sum(int(v.replace(',', '')) for v in vals) - else: - rev = None - else: - rev = None - revs[d] = rev - print(revs) - """ - from haiku.rag.client import HaikuRAG - config = AppConfig() async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -112,17 +53,6 @@ class TestClientAnalysisIntegration: @pytest.mark.asyncio @pytest.mark.vcr() async def test_analyze_with_filter(self, allow_model_requests, temp_db_path): - """Test analysis agent respects filter parameter. - - Agent program: - docs = list_documents(limit=1000) - print(len(docs)) - print(docs[:5]) - - The filter is applied via context, so list_documents() only sees "Cats". - """ - from haiku.rag.client import HaikuRAG - config = AppConfig() async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -142,9 +72,6 @@ class TestClientAnalysisIntegration: async def test_analyze_search_and_identify_source( self, allow_model_requests, temp_db_path ): - """Test analysis agent can search and identify source documents.""" - from haiku.rag.client import HaikuRAG - config = AppConfig() async with HaikuRAG(temp_db_path, config=config, create=True) as client: @@ -163,20 +90,6 @@ class TestClientAnalysisIntegration: @pytest.mark.asyncio @pytest.mark.vcr() async def test_analyze_search_and_extract(self, allow_model_requests, temp_db_path): - """Test analysis agent can use search() to find content and extract information. - - Agent program: - results = search("document element types", limit=20) - print(len(results)) - for r in results[:5]: - print(r['document_title'], r['chunk_id'], r['score']) - print(r['content'][:200]) - - results = search("DocBank element types", limit=10) - ... - """ - from haiku.rag.client import HaikuRAG - pdf_path = Path("tests/data/doclaynet.pdf") config = AppConfig() config.processing.conversion_options.do_ocr = False @@ -190,9 +103,7 @@ class TestClientAnalysisIntegration: "List them all." ) - # The doclaynet.pdf defines exactly 11 class labels for document elements - # Normalize Unicode hyphens (U+2011 non-breaking hyphen) to regular hyphens - answer_lower = result.answer.lower().replace("\u2011", "-") + answer_lower = result.answer.lower().replace("‑", "-") expected_labels = [ "caption", "footnote", @@ -206,8 +117,6 @@ class TestClientAnalysisIntegration: "text", "title", ] - # Check that the agent found at least 6 of the 11 labels - # (LLM summaries may not always include all labels) found_labels = [ label for label in expected_labels