diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a1e171..e07e183f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - **`heading_level` and `tree_depth` on `DocumentItem`.** `extract_items` now captures docling's `SectionHeaderItem.level` (H1–H6 for headers, `0` elsewhere) and the traversal depth from `iterate_items()` for every item, persisting both in the `document_items` table. Foundations for tree-based document navigation in the analysis sandbox. The 0.48.0 migration adds the columns to existing DBs and backfills them from each doc's docling blob. - **`toc.json` in the analysis sandbox VFS.** Each document mounted under `/documents/{id}/` now exposes a `toc.json` view alongside `metadata.json`, `content.txt`, `items.jsonl`. Nodes carry `{self_ref, level, title, position, page_numbers, item_range, children}`; `item_range = [start, end_exclusive]` over the same `position` ints used in `items.jsonl`, so the agent can slice items by range to read a section. HTML/markdown ingests produce a real nested tree; PDF ingests produce a flat sibling list because docling collapses heading levels on PDFs. `tree: []` when the doc has no section headers. `items.jsonl` now surfaces `heading_level` and `tree_depth` on every row. +- **Multimodal analysis sandbox.** New `await show_image(document_id, self_ref)` external function inside the analysis sandbox. The picture's bytes are PIL-verified and attached to the `execute_code` tool's response as a pydantic-ai `BinaryContent` part, so a vision-capable driving model sees the actual figure alongside the printed stdout — same mechanism the QA agent's search tool uses. `search()` results now include a `picture_refs` key (subset of `doc_item_refs` labeled `picture`) so the agent can spot which results are figures with one lookup. + +### Changed + +- **`llm()` removed from the analysis sandbox.** The function was a thin wrapper that spun up an ad-hoc pydantic-ai `Agent` per call. The driving agent already is the LLM — there's no need for a sandbox-internal one. Sandbox external functions are now `search`, `list_documents`, and `show_image`. ### Changed diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/agent.py b/haiku_rag_slim/haiku/rag/agents/analysis/agent.py index 211e738a..1a03d33e 100644 --- a/haiku_rag_slim/haiku/rag/agents/analysis/agent.py +++ b/haiku_rag_slim/haiku/rag/agents/analysis/agent.py @@ -1,4 +1,5 @@ from pydantic_ai import Agent, RunContext +from pydantic_ai.messages import ToolReturn from haiku.rag.agents.analysis.dependencies import AnalysisDeps from haiku.rag.agents.analysis.models import CodeExecution, RawAnalysisResult @@ -32,19 +33,26 @@ def create_analysis_agent(config: AppConfig) -> Agent[AnalysisDeps, RawAnalysisR ) @agent.tool - async def execute_code(ctx: RunContext[AnalysisDeps], code: str) -> CodeExecution: + async def execute_code( + ctx: RunContext[AnalysisDeps], code: str + ) -> CodeExecution | ToolReturn: """Execute Python code in a sandboxed interpreter. - The code has access to search() and llm() functions, and a - virtual filesystem at /documents/ with document content and structure. + The code has access to search(), list_documents(), and show_image() + external functions, and a virtual filesystem at /documents/ with + document content and structure. Use print() to output results. - Use print() to output results. + When the code calls ``show_image(document_id, self_ref)``, the queued + picture bytes are attached to the tool response as ``BinaryContent`` + so a vision-capable driving model can actually see the image. Args: code: Python code to execute. Returns: - Structured result with success status, stdout, and stderr. + Structured result with success status, stdout, and stderr. Wrapped + in a ``ToolReturn`` carrying ``BinaryContent`` parts whenever the + code called ``show_image``. """ result = await ctx.deps.sandbox.execute(code) @@ -55,6 +63,10 @@ def create_analysis_agent(config: AppConfig) -> Agent[AnalysisDeps, RawAnalysisR success=result.success, ) + if result.binary_attachments: + return ToolReturn( + return_value=execution, content=list(result.binary_attachments) + ) return execution return agent diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py b/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py index a7924eef..c12ea4a1 100644 --- a/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py +++ b/haiku_rag_slim/haiku/rag/agents/analysis/prompts.py @@ -12,16 +12,28 @@ Inside execute_code, these functions are ALREADY available in the namespace. Do ### 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 +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 you can surface to the model via `show_image`. ### await list_documents() -> list[dict] List all documents in the knowledge base. Returns list of dicts with keys: id, title, uri, created_at -### await llm(prompt) -> str -Call an LLM directly with the given prompt. Returns the response as a string. -Use this for classification, summarization, extraction, or any task where you -already have the content and just need LLM reasoning. +### await show_image(document_id, self_ref) -> None +Surface a document picture to the driving LLM as a vision input. The picture's +bytes are attached to this `execute_code` tool's response as a `BinaryContent` +part, so a vision-capable model sees the actual image alongside the printed +output. Missing refs and unverifiable payloads are silent no-ops. Only useful +when the configured analysis model is vision-capable; otherwise the model +receives the bytes but ignores them. + +```python +results = await search("revenue chart", limit=5) +for r in results: + for ref in r["picture_refs"]: + await show_image(r["document_id"], ref) + print(f"showed {r['document_id']}:{ref}") +``` ## Document Filesystem @@ -155,7 +167,7 @@ Not supported: most imports (only `json`, `re`, `math`, `pathlib` are available) 3b. **Use toc.json for Section Navigation**: 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. **Use llm() for Reasoning**: When you have content and need classification, summarization, or extraction, use `llm()` rather than writing complex parsing logic. +6. **Show pictures explicitly**: When a search result has `picture_refs` and the question is about figures/charts/diagrams, call `show_image(doc_id, ref)` so the driving model can see the picture. Don't dump bytes into stdout. ## Output Format diff --git a/haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py b/haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py index 57d3b2db..16f0f7c0 100644 --- a/haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py +++ b/haiku_rag_slim/haiku/rag/agents/analysis/sandbox.py @@ -3,11 +3,12 @@ import atexit import concurrent.futures import json from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, Literal import pydantic_monty +from pydantic_ai import BinaryContent from pydantic_monty import CallbackFile, MemoryFile, MontyRepl, OSAccess from haiku.rag.agents.analysis.dependencies import AnalysisContext @@ -26,6 +27,7 @@ class SandboxResult: stdout: str stderr: str success: bool + binary_attachments: list[BinaryContent] = field(default_factory=list) _executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) @@ -95,8 +97,8 @@ class Sandbox: """Execute code in a sandboxed Python interpreter. Uses pydantic-monty, a minimal secure Python interpreter written in Rust. - External functions (search, llm) are called by Monty code using ``await`` - and resolved asynchronously on the host. + External functions (search, list_documents, show_image) are called by + Monty code using ``await`` and resolved asynchronously on the host. Documents are exposed via a virtual filesystem at ``/documents/{id}/``. The interpreter uses a REPL session — variables persist across @@ -113,6 +115,7 @@ class Sandbox: _search_results: "list[SearchResult]" _items_cache: dict[str, str] | None _toc_cache: dict[str, str] | None + _pending_binary: list[BinaryContent] _repl: MontyRepl | None _vfs: OSAccess | None @@ -128,6 +131,7 @@ class Sandbox: self._search_results = [] self._items_cache = None self._toc_cache = None + self._pending_binary = [] self._repl = None self._vfs = None @@ -144,21 +148,29 @@ class Sandbox: results = await rag.search(query, limit=limit, filter=context.filter) expanded = await rag.expand_context(results) self._search_results.extend(expanded) - return [ - { - "chunk_id": r.chunk_id, - "content": r.content, - "document_id": r.document_id, - "document_title": r.document_title, - "document_uri": r.document_uri, - "score": r.score, - "page_numbers": r.page_numbers, - "headings": r.headings, - "doc_item_refs": r.doc_item_refs, - "labels": r.labels, - } - for r in expanded - ] + out: list[dict[str, Any]] = [] + for r in expanded: + picture_refs = [ + ref + for ref, lbl in zip(r.doc_item_refs, r.labels, strict=False) + if lbl == "picture" + ] + out.append( + { + "chunk_id": r.chunk_id, + "content": r.content, + "document_id": r.document_id, + "document_title": r.document_title, + "document_uri": r.document_uri, + "score": r.score, + "page_numbers": r.page_numbers, + "headings": r.headings, + "doc_item_refs": r.doc_item_refs, + "labels": r.labels, + "picture_refs": picture_refs, + } + ) + return out async def list_documents() -> list[dict[str, Any]]: from haiku.rag.client import HaikuRAG @@ -175,20 +187,41 @@ class Sandbox: for d in docs ] - async def llm(prompt: str) -> str: - from pydantic_ai import Agent + sandbox = self - from haiku.rag.utils import get_model + async def show_image(document_id: str, self_ref: str) -> None: + """Surface a document picture to the driving LLM as a vision input. - model = get_model(config.analysis.model, config) - agent: Agent[None, str] = Agent(model, output_type=str) - result = await agent.run(prompt) - return result.output + Looks up the picture's bytes by (document_id, self_ref), verifies the + payload via PIL, and queues a ``BinaryContent`` part on the next + ``execute_code`` tool return. The driving model sees the picture as + content alongside the textual stdout. Missing refs and unverifiable + payloads are silent no-ops. + """ + from io import BytesIO + + from PIL import Image + + from haiku.rag.client import HaikuRAG + + async with HaikuRAG(db_path, config=config, read_only=True) as rag: + data = await rag.document_item_repository.get_picture_bytes( + document_id, self_ref + ) + if not data: + return + try: + Image.open(BytesIO(data)).verify() + except Exception: + return + sandbox._pending_binary.append( + BinaryContent(data=data, media_type="image/png", identifier=self_ref) + ) return { "search": search, "list_documents": list_documents, - "llm": llm, + "show_image": show_image, } async def _build_vfs(self) -> OSAccess: @@ -376,9 +409,12 @@ class Sandbox: """Execute Python code in the Monty REPL. Variables persist across calls within the same Sandbox instance. + Binary attachments queued by ``show_image`` during this call are + drained into the returned ``SandboxResult.binary_attachments``. """ repl, vfs = await self._ensure_initialized() external_fns = self._build_external_functions() + self._pending_binary = [] stdout_lines: list[str] = [] @@ -401,7 +437,12 @@ class Sandbox: stdout = "".join(stdout_lines) if len(stdout) > max_chars: stdout = stdout[:max_chars] + "\n... (output truncated)" - return SandboxResult(stdout=stdout, stderr=str(e), success=False) + return SandboxResult( + stdout=stdout, + stderr=str(e), + success=False, + binary_attachments=self._pending_binary, + ) stdout = "".join(stdout_lines) if output is not None: @@ -414,4 +455,9 @@ class Sandbox: stdout_with_output[:max_chars] + "\n... (output truncated)" ) - return SandboxResult(stdout=stdout_with_output, stderr="", success=True) + return SandboxResult( + stdout=stdout_with_output, + stderr="", + success=True, + binary_attachments=self._pending_binary, + ) diff --git a/haiku_rag_slim/haiku/rag/skill_generator/templates/SKILL.md.j2 b/haiku_rag_slim/haiku/rag/skill_generator/templates/SKILL.md.j2 index cf83f3fb..505d4e9a 100644 --- a/haiku_rag_slim/haiku/rag/skill_generator/templates/SKILL.md.j2 +++ b/haiku_rag_slim/haiku/rag/skill_generator/templates/SKILL.md.j2 @@ -26,7 +26,7 @@ Retrieve a document by ID, title, or URI. Partial matches work. {% if "execute_code" in tool_names %} ### execute_code -Execute Python code in a sandboxed interpreter. Inside the code you have access to `await search()`, `await list_documents()`, `await llm()`, and a virtual filesystem at `/documents/` with document content and structure. +Execute Python code in a sandboxed interpreter. Inside the code you have access to `await search()`, `await list_documents()`, `await show_image(document_id, self_ref)`, and a virtual filesystem at `/documents/` with document content and structure. {% endif %} {% if "cite" in tool_names %} diff --git a/haiku_rag_slim/haiku/rag/skills/_tools.py b/haiku_rag_slim/haiku/rag/skills/_tools.py index 5c8d5279..928bbec5 100644 --- a/haiku_rag_slim/haiku/rag/skills/_tools.py +++ b/haiku_rag_slim/haiku/rag/skills/_tools.py @@ -242,9 +242,10 @@ def create_skill_tools( async def execute_code(ctx: RunContext[AnalysisRunDeps], code: str) -> str: """Execute Python code in a sandboxed interpreter. - The code has access to search(), list_documents(), llm() functions - and a virtual filesystem at /documents/ with document content and - structure (metadata.json, content.txt, items.jsonl per document). + The code has access to search(), list_documents(), show_image() + functions and a virtual filesystem at /documents/ with document + content and structure (metadata.json, content.txt, items.jsonl, + toc.json per document). Use print() to output results. Variables persist between calls within the same skill invocation. diff --git a/haiku_rag_slim/haiku/rag/skills/rag-analysis/SKILL.md b/haiku_rag_slim/haiku/rag/skills/rag-analysis/SKILL.md index 453480bf..7cbcd9aa 100644 --- a/haiku_rag_slim/haiku/rag/skills/rag-analysis/SKILL.md +++ b/haiku_rag_slim/haiku/rag/skills/rag-analysis/SKILL.md @@ -18,9 +18,9 @@ You solve complex analytical questions by writing and executing Python code agai Execute Python code in a sandboxed interpreter. Variables persist between calls — you can build state incrementally. Use `print()` to output results. Inside the code, these functions are available (use `await`): -- `await search(query, limit=10)` → list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings, doc_item_refs, labels +- `await search(query, limit=10)` → list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings, doc_item_refs, labels, picture_refs (subset of doc_item_refs labeled `picture`) - `await list_documents()` → list of dicts with keys: id, title, uri, created_at -- `await llm(prompt)` → string response from an LLM (for classification, summarization, extraction) +- `await show_image(document_id, self_ref)` → attaches a document picture's bytes as a `BinaryContent` part on this tool's response so a vision-capable model sees it. Silent no-op for missing or unverifiable refs. Available modules: `json`, `re`, `math`, `pathlib` Not supported: class definitions, generators/yield, match statements, decorators, `with` statements @@ -102,6 +102,6 @@ Search results include `doc_item_refs` (e.g. `["#/texts/48", "#/tables/0"]`) tha - Variables persist between `execute_code` calls — you can search in one call and process results in the next - Use `print()` to output results — the output is your only feedback - Always execute code to answer questions — don't just describe what code would do -- Use `await` for all async functions inside execute_code (search, list_documents, llm) +- Use `await` for all async functions inside execute_code (search, list_documents, show_image) - Use `Path.read_text()` to read files — do NOT use `open()`, `with` statements, or `collections` module - Do NOT include chunk IDs or UUIDs in your answer text — use the `cite` tool separately diff --git a/tests/agents/analysis/test_agent.py b/tests/agents/analysis/test_agent.py index 3f2ec6ff..f10cb978 100644 --- a/tests/agents/analysis/test_agent.py +++ b/tests/agents/analysis/test_agent.py @@ -160,59 +160,6 @@ class TestClientAnalysisIntegration: assert "Animal Facts" in result.answer - @pytest.mark.asyncio - @pytest.mark.vcr() - async def test_analyze_semantic_analysis_with_llm( - self, allow_model_requests, temp_db_path - ): - """Test analysis agent can use llm() for semantic analysis combined with computation. - - Agent program: - docs = list_documents(limit=100) - print(len(docs)) - print([d['title'] for d in docs[:20]]) - - sentiments = {} - for title in ['Q1 Update', 'Q2 Update', 'Q3 Update']: - content = get_document(title) - if content: - result = llm(f"Classify sentiment as positive/negative/mixed: {content}") - sentiments[title] = result - print(sentiments) - """ - from haiku.rag.client import HaikuRAG - - config = AppConfig() - - async with HaikuRAG(temp_db_path, config=config, create=True) as client: - await client.create_document( - "The new product launch exceeded expectations. Sales grew 40% " - "and customer feedback has been overwhelmingly positive. " - "Team morale is at an all-time high.", - title="Q1 Update", - ) - await client.create_document( - "We faced significant challenges this quarter. Supply chain issues " - "caused delays, and we missed our revenue target by 15%. " - "Several key employees left the company.", - title="Q2 Update", - ) - await client.create_document( - "Mixed results this quarter. While product quality improved, " - "marketing campaigns underperformed. Revenue was flat compared " - "to last year but customer retention increased.", - title="Q3 Update", - ) - - result = await client.analyze( - "Analyze the sentiment of each quarterly update. " - "How many quarters were positive, negative, and mixed?" - ) - - # Should identify: Q1=positive, Q2=negative, Q3=mixed - assert "positive" in result.answer.lower() - assert "negative" in result.answer.lower() - @pytest.mark.asyncio @pytest.mark.vcr() async def test_analyze_search_and_extract(self, allow_model_requests, temp_db_path): diff --git a/tests/agents/analysis/test_sandbox.py b/tests/agents/analysis/test_sandbox.py index 26bc4679..3809f880 100644 --- a/tests/agents/analysis/test_sandbox.py +++ b/tests/agents/analysis/test_sandbox.py @@ -446,22 +446,3 @@ class TestSandboxPreloadedDocuments: assert "2" in result.stdout assert "Doc A" in result.stdout assert "Doc B" in result.stdout - - -class TestSandboxLLM: - """Test llm() external function.""" - - @pytest.mark.asyncio - @pytest.mark.vcr() - async def test_llm_function(self, allow_model_requests, temp_db_path): - """Test llm() calls the model and returns a string.""" - async with HaikuRAG(temp_db_path, create=True): - config = AppConfig() - context = AnalysisContext() - sb = Sandbox(db_path=temp_db_path, config=config, context=context) - result = await sb.execute( - "answer = await llm('What is 2 + 2? Reply with just the number.')\n" - "print(answer)" - ) - assert result.success - assert "4" in result.stdout diff --git a/tests/agents/analysis/test_sandbox_multimodal.py b/tests/agents/analysis/test_sandbox_multimodal.py new file mode 100644 index 00000000..b668573e --- /dev/null +++ b/tests/agents/analysis/test_sandbox_multimodal.py @@ -0,0 +1,186 @@ +"""Tests for the multimodal sandbox surface: show_image, picture_refs in +search results, binary_attachments on SandboxResult, and the absence of +the old llm() external function. +""" + +import base64 +from io import BytesIO + +import pytest +from PIL import Image + +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.store.models.chunk import SearchResult +from haiku.rag.store.models.document_item import DocumentItem + + +def _png_bytes(color: str = "red", size: tuple[int, int] = (8, 8)) -> bytes: + """Generate a real PNG so PIL.Image.verify() accepts it.""" + img = Image.new("RGB", size, color) + buf = BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + +async def _seed_doc_with_picture(client, *, png: bytes) -> tuple[str, str]: + """Create a Document row, replace its items with one picture row carrying + the given bytes. Returns (doc_id, self_ref).""" + doc = await client.create_document(content="x", uri="test://pic", title="Pic") + await client.document_item_repository.delete_by_document_id(doc.id) + self_ref = "#/pictures/0" + items = [ + DocumentItem( + document_id=doc.id, + position=0, + self_ref=self_ref, + label="picture", + text="", + page_numbers=[1], + picture_data=png, + ) + ] + await client.document_item_repository.create_items(doc.id, items) + return doc.id, self_ref + + +@pytest.mark.asyncio +class TestShowImage: + """show_image() appends a BinaryContent attachment when bytes verify.""" + + async def test_appends_binary_attachment(self, temp_db_path): + png = _png_bytes("red") + async with HaikuRAG(temp_db_path, create=True) as client: + doc_id, ref = await _seed_doc_with_picture(client, png=png) + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + result = await sandbox.execute( + f"await show_image({doc_id!r}, {ref!r})\nprint('done')" + ) + assert result.success, result.stderr + assert len(result.binary_attachments) == 1 + att = result.binary_attachments[0] + assert att.media_type == "image/png" + assert att.identifier == ref + assert att.data == png + + async def test_missing_picture_is_silent_noop(self, temp_db_path): + png = _png_bytes("red") + async with HaikuRAG(temp_db_path, create=True) as client: + doc_id, _ = await _seed_doc_with_picture(client, png=png) + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + result = await sandbox.execute( + f"await show_image({doc_id!r}, '#/pictures/999')\nprint('ok')" + ) + assert result.success, result.stderr + assert result.binary_attachments == [] + + async def test_invalid_bytes_rejected(self, temp_db_path): + # Garbage bytes — PIL.verify() should refuse, no attachment emitted. + garbage = b"this is not a PNG" + async with HaikuRAG(temp_db_path, create=True) as client: + doc_id, ref = await _seed_doc_with_picture(client, png=garbage) + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + result = await sandbox.execute( + f"await show_image({doc_id!r}, {ref!r})\nprint('checked')" + ) + assert result.success, result.stderr + assert result.binary_attachments == [] + + async def test_attachments_reset_across_executes(self, temp_db_path): + png = _png_bytes("red") + async with HaikuRAG(temp_db_path, create=True) as client: + doc_id, ref = await _seed_doc_with_picture(client, png=png) + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + first = await sandbox.execute( + f"await show_image({doc_id!r}, {ref!r})\nprint('first')" + ) + second = await sandbox.execute("print('second')") + assert first.success and len(first.binary_attachments) == 1 + assert second.success and second.binary_attachments == [] + + +@pytest.mark.asyncio +class TestSearchPictureRefs: + """search() result dicts carry a `picture_refs` list (subset of + doc_item_refs labeled 'picture'). No `image_data` base64 in the dict.""" + + async def test_picture_refs_extracted_from_labels(self, temp_db_path, monkeypatch): + # Build a fake SearchResult with mixed labels so we don't need an embedder. + synthetic = [ + SearchResult( + chunk_id="c1", + content="hit", + document_id="d1", + document_uri="test://d1", + document_title="D1", + score=1.0, + page_numbers=[1], + headings=None, + doc_item_refs=["#/texts/0", "#/pictures/0", "#/pictures/1"], + labels=["text", "picture", "picture"], + ), + SearchResult( + chunk_id="c2", + content="text only", + document_id="d1", + document_uri="test://d1", + document_title="D1", + score=0.5, + page_numbers=[2], + headings=None, + doc_item_refs=["#/texts/5"], + labels=["text"], + ), + ] + + async def fake_search(self, *args, **kwargs): + return synthetic + + async def fake_expand_context(self, results): + return results + + # Patch HaikuRAG.search and expand_context so the sandbox closure runs + # without an embedder. The sandbox opens its own HaikuRAG instance, so + # we patch on the class. + monkeypatch.setattr(HaikuRAG, "search", fake_search) + monkeypatch.setattr(HaikuRAG, "expand_context", fake_expand_context) + + async with HaikuRAG(temp_db_path, create=True): + pass # ensure the DB exists so the sandbox can open it read-only + + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + external = sandbox._build_external_functions() + results = await external["search"]("anything") + + assert len(results) == 2 + assert results[0]["picture_refs"] == ["#/pictures/0", "#/pictures/1"] + assert results[1]["picture_refs"] == [] + # No raw base64 garbage in the dict. + assert "image_data" not in results[0] + + +@pytest.mark.asyncio +class TestExternalFunctionsShape: + """llm() is gone; show_image() is present.""" + + async def test_llm_gone_show_image_present(self, temp_db_path): + async with HaikuRAG(temp_db_path, create=True): + pass + sandbox = Sandbox(temp_db_path, AppConfig(), AnalysisContext()) + external = sandbox._build_external_functions() + assert "llm" not in external + assert "show_image" in external + assert "search" in external + assert "list_documents" in external + + +# Silence unused-import flake — base64 is reserved for follow-up tests that +# decode attachment.data and compare. Kept eagerly imported for parity with +# the QA binary-content tests. +_ = base64 diff --git a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_semantic_analysis_with_llm.yaml b/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_semantic_analysis_with_llm.yaml deleted file mode 100644 index 95e55484..00000000 --- a/tests/cassettes/test_analysis/TestClientAnalysisIntegration.test_analyze_semantic_analysis_with_llm.yaml +++ /dev/null @@ -1,1055 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '222' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - The new product launch exceeded expectations. Sales grew 40% and customer feedback has been overwhelmingly positive. - Team morale is at an all-time high. - 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: 31 - total_tokens: 31 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '231' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - We faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target - by 15%. Several key employees left the company. - 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: 32 - total_tokens: 32 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '238' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - Mixed results this quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared - to last year but customer retention increased. - 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: 29 - total_tokens: 29 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '7320' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: |- - You are a Recursive Language Model (RLM) 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 described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - - 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 - - ## Available Functions - - ### await search(query, limit=10) -> list[dict] - Search the knowledge base using hybrid search (vector + full-text). - Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings - - ### await list_documents(limit=10, offset=0) -> list[dict] - List available documents in the knowledge base. - Returns list of dicts with keys: id, title, uri, created_at - - ### await get_document(id_or_title) -> str | None - Get the full text content of a document by ID, title, or URI. - Returns the document content as a string, or None if not found. - - ### await get_chunk(chunk_id) -> dict | None - Get a specific chunk by its ID (from search results). - Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels - Use this to retrieve full chunk details and metadata for citation. - - ### await get_docling_document(document_id) -> dict | None - Get the full document structure as a dict (DoclingDocument format). - Use `list_documents()` or search results to get document IDs first. - - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) - - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` - - `pictures`: list of figures/images with metadata - - `pages`: page dimensions and metadata - - ### await llm(prompt) -> str - Call an LLM directly with the given prompt. Returns the response as a string. - Use this for classification, summarization, extraction, or any task where you - already have the content and just need LLM reasoning. - - ## 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. - - Not supported: most imports (only `json`, `re`, `math` are available), class definitions, generators/yield, match statements, decorators, `with` statements. - - For pattern matching or text extraction, use `import re`, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. - - ## Strategy Guide - - 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. - 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. - 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. - 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. - - ## Example Patterns - - ### Counting documents matching a condition - ```python - docs = await list_documents(limit=100) - count = 0 - for doc in docs: - content = await get_document(doc['id']) - if content and 'keyword' in content.lower(): - count += 1 - print(f"Found in: {doc['title']}") - print(f"Total: {count}") - ``` - - ### Extracting data with regex - ```python - import re - numbers = [] - results = await search("financial data", limit=20) - for r in results: - amounts = re.findall(r'\$([\d,]+)', r['content']) - for a in amounts: - numbers.append(int(a.replace(',', ''))) - if numbers: - print(f"Average: {sum(numbers) / len(numbers)}") - ``` - - ### Extracting tables from a document - ```python - docs = await list_documents(limit=10) - for d in docs: - doc = await get_docling_document(d['id']) - if doc: - tables = doc.get('tables', []) - if tables: - print(f"{d['title']}: {len(tables)} table(s)") - for i, table in enumerate(tables): - grid = table.get('data', {}).get('grid', []) - for row in grid: - cells = [cell.get('text', '') for cell in row] - print(f" Table {i}: {cells}") - ``` - - ## Output Format - - Your final response MUST be valid JSON matching this exact schema: - ```json - {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} - ``` - - - `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: {"answer": "...", "program": "..."} - - You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. - role: system - - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? - role: user - model: gpt-oss - reasoning_effort: low - response_format: - json_schema: - description: Result from RLM agent execution. - name: RLMResult - schema: - additionalProperties: false - properties: - answer: - description: The answer to the user's question - type: string - program: - description: The final consolidated program - type: string - required: - - answer - - program - type: object - strict: true - type: json_schema - stream: false - temperature: 0.0 - tool_choice: auto - tools: - - function: - description: |- - Execute Python code in a sandboxed interpreter. - - The code has access to haiku.rag functions (search, list_documents, - get_document, get_chunk, llm). - - Use print() to output results. - - Structured result with success status, stdout, and stderr. - - name: execute_code - parameters: - additionalProperties: false - properties: - code: - description: Python code to execute. - type: string - required: - - code - type: object - strict: true - type: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '725' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: We need quarterly updates. Likely documents contain quarterly updates. Search for "quarterly update". - role: assistant - tool_calls: - - function: - arguments: '{"code":"import asyncio\nresults = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor - r in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''score''])\n"}' - name: execute_code - id: call_gt6nr456 - index: 0 - type: function - created: 1773329127 - id: chatcmpl-207 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 92 - prompt_tokens: 1603 - total_tokens: 1695 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '86' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - quarterly update - 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: 4 - total_tokens: 4 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '8300' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: |- - You are a Recursive Language Model (RLM) 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 described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - - 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 - - ## Available Functions - - ### await search(query, limit=10) -> list[dict] - Search the knowledge base using hybrid search (vector + full-text). - Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings - - ### await list_documents(limit=10, offset=0) -> list[dict] - List available documents in the knowledge base. - Returns list of dicts with keys: id, title, uri, created_at - - ### await get_document(id_or_title) -> str | None - Get the full text content of a document by ID, title, or URI. - Returns the document content as a string, or None if not found. - - ### await get_chunk(chunk_id) -> dict | None - Get a specific chunk by its ID (from search results). - Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels - Use this to retrieve full chunk details and metadata for citation. - - ### await get_docling_document(document_id) -> dict | None - Get the full document structure as a dict (DoclingDocument format). - Use `list_documents()` or search results to get document IDs first. - - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) - - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` - - `pictures`: list of figures/images with metadata - - `pages`: page dimensions and metadata - - ### await llm(prompt) -> str - Call an LLM directly with the given prompt. Returns the response as a string. - Use this for classification, summarization, extraction, or any task where you - already have the content and just need LLM reasoning. - - ## 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. - - Not supported: most imports (only `json`, `re`, `math` are available), class definitions, generators/yield, match statements, decorators, `with` statements. - - For pattern matching or text extraction, use `import re`, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. - - ## Strategy Guide - - 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. - 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. - 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. - 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. - - ## Example Patterns - - ### Counting documents matching a condition - ```python - docs = await list_documents(limit=100) - count = 0 - for doc in docs: - content = await get_document(doc['id']) - if content and 'keyword' in content.lower(): - count += 1 - print(f"Found in: {doc['title']}") - print(f"Total: {count}") - ``` - - ### Extracting data with regex - ```python - import re - numbers = [] - results = await search("financial data", limit=20) - for r in results: - amounts = re.findall(r'\$([\d,]+)', r['content']) - for a in amounts: - numbers.append(int(a.replace(',', ''))) - if numbers: - print(f"Average: {sum(numbers) / len(numbers)}") - ``` - - ### Extracting tables from a document - ```python - docs = await list_documents(limit=10) - for d in docs: - doc = await get_docling_document(d['id']) - if doc: - tables = doc.get('tables', []) - if tables: - print(f"{d['title']}: {len(tables)} table(s)") - for i, table in enumerate(tables): - grid = table.get('data', {}).get('grid', []) - for row in grid: - cells = [cell.get('text', '') for cell in row] - print(f" Table {i}: {cells}") - ``` - - ## Output Format - - Your final response MUST be valid JSON matching this exact schema: - ```json - {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} - ``` - - - `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: {"answer": "...", "program": "..."} - - You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. - role: system - - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? - role: user - - content: null - reasoning: We need quarterly updates. Likely documents contain quarterly updates. Search for "quarterly update". - role: assistant - tool_calls: - - function: - arguments: '{"code":"import asyncio\nresults = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor - r in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''score''])\n"}' - name: execute_code - id: call_gt6nr456 - type: function - - content: '{"code":"import asyncio\nresults = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor - r in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''score''])\n","stdout":"3\n4633d592-5df2-4e33-8810-0836f5de1781 - Q3 Update 0.032786883413791656\n3be7131b-6ac5-4eb8-8c4b-af4826a84c29 Q2 Update 0.032258063554763794\n58bde75b-5c9c-4f6f-b76e-88844b0ce6ac - Q1 Update 0.01587301678955555\n","stderr":"","success":true}' - role: tool - tool_call_id: call_gt6nr456 - model: gpt-oss - reasoning_effort: low - response_format: - json_schema: - description: Result from RLM agent execution. - name: RLMResult - schema: - additionalProperties: false - properties: - answer: - description: The answer to the user's question - type: string - program: - description: The final consolidated program - type: string - required: - - answer - - program - type: object - strict: true - type: json_schema - stream: false - temperature: 0.0 - tool_choice: auto - tools: - - function: - description: |- - Execute Python code in a sandboxed interpreter. - - The code has access to haiku.rag functions (search, list_documents, - get_document, get_chunk, llm). - - Use print() to output results. - - Structured result with success status, stdout, and stderr. - - name: execute_code - parameters: - additionalProperties: false - properties: - code: - description: Python code to execute. - type: string - required: - - code - type: object - strict: true - type: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '938' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: 'We have 3 chunks: Q3, Q2, Q1. Need sentiment of each. Use llm on content. Get chunks.' - role: assistant - tool_calls: - - function: - arguments: '{"code":"import asyncio\nchunks = [\"4633d592-5df2-4e33-8810-0836f5de1781\",\"3be7131b-6ac5-4eb8-8c4b-af4826a84c29\",\"58bde75b-5c9c-4f6f-b76e-88844b0ce6ac\"]\nfor - cid in chunks:\n chunk = await get_chunk(cid)\n print(cid, chunk[''document_title''])\n sentiment - = await llm(f\"Determine if the following text is positive, negative, or mixed. Text: {chunk[''content'']}\")\n print(sentiment)\n"}' - name: execute_code - id: call_b65254bz - index: 0 - type: function - created: 1773329139 - id: chatcmpl-759 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 202 - prompt_tokens: 1884 - total_tokens: 2086 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '358' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: 'Determine if the following text is positive, negative, or mixed. Text: Mixed results this quarter. While - product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year but customer - retention increased.' - role: user - model: gpt-oss - reasoning_effort: low - stream: false - temperature: 0.0 - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '424' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: Mixed. - reasoning: 'We need to classify sentiment: positive, negative, or mixed. The text has both positive and negative - aspects. So mixed.' - role: assistant - created: 1773329150 - id: chatcmpl-678 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 37 - prompt_tokens: 109 - total_tokens: 146 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '351' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: 'Determine if the following text is positive, negative, or mixed. Text: We faced significant challenges this - quarter. Supply chain issues caused delays, and we missed our revenue target by 15%. Several key employees left - the company.' - role: user - model: gpt-oss - reasoning_effort: low - stream: false - temperature: 0.0 - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '339' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: '**Negative**' - reasoning: 'We need sentiment: negative.' - role: assistant - created: 1773329166 - id: chatcmpl-974 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 19 - prompt_tokens: 112 - total_tokens: 131 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '342' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: 'Determine if the following text is positive, negative, or mixed. Text: The new product launch exceeded expectations. - Sales grew 40% and customer feedback has been overwhelmingly positive. Team morale is at an all-time high.' - role: user - model: gpt-oss - reasoning_effort: low - stream: false - temperature: 0.0 - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '348' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: Positive. - reasoning: 'We need to classify sentiment: positive.' - role: assistant - created: 1773329173 - id: chatcmpl-656 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 20 - prompt_tokens: 111 - total_tokens: 131 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '9692' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: |- - You are a Recursive Language Model (RLM) 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 described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - - 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 - - ## Available Functions - - ### await search(query, limit=10) -> list[dict] - Search the knowledge base using hybrid search (vector + full-text). - Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings - - ### await list_documents(limit=10, offset=0) -> list[dict] - List available documents in the knowledge base. - Returns list of dicts with keys: id, title, uri, created_at - - ### await get_document(id_or_title) -> str | None - Get the full text content of a document by ID, title, or URI. - Returns the document content as a string, or None if not found. - - ### await get_chunk(chunk_id) -> dict | None - Get a specific chunk by its ID (from search results). - Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels - Use this to retrieve full chunk details and metadata for citation. - - ### await get_docling_document(document_id) -> dict | None - Get the full document structure as a dict (DoclingDocument format). - Use `list_documents()` or search results to get document IDs first. - - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) - - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` - - `pictures`: list of figures/images with metadata - - `pages`: page dimensions and metadata - - ### await llm(prompt) -> str - Call an LLM directly with the given prompt. Returns the response as a string. - Use this for classification, summarization, extraction, or any task where you - already have the content and just need LLM reasoning. - - ## 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. - - Not supported: most imports (only `json`, `re`, `math` are available), class definitions, generators/yield, match statements, decorators, `with` statements. - - For pattern matching or text extraction, use `import re`, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. - - ## Strategy Guide - - 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. - 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. - 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. - 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. - - ## Example Patterns - - ### Counting documents matching a condition - ```python - docs = await list_documents(limit=100) - count = 0 - for doc in docs: - content = await get_document(doc['id']) - if content and 'keyword' in content.lower(): - count += 1 - print(f"Found in: {doc['title']}") - print(f"Total: {count}") - ``` - - ### Extracting data with regex - ```python - import re - numbers = [] - results = await search("financial data", limit=20) - for r in results: - amounts = re.findall(r'\$([\d,]+)', r['content']) - for a in amounts: - numbers.append(int(a.replace(',', ''))) - if numbers: - print(f"Average: {sum(numbers) / len(numbers)}") - ``` - - ### Extracting tables from a document - ```python - docs = await list_documents(limit=10) - for d in docs: - doc = await get_docling_document(d['id']) - if doc: - tables = doc.get('tables', []) - if tables: - print(f"{d['title']}: {len(tables)} table(s)") - for i, table in enumerate(tables): - grid = table.get('data', {}).get('grid', []) - for row in grid: - cells = [cell.get('text', '') for cell in row] - print(f" Table {i}: {cells}") - ``` - - ## Output Format - - Your final response MUST be valid JSON matching this exact schema: - ```json - {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} - ``` - - - `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: {"answer": "...", "program": "..."} - - You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. - role: system - - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? - role: user - - content: null - reasoning: We need quarterly updates. Likely documents contain quarterly updates. Search for "quarterly update". - role: assistant - tool_calls: - - function: - arguments: '{"code":"import asyncio\nresults = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor - r in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''score''])\n"}' - name: execute_code - id: call_gt6nr456 - type: function - - content: '{"code":"import asyncio\nresults = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor - r in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''score''])\n","stdout":"3\n4633d592-5df2-4e33-8810-0836f5de1781 - Q3 Update 0.032786883413791656\n3be7131b-6ac5-4eb8-8c4b-af4826a84c29 Q2 Update 0.032258063554763794\n58bde75b-5c9c-4f6f-b76e-88844b0ce6ac - Q1 Update 0.01587301678955555\n","stderr":"","success":true}' - role: tool - tool_call_id: call_gt6nr456 - - content: null - reasoning: 'We have 3 chunks: Q3, Q2, Q1. Need sentiment of each. Use llm on content. Get chunks.' - role: assistant - tool_calls: - - function: - arguments: '{"code":"import asyncio\nchunks = [\"4633d592-5df2-4e33-8810-0836f5de1781\",\"3be7131b-6ac5-4eb8-8c4b-af4826a84c29\",\"58bde75b-5c9c-4f6f-b76e-88844b0ce6ac\"]\nfor - cid in chunks:\n chunk = await get_chunk(cid)\n print(cid, chunk[''document_title''])\n sentiment = - await llm(f\"Determine if the following text is positive, negative, or mixed. Text: {chunk[''content'']}\")\n print(sentiment)\n"}' - name: execute_code - id: call_b65254bz - type: function - - content: '{"code":"import asyncio\nchunks = [\"4633d592-5df2-4e33-8810-0836f5de1781\",\"3be7131b-6ac5-4eb8-8c4b-af4826a84c29\",\"58bde75b-5c9c-4f6f-b76e-88844b0ce6ac\"]\nfor - cid in chunks:\n chunk = await get_chunk(cid)\n print(cid, chunk[''document_title''])\n sentiment = await - llm(f\"Determine if the following text is positive, negative, or mixed. Text: {chunk[''content'']}\")\n print(sentiment)\n","stdout":"4633d592-5df2-4e33-8810-0836f5de1781 - Q3 Update\nMixed.\n3be7131b-6ac5-4eb8-8c4b-af4826a84c29 Q2 Update\n**Negative**\n58bde75b-5c9c-4f6f-b76e-88844b0ce6ac - Q1 Update\nPositive.\n","stderr":"","success":true}' - role: tool - tool_call_id: call_b65254bz - model: gpt-oss - reasoning_effort: low - response_format: - json_schema: - description: Result from RLM agent execution. - name: RLMResult - schema: - additionalProperties: false - properties: - answer: - description: The answer to the user's question - type: string - program: - description: The final consolidated program - type: string - required: - - answer - - program - type: object - strict: true - type: json_schema - stream: false - temperature: 0.0 - tool_choice: auto - tools: - - function: - description: |- - Execute Python code in a sandboxed interpreter. - - The code has access to haiku.rag functions (search, list_documents, - get_document, get_chunk, llm). - - Use print() to output results. - - Structured result with success status, stdout, and stderr. - - name: execute_code - parameters: - additionalProperties: false - properties: - code: - description: Python code to execute. - type: string - required: - - code - type: object - strict: true - type: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '1730' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: '{"answer":"There were three quarterly updates. The sentiment analysis shows: 1 positive quarter (Q1), - 1 negative quarter (Q2), and 1 mixed quarter (Q3).","program":"import asyncio\n\n# Define the chunk IDs for the - quarterly updates\nchunk_ids = [\n \"4633d592-5df2-4e33-8810-0836f5de1781\", # Q3 Update\n \"3be7131b-6ac5-4eb8-8c4b-af4826a84c29\", # - Q2 Update\n \"58bde75b-5c9c-4f6f-b76e-88844b0ce6ac\" # Q1 Update\n]\n\n# Function to get sentiment for a - chunk\nasync def get_sentiment(chunk_id):\n chunk = await get_chunk(chunk_id)\n prompt = f\"Determine if - the following text is positive, negative, or mixed. Text: {chunk[''content'']}\"\n sentiment = await llm(prompt)\n return - chunk[''document_title''], sentiment.strip()\n\n# Main async routine\nasync def main():\n results = []\n for - cid in chunk_ids:\n title, sentiment = await get_sentiment(cid)\n results.append((title, sentiment))\n # - Count sentiments\n counts = {\"Positive\":0, \"Negative\":0, \"Mixed\":0}\n for _, s in results:\n key - = s.split()[0] # first word (Positive, Negative, Mixed)\n counts[key] += 1\n # Print summary\n print(\"Sentiment - counts:\")\n for k, v in counts.items():\n print(f\"{k}: {v}\")\n\n# Run the async main function\nasyncio.run(main())"}' - reasoning: 'Counts: positive 1, negative 1, mixed 1.' - role: assistant - created: 1773329203 - id: chatcmpl-158 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 405 - prompt_tokens: 2378 - total_tokens: 2783 - status: - code: 200 - message: OK -version: 1 diff --git a/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml b/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml deleted file mode 100644 index f3dae05e..00000000 --- a/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml +++ /dev/null @@ -1,51 +0,0 @@ -interactions: -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '143' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - messages: - - content: What is 2 + 2? Reply with just the number. - role: user - model: gpt-oss - reasoning_effort: low - stream: false - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '311' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: '4' - reasoning: Just reply 4. - role: assistant - created: 1771924616 - id: chatcmpl-525 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 16 - prompt_tokens: 81 - total_tokens: 97 - status: - code: 200 - message: OK -version: 1