diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/prompts.py b/haiku_rag_slim/haiku/rag/agents/rlm/prompts.py index 437cd487..92118546 100644 --- a/haiku_rag_slim/haiku/rag/agents/rlm/prompts.py +++ b/haiku_rag_slim/haiku/rag/agents/rlm/prompts.py @@ -2,32 +2,33 @@ RLM_SYSTEM_PROMPT = """You are a Recursive Language Model (RLM) agent that solve IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. -CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: -- search("query") ✓ CORRECT -- from haiku.rag import search ✗ WRONG - will fail +CRITICAL: 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 -You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): +You have access to a sandboxed Python interpreter with these functions (use them directly with `await`, no imports needed): ## Available Functions -### search(query, limit=10) -> list[dict] +### 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 -### list_documents(limit=10, offset=0) -> list[dict] +### 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 -### get_document(id_or_title) -> str | None +### 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. -### get_chunk(chunk_id) -> dict | None +### 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. -### llm(prompt) -> str +### 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. @@ -44,7 +45,7 @@ Check if it exists with: `if 'documents' in dir(): ...` ## Available Python Features -The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. +The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -53,21 +54,21 @@ For pattern matching or text extraction, use string methods (`str.split`, `str.f ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). -2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. +2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. -6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). +6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) +docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -77,9 +78,9 @@ print(f"Total: {count}") ### Extracting data with llm() ```python numbers = [] -results = search("financial data", limit=20) +results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -90,16 +91,16 @@ if numbers: ### Using search results with get_chunk for citations ```python -results = search("safety requirements", limit=5) +results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python -content = get_document("Q1 Report") -sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") +content = await get_document("Q1 Report") +sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py b/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py index 72984c79..cdd7be3f 100644 --- a/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py +++ b/haiku_rag_slim/haiku/rag/agents/rlm/sandbox.py @@ -1,7 +1,4 @@ -import asyncio -from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass -from functools import partial from typing import TYPE_CHECKING, Any, Literal import pydantic_monty @@ -27,12 +24,10 @@ class Sandbox: Uses pydantic-monty, a minimal secure Python interpreter written in Rust. External functions (search, list_documents, etc.) are called by Monty code - and resolved asynchronously on the host. + using ``await`` and resolved asynchronously on the host. - Use as an async context manager: - - async with Sandbox(client, config, context) as sandbox: - result = await sandbox.execute("print('hello')") + sandbox = Sandbox(client, config, context) + result = await sandbox.execute("print('hello')") """ _client: "HaikuRAG" @@ -49,14 +44,6 @@ class Sandbox: self._config = config self._context = context - async def __aenter__(self) -> "Sandbox": - return self - - async def __aexit__( - self, exc_type: object, exc_val: object, exc_tb: object - ) -> None: - pass - def _build_external_functions(self) -> dict[str, Any]: """Build async external functions for the Monty interpreter.""" client = self._client @@ -138,12 +125,7 @@ class Sandbox: } async def execute(self, code: str) -> SandboxResult: - """Execute Python code in the Monty interpreter. - - Uses a manual start/resume loop so that async external functions - are awaited on the host while Monty code calls them synchronously - (without ``await``). - """ + """Execute Python code in the Monty interpreter.""" external_fns = self._build_external_functions() input_names: list[str] = [] @@ -181,45 +163,14 @@ class Sandbox: "max_duration_secs": self._config.rlm.code_timeout, } - loop = asyncio.get_running_loop() - try: - with ThreadPoolExecutor() as pool: - - async def run_in_pool(func: Any) -> Any: - return await loop.run_in_executor(pool, func) - - progress = await run_in_pool( - partial( - monty.start, - inputs=inputs, - limits=limits, - print_callback=print_callback, - ) - ) - - while not isinstance(progress, pydantic_monty.MontyComplete): - assert isinstance(progress, pydantic_monty.MontySnapshot) - fn = external_fns.get(progress.function_name) - if fn is None: - exc = KeyError(f"Function {progress.function_name} not found") - progress = await run_in_pool( - partial(progress.resume, exception=exc) - ) - continue - - try: - result = await fn(*progress.args, **progress.kwargs) - except Exception as exc: - progress = await run_in_pool( - partial(progress.resume, exception=exc) - ) - else: - progress = await run_in_pool( - partial(progress.resume, return_value=result) - ) - - output = progress.output + output = await pydantic_monty.run_monty_async( + monty, + inputs=inputs, + external_functions=external_fns, + limits=limits, + print_callback=print_callback, + ) except pydantic_monty.MontyRuntimeError as e: stdout = "".join(stdout_lines) if len(stdout) > max_chars: diff --git a/haiku_rag_slim/haiku/rag/client.py b/haiku_rag_slim/haiku/rag/client.py index 3f828544..9c94a3d3 100644 --- a/haiku_rag_slim/haiku/rag/client.py +++ b/haiku_rag_slim/haiku/rag/client.py @@ -1406,20 +1406,20 @@ class HaikuRAG: loaded_docs.append(doc) context.documents = loaded_docs if loaded_docs else None - async with Sandbox( + sandbox = Sandbox( client=self, config=self._config, context=context, - ) as sandbox: - deps = RLMDeps( - sandbox=sandbox, - context=context, - ) + ) + deps = RLMDeps( + sandbox=sandbox, + context=context, + ) - agent = create_rlm_agent(self._config) - result = await agent.run(question, deps=deps) + agent = create_rlm_agent(self._config) + result = await agent.run(question, deps=deps) - return result.output + return result.output async def visualize_chunk(self, chunk: Chunk) -> list: """Render page images with bounding box highlights for a chunk. diff --git a/haiku_rag_slim/haiku/rag/skills/rag-rlm/SKILL.md b/haiku_rag_slim/haiku/rag/skills/rag-rlm/SKILL.md index 93253102..f59a148f 100644 --- a/haiku_rag_slim/haiku/rag/skills/rag-rlm/SKILL.md +++ b/haiku_rag_slim/haiku/rag/skills/rag-rlm/SKILL.md @@ -1,7 +1,7 @@ --- name: rag-rlm description: > - Computational analysis of the knowledge base via code execution in a Docker sandbox. + Computational analysis of the knowledge base via code execution in a sandboxed Python interpreter. Use for questions requiring counting, aggregation, statistics, data traversal, comparison across documents, or any task best answered by writing Python code. Examples: "how many pages?", "compare table 3 across documents", @@ -10,4 +10,4 @@ description: > # RLM Analysis -Use the `analyze` tool for complex analytical questions. It writes and executes Python code against the knowledge base in an isolated Docker sandbox. +Use the `analyze` tool for complex analytical questions. It writes and executes Python code against the knowledge base in a sandboxed Python interpreter. diff --git a/haiku_rag_slim/haiku/rag/tools/analysis.py b/haiku_rag_slim/haiku/rag/tools/analysis.py index 57576547..fd9b54a5 100644 --- a/haiku_rag_slim/haiku/rag/tools/analysis.py +++ b/haiku_rag_slim/haiku/rag/tools/analysis.py @@ -62,25 +62,25 @@ def create_analysis_toolset( rlm_context = RLMContext(filter=effective_filter) - async with Sandbox( + sandbox = Sandbox( client=client, config=config, context=rlm_context, - ) as sandbox: - deps = RLMDeps( - sandbox=sandbox, - context=rlm_context, - ) + ) + deps = RLMDeps( + sandbox=sandbox, + context=rlm_context, + ) - rlm_agent = create_rlm_agent(config) - result = await rlm_agent.run(task, deps=deps) + rlm_agent = create_rlm_agent(config) + result = await rlm_agent.run(task, deps=deps) - program = result.output.program + program = result.output.program - return AnalysisResult( - answer=result.output.answer, - code_executed=bool(program), - ) + return AnalysisResult( + answer=result.output.answer, + code_executed=bool(program), + ) toolset: FunctionToolset[RAGDeps] = FunctionToolset() toolset.add_function(analyze, name=tool_name) diff --git a/tests/agents/rlm/conftest.py b/tests/agents/rlm/conftest.py index b641a030..b1df0f4c 100644 --- a/tests/agents/rlm/conftest.py +++ b/tests/agents/rlm/conftest.py @@ -18,5 +18,4 @@ async def sandbox(empty_client): """Create a Monty sandbox for testing.""" config = AppConfig() context = RLMContext() - async with Sandbox(client=empty_client, config=config, context=context) as sandbox: - yield sandbox + return Sandbox(client=empty_client, config=config, context=context) diff --git a/tests/agents/rlm/test_sandbox.py b/tests/agents/rlm/test_sandbox.py index 2b4f622d..5a9fb71b 100644 --- a/tests/agents/rlm/test_sandbox.py +++ b/tests/agents/rlm/test_sandbox.py @@ -74,7 +74,7 @@ class TestSandboxHaikuRAG: async def test_list_documents_empty(self, sandbox): """Test list_documents returns empty list for empty database.""" result = await sandbox.execute( - "docs = list_documents()\nprint(type(docs).__name__, len(docs))" + "docs = await list_documents()\nprint(type(docs).__name__, len(docs))" ) assert result.success assert "list 0" in result.stdout @@ -92,13 +92,15 @@ class TestSandboxHaikuRAG: ) context = RLMContext() - async with Sandbox(client=client, config=config, context=context) as sb: - result = await sb.execute( - "docs = list_documents()\nprint(len(docs))\nprint(docs[0]['title'])" - ) - assert result.success - assert "1" in result.stdout - assert "Test Document" in result.stdout + sb = Sandbox(client=client, config=config, context=context) + result = await sb.execute( + "docs = await list_documents()\n" + "print(len(docs))\n" + "print(docs[0]['title'])" + ) + assert result.success + assert "1" in result.stdout + assert "Test Document" in result.stdout @pytest.mark.asyncio @pytest.mark.vcr() @@ -113,15 +115,15 @@ class TestSandboxHaikuRAG: ) context = RLMContext() - async with Sandbox(client=client, config=config, context=context) as sb: - result = await sb.execute( - "results = search('fox', limit=5)\n" - "print(len(results))\n" - "if results:\n" - " print('fox' in results[0]['content'].lower())" - ) - assert result.success - assert "True" in result.stdout or "1" in result.stdout + sb = Sandbox(client=client, config=config, context=context) + result = await sb.execute( + "results = await search('fox', limit=5)\n" + "print(len(results))\n" + "if results:\n" + " print('fox' in results[0]['content'].lower())" + ) + assert result.success + assert "True" in result.stdout or "1" in result.stdout @pytest.mark.asyncio @pytest.mark.vcr() @@ -136,19 +138,19 @@ class TestSandboxHaikuRAG: ) context = RLMContext() - async with Sandbox(client=client, config=config, context=context) as sb: - result = await sb.execute( - f"content = get_document('{doc.id}')\n" - "print('foxes' in content.lower() if content else 'None')" - ) - assert result.success - assert "True" in result.stdout + sb = Sandbox(client=client, config=config, context=context) + result = await sb.execute( + f"content = await get_document('{doc.id}')\n" + "print('foxes' in content.lower() if content else 'None')" + ) + assert result.success + assert "True" in result.stdout @pytest.mark.asyncio async def test_get_document_not_found(self, sandbox): """Test get_document returns None for missing document.""" result = await sandbox.execute( - "content = get_document('nonexistent-id')\nprint(content is None)" + "content = await get_document('nonexistent-id')\nprint(content is None)" ) assert result.success assert "True" in result.stdout @@ -166,24 +168,24 @@ class TestSandboxHaikuRAG: ) context = RLMContext() - async with Sandbox(client=client, config=config, context=context) as sb: - # First search to get a chunk_id - result = await sb.execute( - "results = search('foxes', limit=1)\n" - "chunk_id = results[0]['chunk_id']\n" - "chunk = get_chunk(chunk_id)\n" - "print(chunk['document_title'])\n" - "print('content' in chunk)" - ) - assert result.success - assert "Fox Document" in result.stdout - assert "True" in result.stdout + sb = Sandbox(client=client, config=config, context=context) + # First search to get a chunk_id + result = await sb.execute( + "results = await search('foxes', limit=1)\n" + "chunk_id = results[0]['chunk_id']\n" + "chunk = await get_chunk(chunk_id)\n" + "print(chunk['document_title'])\n" + "print('content' in chunk)" + ) + assert result.success + assert "Fox Document" in result.stdout + assert "True" in result.stdout @pytest.mark.asyncio async def test_get_chunk_not_found(self, sandbox): """Test get_chunk returns None for missing chunk.""" result = await sandbox.execute( - "chunk = get_chunk('nonexistent-id')\nprint(chunk is None)" + "chunk = await get_chunk('nonexistent-id')\nprint(chunk is None)" ) assert result.success assert "True" in result.stdout @@ -205,7 +207,11 @@ class TestSandboxExternalFunctionEdgeCases: sandbox._build_external_functions = patched_build result = await sandbox.execute( - "try:\n search('hello')\nexcept:\n print('caught')\nprint('done')" + "try:\n" + " await search('hello')\n" + "except:\n" + " print('caught')\n" + "print('done')" ) assert result.success assert "caught" in result.stdout @@ -213,7 +219,12 @@ class TestSandboxExternalFunctionEdgeCases: @pytest.mark.asyncio async def test_external_function_raises_exception(self, sandbox): - """Test that exceptions from external functions are propagated to Monty.""" + """Test that exceptions from async external functions surface as errors. + + With run_monty_async, exceptions from async external functions + propagate as MontyRuntimeError rather than being catchable inside + Monty's try/except. + """ original_build = sandbox._build_external_functions def patched_build(): @@ -227,12 +238,9 @@ class TestSandboxExternalFunctionEdgeCases: sandbox._build_external_functions = patched_build - result = await sandbox.execute( - "try:\n search('hello')\nexcept:\n print('caught')\nprint('done')" - ) - assert result.success - assert "caught" in result.stdout - assert "done" in result.stdout + result = await sandbox.execute("await search('hello')") + assert not result.success + assert "external error" in result.stderr class TestSandboxOutputTruncation: @@ -244,12 +252,12 @@ class TestSandboxOutputTruncation: config = AppConfig() config.rlm.max_output_chars = 20 context = RLMContext() - async with Sandbox(client=empty_client, config=config, context=context) as sb: - result = await sb.execute("print('a' * 100)\nx = 1/0") - assert not result.success - assert "ZeroDivisionError" in result.stderr - assert result.stdout.endswith("... (output truncated)") - assert len(result.stdout) < 100 + sb = Sandbox(client=empty_client, config=config, context=context) + result = await sb.execute("print('a' * 100)\nx = 1/0") + assert not result.success + assert "ZeroDivisionError" in result.stderr + assert result.stdout.endswith("... (output truncated)") + assert len(result.stdout) < 100 @pytest.mark.asyncio async def test_truncate_successful_output(self, empty_client): @@ -257,11 +265,11 @@ class TestSandboxOutputTruncation: config = AppConfig() config.rlm.max_output_chars = 20 context = RLMContext() - async with Sandbox(client=empty_client, config=config, context=context) as sb: - result = await sb.execute("print('b' * 100)") - assert result.success - assert result.stdout.endswith("... (output truncated)") - assert len(result.stdout) < 100 + sb = Sandbox(client=empty_client, config=config, context=context) + result = await sb.execute("print('b' * 100)") + assert result.success + assert result.stdout.endswith("... (output truncated)") + assert len(result.stdout) < 100 class TestSandboxContextFilter: @@ -285,17 +293,17 @@ class TestSandboxContextFilter: ) context = RLMContext(filter="uri LIKE 'public://%'") - async with Sandbox(client=client, config=config, context=context) as sb: - result = await sb.execute( - "docs = list_documents()\n" - "print(len(docs))\n" - "if docs:\n" - " print(docs[0]['title'])" - ) - assert result.success - assert "1" in result.stdout - assert "Public Doc" in result.stdout - assert "Private Doc" not in result.stdout + sb = Sandbox(client=client, config=config, context=context) + result = await sb.execute( + "docs = await list_documents()\n" + "print(len(docs))\n" + "if docs:\n" + " print(docs[0]['title'])" + ) + assert result.success + assert "1" in result.stdout + assert "Public Doc" in result.stdout + assert "Private Doc" not in result.stdout class TestSandboxPreloadedDocuments: @@ -317,16 +325,16 @@ class TestSandboxPreloadedDocuments: Document(id="2", content="Content B", title="Doc B", uri="b://2"), ] context = RLMContext(documents=docs) - async with Sandbox(client=empty_client, config=config, context=context) as sb: - 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 + sb = Sandbox(client=empty_client, 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 class TestSandboxLLM: @@ -338,10 +346,10 @@ class TestSandboxLLM: """Test llm() calls the model and returns a string.""" config = AppConfig() context = RLMContext() - async with Sandbox(client=empty_client, config=config, context=context) as sb: - result = await sb.execute( - "answer = llm('What is 2 + 2? Reply with just the number.')\n" - "print(answer)" - ) - assert result.success - assert "4" in result.stdout + sb = Sandbox(client=empty_client, 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/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_aggregation.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_aggregation.yaml index 48e578d9..f2b1b2ce 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_aggregation.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_aggregation.yaml @@ -128,7 +128,7 @@ interactions: connection: - keep-alive content-length: - - '7099' + - '7341' content-type: - application/json host: @@ -141,32 +141,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -183,7 +184,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -192,21 +193,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -216,9 +217,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -229,16 +230,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -314,7 +315,7 @@ interactions: response: headers: content-length: - - '744' + - '809' content-type: - application/json parsed_body: @@ -323,26 +324,27 @@ interactions: index: 0 message: content: '' - reasoning: Need to search for quarterly reports. Likely files like "Q1 Report", "Quarterly Report". Search for "quarterly - report revenue". + reasoning: We need to find quarterly reports documents and extract revenue figures. Likely documents include Q1, + Q2, etc. Search for "quarterly report revenue". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor r in - results:\n print(r[''document_title''], r[''chunk_id''], r[''content''][:200])\n"}' + arguments: '{"code":"# Search for quarterly report revenue\nresults = await search(\"quarterly report revenue\", + limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], r[''page_numbers''], + r[''score''])\n"}' name: execute_code - id: call_djn3wl0z + id: call_00l27zrg index: 0 type: function - created: 1771336707 - id: chatcmpl-891 + created: 1771924503 + id: chatcmpl-643 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 99 - prompt_tokens: 1564 - total_tokens: 1663 + completion_tokens: 110 + prompt_tokens: 1625 + total_tokens: 1735 status: code: 200 message: OK @@ -395,7 +397,7 @@ interactions: connection: - keep-alive content-length: - - '8142' + - '8336' content-type: - application/json host: @@ -408,32 +410,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -450,7 +453,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -459,21 +462,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -483,9 +486,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -496,16 +499,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -532,22 +535,22 @@ interactions: - content: What is the total revenue across all quarterly reports? role: user - content: null - reasoning: Need to search for quarterly reports. Likely files like "Q1 Report", "Quarterly Report". Search for "quarterly - report revenue". + reasoning: We need to find quarterly reports documents and extract revenue figures. Likely documents include Q1, Q2, + etc. Search for "quarterly report revenue". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''content''][:200])\n"}' + arguments: '{"code":"# Search for quarterly report revenue\nresults = await search(\"quarterly report revenue\", + limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], r[''page_numbers''], + r[''score''])\n"}' name: execute_code - id: call_djn3wl0z + id: call_00l27zrg type: function - - content: '{"code":"results = search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''content''][:200])\n","stdout":"3\nQ1 Report 3b154808-f8fa-4322-ad0c-4d462b54f59a Sales report - Q1: Revenue was $100,000.\nQ2 Report debfe200-93d6-4c8d-beed-5e244b0474dd Sales report Q2: Revenue was $150,000.\nQ3 - Report 9b7ef62a-b01e-48aa-92ca-927284307fcf Sales report Q3: Revenue was $200,000.\n","stderr":"","success":true}' + - content: '{"code":"# Search for quarterly report revenue\nresults = await search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor + r in results[:5]:\n print(r[''document_title''], r[''page_numbers''], r[''score''])\n","stdout":"3\nQ1 Report + [] 0.03226645663380623\nQ2 Report [] 0.03226645663380623\nQ3 Report [] 0.032258063554763794\n","stderr":"","success":true}' role: tool - tool_call_id: call_djn3wl0z + tool_call_id: call_00l27zrg model: gpt-oss reasoning_effort: low stream: false @@ -598,7 +601,7 @@ interactions: response: headers: content-length: - - '612' + - '567' content-type: - application/json parsed_body: @@ -607,24 +610,23 @@ interactions: index: 0 message: content: '' - reasoning: We need total revenue across all quarterly reports. There are Q1, Q2, Q3. Probably Q4 as well. Search - for Q4. + reasoning: Found Q1, Q2, Q3. Probably Q4 too. Search for Q4. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"Q4 Report\", limit=10)\nprint(results)\n"}' + arguments: '{"code":"results4 = await search(\"Q4 Report revenue\", limit=5)\nprint(results4)\n"}' name: execute_code - id: call_ls6idggl + id: call_s2cf4xgs index: 0 type: function - created: 1771336709 - id: chatcmpl-119 + created: 1771924505 + id: chatcmpl-466 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 71 - prompt_tokens: 1851 + completion_tokens: 64 + prompt_tokens: 1858 total_tokens: 1922 status: code: 200 @@ -638,7 +640,7 @@ interactions: connection: - keep-alive content-length: - - '79' + - '87' content-type: - application/json host: @@ -647,7 +649,7 @@ interactions: parsed_body: encoding_format: base64 input: - - Q4 Report + - Q4 Report revenue model: qwen3-embedding:4b uri: http://localhost:11434/v1/embeddings response: @@ -658,14 +660,14 @@ interactions: - chunked parsed_body: data: - - embedding: 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 + - embedding: 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 index: 0 object: embedding model: qwen3-embedding:4b object: list usage: - prompt_tokens: 4 - total_tokens: 4 + prompt_tokens: 5 + total_tokens: 5 status: code: 200 message: OK @@ -678,7 +680,7 @@ interactions: connection: - keep-alive content-length: - - '9530' + - '9694' content-type: - application/json host: @@ -691,32 +693,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -733,7 +736,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -742,21 +745,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -766,9 +769,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -779,16 +782,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -815,43 +818,42 @@ interactions: - content: What is the total revenue across all quarterly reports? role: user - content: null - reasoning: Need to search for quarterly reports. Likely files like "Q1 Report", "Quarterly Report". Search for "quarterly - report revenue". + reasoning: We need to find quarterly reports documents and extract revenue figures. Likely documents include Q1, Q2, + etc. Search for "quarterly report revenue". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''content''][:200])\n"}' + arguments: '{"code":"# Search for quarterly report revenue\nresults = await search(\"quarterly report revenue\", + limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], r[''page_numbers''], + r[''score''])\n"}' name: execute_code - id: call_djn3wl0z + id: call_00l27zrg type: function - - content: '{"code":"results = search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''content''][:200])\n","stdout":"3\nQ1 Report 3b154808-f8fa-4322-ad0c-4d462b54f59a Sales report - Q1: Revenue was $100,000.\nQ2 Report debfe200-93d6-4c8d-beed-5e244b0474dd Sales report Q2: Revenue was $150,000.\nQ3 - Report 9b7ef62a-b01e-48aa-92ca-927284307fcf Sales report Q3: Revenue was $200,000.\n","stderr":"","success":true}' + - content: '{"code":"# Search for quarterly report revenue\nresults = await search(\"quarterly report revenue\", limit=20)\nprint(len(results))\nfor + r in results[:5]:\n print(r[''document_title''], r[''page_numbers''], r[''score''])\n","stdout":"3\nQ1 Report + [] 0.03226645663380623\nQ2 Report [] 0.03226645663380623\nQ3 Report [] 0.032258063554763794\n","stderr":"","success":true}' role: tool - tool_call_id: call_djn3wl0z + tool_call_id: call_00l27zrg - content: null - reasoning: We need total revenue across all quarterly reports. There are Q1, Q2, Q3. Probably Q4 as well. Search for - Q4. + reasoning: Found Q1, Q2, Q3. Probably Q4 too. Search for Q4. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"Q4 Report\", limit=10)\nprint(results)\n"}' + arguments: '{"code":"results4 = await search(\"Q4 Report revenue\", limit=5)\nprint(results4)\n"}' name: execute_code - id: call_ls6idggl + id: call_s2cf4xgs type: function - - content: '{"code":"results = search(\"Q4 Report\", limit=10)\nprint(results)\n","stdout":"[{''chunk_id'': ''3b154808-f8fa-4322-ad0c-4d462b54f59a'', - ''content'': ''Sales report Q1: Revenue was $100,000.'', ''document_id'': ''23ce9e44-5cd4-4e67-99d7-41bc69250ea8'', - ''document_title'': ''Q1 Report'', ''document_uri'': None, ''score'': 0.03226645663380623, ''page_numbers'': [], - ''headings'': None}, {''chunk_id'': ''debfe200-93d6-4c8d-beed-5e244b0474dd'', ''content'': ''Sales report Q2: Revenue - was $150,000.'', ''document_id'': ''a1856b41-de85-41af-9515-ddfa4ded5682'', ''document_title'': ''Q2 Report'', ''document_uri'': - None, ''score'': 0.03226645663380623, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''9b7ef62a-b01e-48aa-92ca-927284307fcf'', - ''content'': ''Sales report Q3: Revenue was $200,000.'', ''document_id'': ''6ef871e0-c379-4af0-8b72-ef617ade0dea'', - ''document_title'': ''Q3 Report'', ''document_uri'': None, ''score'': 0.032258063554763794, ''page_numbers'': [], - ''headings'': None}]\n","stderr":"","success":true}' + - content: '{"code":"results4 = await search(\"Q4 Report revenue\", limit=5)\nprint(results4)\n","stdout":"[{''chunk_id'': + ''fb347b1c-9bf0-455b-8535-3484f9578731'', ''content'': ''Sales report Q1: Revenue was $100,000.'', ''document_id'': + ''7c76fa0e-4d74-49d1-88a6-908391e63fe5'', ''document_title'': ''Q1 Report'', ''document_uri'': None, ''score'': + 0.03226645663380623, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''50f60a6f-53e4-4850-8d5b-a3c00d4375c6'', + ''content'': ''Sales report Q2: Revenue was $150,000.'', ''document_id'': ''792e21c1-2736-4fa8-ba8e-0629b1cb5a2f'', + ''document_title'': ''Q2 Report'', ''document_uri'': None, ''score'': 0.03226645663380623, ''page_numbers'': [], + ''headings'': None}, {''chunk_id'': ''70233263-be1a-414e-8841-c9509076530e'', ''content'': ''Sales report Q3: Revenue + was $200,000.'', ''document_id'': ''ab42afcc-0fe0-4fdc-8c26-656d747c950e'', ''document_title'': ''Q3 Report'', ''document_uri'': + None, ''score'': 0.032258063554763794, ''page_numbers'': [], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_ls6idggl + tool_call_id: call_s2cf4xgs model: gpt-oss reasoning_effort: low stream: false @@ -902,7 +904,7 @@ interactions: response: headers: content-length: - - '1186' + - '1421' content-type: - application/json parsed_body: @@ -910,24 +912,27 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"The total revenue reported across all quarterly reports is **$450,000**.\n\nBreakdown:\n- Q1 - Report: $100,000\n- Q2 Report: $150,000\n- Q3 Report: $200,000\n\n(Quarter 4 is not present in the current knowledge - base.)","program":"# Calculate total revenue across all quarterly reports\nresults = search(\"quarterly report - revenue\", limit=20)\nrevenues = []\nfor r in results:\n # Extract numeric amount from the content string\n amount_str - = llm(f\"Extract the dollar amount from this text (no $ sign or commas): {r[''content'']}\")\n # Clean and - convert to integer\n amount = int(amount_str.strip().replace('','', ''''))\n revenues.append(amount)\n# - Compute total\ntotal = sum(revenues)\nprint(f\"Total revenue: ${total:,}\")"}' - reasoning: No Q4 in search; maybe none. So total revenue = 100k+150k+200k=450k. Provide answer and program. + content: '{"answer":"The summed revenue reported across all available quarterly reports (Q1, Q2, and Q3) is **$450,000**. + The figures are:\n- **Q1 Report** – $100,000 (citation: chunk fb347b1c‑9bf0‑455b‑8535‑3484f9578731)\n- **Q2 Report** + – $150,000 (citation: chunk 50f60a6f‑53e4‑4850‑8d5b‑a3c00d4375c6)\n- **Q3 Report** – $200,000 (citation: chunk + 70233263‑be1a‑414e‑8841‑c9509076530e)\n\nNo Q4 report was found in the knowledge base. The total is therefore + 100 k + 150 k + 200 k = 450 k dollars.", "program":"# Calculate total revenue from quarterly reports\nresults + = await search(\"sales report revenue\", limit=10)\nrevenue = 0\nfor r in results:\n # Use llm to extract dollar + amount\n amount_text = await llm(f\"Extract the dollar amount from this snippet: {r[''content'']}\")\n # + Clean and convert\n amt = int(''''.join([c for c in amount_text if c.isdigit()]))\n revenue += amt\nprint(f\"Total + revenue: ${revenue}\")"}' + reasoning: Results show Q1 100k, Q2 150k, Q3 200k. No Q4? Maybe not included. Total revenue = 450k. Provide citations + from chunks. role: assistant - created: 1771336715 - id: chatcmpl-973 + created: 1771924512 + id: chatcmpl-210 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 243 - prompt_tokens: 2296 - total_tokens: 2539 + completion_tokens: 367 + prompt_tokens: 2311 + total_tokens: 2678 status: code: 200 message: OK diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_count_documents.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_count_documents.yaml index 2bca76ac..ae352e94 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_count_documents.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_count_documents.yaml @@ -128,7 +128,7 @@ interactions: connection: - keep-alive content-length: - - '7083' + - '7325' content-type: - application/json host: @@ -141,32 +141,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -183,7 +184,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -192,21 +193,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -216,9 +217,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -229,16 +230,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -314,7 +315,7 @@ interactions: response: headers: content-length: - - '552' + - '514' content-type: - application/json parsed_body: @@ -323,24 +324,24 @@ interactions: index: 0 message: content: '' - reasoning: Need to list documents. + reasoning: Need to list docs. role: assistant tool_calls: - function: - arguments: '{"code":"docs=list_documents(limit=1000)\nprint(len(docs))\nfor d in docs:\n print(d[''title''])"}' + arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' name: execute_code - id: call_tumky965 + id: call_stp0fimx index: 0 type: function - created: 1771336699 - id: chatcmpl-910 + created: 1771924497 + id: chatcmpl-750 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 56 - prompt_tokens: 1562 - total_tokens: 1618 + completion_tokens: 44 + prompt_tokens: 1623 + total_tokens: 1667 status: code: 200 message: OK @@ -353,7 +354,7 @@ interactions: connection: - keep-alive content-length: - - '7612' + - '7759' content-type: - application/json host: @@ -366,32 +367,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -408,7 +410,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -417,21 +419,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -441,9 +443,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -454,16 +456,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -490,18 +492,17 @@ interactions: - content: How many documents are in the database? role: user - content: null - reasoning: Need to list documents. + reasoning: Need to list docs. role: assistant tool_calls: - function: - arguments: '{"code":"docs=list_documents(limit=1000)\nprint(len(docs))\nfor d in docs:\n print(d[''title''])"}' + arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' name: execute_code - id: call_tumky965 + id: call_stp0fimx type: function - - content: '{"code":"docs=list_documents(limit=1000)\nprint(len(docs))\nfor d in docs:\n print(d[''title''])","stdout":"3\nDoc - 1\nDoc 2\nDoc 3\n","stderr":"","success":true}' + - content: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_tumky965 + tool_call_id: call_stp0fimx model: gpt-oss reasoning_effort: low stream: false @@ -552,7 +553,7 @@ interactions: response: headers: content-length: - - '517' + - '416' content-type: - application/json parsed_body: @@ -560,18 +561,17 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"There are 3 documents in the database.","program":"docs = list_documents(limit=1000)\nprint(f\"Number - of documents: {len(docs)}\")\nfor doc in docs:\n print(f\"- {doc[''title'']} (ID: {doc[''id'']})\")"}' + content: '{"answer":"There are 3 documents in the database.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' role: assistant - created: 1771336701 - id: chatcmpl-57 + created: 1771924498 + id: chatcmpl-945 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 82 - prompt_tokens: 1684 - total_tokens: 1766 + completion_tokens: 38 + prompt_tokens: 1709 + total_tokens: 1747 status: code: 200 message: OK diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_extract.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_extract.yaml index 05633044..04962a11 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_extract.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_extract.yaml @@ -182,7 +182,7 @@ interactions: connection: - keep-alive content-length: - - '7178' + - '7420' content-type: - application/json host: @@ -195,32 +195,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -237,7 +238,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -246,21 +247,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -270,9 +271,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -283,16 +284,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -369,7 +370,7 @@ interactions: response: headers: content-length: - - '601' + - '683' content-type: - application/json parsed_body: @@ -378,25 +379,25 @@ interactions: index: 0 message: content: '' - reasoning: Need to search. + reasoning: Need to search for document element types or labels. Use search. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"document element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n"}' + arguments: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor + r in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''content''][:200])\n"}' name: execute_code - id: call_a5m383mw + id: call_j7uw3i6n index: 0 type: function - created: 1771336770 - id: chatcmpl-12 + created: 1771924566 + id: chatcmpl-52 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 67 - prompt_tokens: 1578 - total_tokens: 1645 + completion_tokens: 87 + prompt_tokens: 1639 + total_tokens: 1726 status: code: 200 message: OK @@ -449,7 +450,7 @@ interactions: connection: - keep-alive content-length: - - '11962' + - '9384' content-type: - application/json host: @@ -462,32 +463,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -504,7 +506,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -513,21 +515,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -537,9 +539,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -550,16 +552,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -587,53 +589,29 @@ interactions: mentioned? List them all. role: user - content: null - reasoning: Need to search. + reasoning: Need to search for document element types or labels. Use search. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"document element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n"}' + arguments: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r + in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''content''][:200])\n"}' name: execute_code - id: call_a5m383mw + id: call_j7uw3i6n type: function - - content: '{"code":"results = search(\"document element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n","stdout":"baf79253-e710-41e4-9afc-cd53a58b14e6 None Phase 2: Label - selection and guideline. We reviewed the collected documents and identified the most common structural features - they exhibit. This was achieved by identifying recurrent layout elements \n6377d621-2597-4bda-ba58-11dc72b87477 + - content: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''chunk_id''], + r[''document_title''], r[''content''][:200])\n","stdout":"17\n7c3a912c-a769-49e9-a661-402d7d64ec9f None Phase 2: + Label selection and guideline. We reviewed the collected documents and identified the most common structural features + they exhibit. This was achieved by identifying recurrent layout elements \n62e98936-a694-4cb0-a1a4-409b0121e08d None Phase 1: Data selection and preparation. Our inclusion criteria for documents were described in Section 3. - A large effort went into ensuring that all documents are free to use. The data sources includ\n1cbffceb-fa16-434f-a168-54ad860b1c21 + A large effort went into ensuring that all documents are free to use. The data sources includ\nd583f0ed-c230-4f8e-988a-86875c0c633d None $_{Affiliation}$, as seen in DocBank, are often only distinguishable by discriminating on\nPreparation work - included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n44a5a4d6-7f42-46d2-9115-4dda6b6d62ee + included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n180df00a-2a5b-4e2a-bfcc-4cd67fbc9db0 None \nTable 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative - occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\nfe743e25-0740-423c-9f26-d42e658269f2 + occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\nf3c18411-85df-4c72-a784-f39f602745ec None Page-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 96-98. Page-header, Count = 58022. Page-header, - % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\ned730761-c984-419a-9fdd-06fd37aa632a - None 0.5-0.95 (%).Law = 87-94. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-73. Section-header, - triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 78-86. Table, Count = 34733. Table, % o\na669db88-1bd8-46f3-87ec-078b74151258 - None Caption, Count = 22524. Caption, % of Total.Train = 2.04. Caption, % of Total.Test = 1.77. Caption, % of Total.Val - = 2.32. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).All = 84-89. Caption, trip\n38cf7b9f-3947-45f2-a66a-5ef4d89a88c2 - None inter-annotator mAP @ 0.5-0.95 (%).Pat = 66-71. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 59-76. - Section-header, Count = 142884. Section-header, % of Total.Train = 12.60. Section-header\n26781f97-2e94-4270-85e7-84670a7971f2 - None n/a. Footnote, Count = 6318. Footnote, % of Total.Train = 0.60. Footnote, % of Total.Test = 0.31. Footnote, - % of Total.Val = 0.58. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-91. Foot\n1eb6520a-3d81-4113-a8b1-4a045bdd581d - None 185660. List-item, % of Total.Train = 17.19. List-item, % of Total.Test = 13.34. List-item, % of Total.Val - = 15.82. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).All = 87-88. List-item, triple \nec1f7ead-7b8c-46f7-926b-f0d6c942ad02 - None 0.5-0.95 (%).Ten = 93-95. Page-footer, Count = 70878. Page-footer, % of Total.Train = 6.51. Page-footer, % - of Total.Test = 5.58. Page-footer, % of Total.Val = 6.00. Page-footer, triple inter-annotator\n8e924800-19a6-4096-8e36-c211ac858ffa - None 89-93. Text, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 87-92. Text, triple inter-annotator mAP @ 0.5-0.95 - (%).Pat = 71-79. Text, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 87-95. Title, Cou\na36c2e35-4c45-462f-8997-d800667e2c5c - None 0.5-0.95 (%).Pat = 97-99. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 81-86. Picture, Count - = 45976. Picture, % of Total.Train = 4.21. Picture, % of Total.Test = 2.78. Picture, % of T\nd0d1d4b6-bd81-42f4-a9d1-443f8869defd - None (%).Sci = 94-100. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 82-96. Title, triple inter-annotator - mAP @ 0.5-0.95 (%).Pat = 68-79. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 24-\n3cdae3b8-d62e-497a-908b-dac06a9b87a8 - None inter-annotator mAP @ 0.5-0.95 (%).Law = 58-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 79-84. - Table, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 70-85. Text, Count = 510377. Text, \n525a5cca-93ee-45be-8d67-f3e54286a80f - None = 82-97. Formula, Count = 25027. Formula, % of Total.Train = 2.25. Formula, % of Total.Test = 1.90. Formula, - % of Total.Val = 2.96. Formula, triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-85. Form\n5667fabf-8421-4004-bccf-9ce1b100f49a - None mAP @ 0.5-0.95 (%).Sci = 89-94. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 86-91. Total, triple - inter-annotator mAP @ 0.5-0.95 (%).Pat = 71-76. Total, triple inter-annotator mAP @ 0.5-0.95\n","stderr":"","success":true}' + % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\n","stderr":"","success":true}' role: tool - tool_call_id: call_a5m383mw + tool_call_id: call_j7uw3i6n model: gpt-oss reasoning_effort: low stream: false @@ -684,7 +662,7 @@ interactions: response: headers: content-length: - - '817' + - '891' content-type: - application/json parsed_body: @@ -693,27 +671,27 @@ interactions: index: 0 message: content: '' - reasoning: 'We need list of element types. From snippets: Page-footer, Page-header, Section-header, Table, Caption, - Picture, Footnote, List-item, Text, Title, Formula. Also maybe others like List, Equation? Let''s search for "element - types" keyword.' + reasoning: We need list all document element types or labels mentioned. Likely these are labels in documents like + "Page-header", "Page-footer", etc. Let's search more. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n"}' + arguments: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look + for uppercase words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", + r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n"}' name: execute_code - id: call_5sejw3xj + id: call_gzg850pl index: 0 type: function - created: 1771336775 - id: chatcmpl-749 + created: 1771924570 + id: chatcmpl-449 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 116 - prompt_tokens: 3346 - total_tokens: 3462 + completion_tokens: 142 + prompt_tokens: 2158 + total_tokens: 2300 status: code: 200 message: OK @@ -726,7 +704,7 @@ interactions: connection: - keep-alive content-length: - - '83' + - '75' content-type: - application/json host: @@ -735,7 +713,7 @@ interactions: parsed_body: encoding_format: base64 input: - - element types + - label model: qwen3-embedding:4b uri: http://localhost:11434/v1/embeddings response: @@ -746,14 +724,14 @@ interactions: - chunked parsed_body: data: - - embedding: 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 + - embedding: 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 index: 0 object: embedding model: qwen3-embedding:4b object: list usage: - prompt_tokens: 3 - total_tokens: 3 + prompt_tokens: 2 + total_tokens: 2 status: code: 200 message: OK @@ -766,7 +744,7 @@ interactions: connection: - keep-alive content-length: - - '16951' + - '10466' content-type: - application/json host: @@ -779,32 +757,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -821,7 +800,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -830,21 +809,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -854,9 +833,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -867,16 +846,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -904,103 +883,46 @@ interactions: mentioned? List them all. role: user - content: null - reasoning: Need to search. + reasoning: Need to search for document element types or labels. Use search. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"document element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n"}' + arguments: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r + in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''content''][:200])\n"}' name: execute_code - id: call_a5m383mw + id: call_j7uw3i6n type: function - - content: '{"code":"results = search(\"document element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n","stdout":"baf79253-e710-41e4-9afc-cd53a58b14e6 None Phase 2: Label - selection and guideline. We reviewed the collected documents and identified the most common structural features - they exhibit. This was achieved by identifying recurrent layout elements \n6377d621-2597-4bda-ba58-11dc72b87477 + - content: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''chunk_id''], + r[''document_title''], r[''content''][:200])\n","stdout":"17\n7c3a912c-a769-49e9-a661-402d7d64ec9f None Phase 2: + Label selection and guideline. We reviewed the collected documents and identified the most common structural features + they exhibit. This was achieved by identifying recurrent layout elements \n62e98936-a694-4cb0-a1a4-409b0121e08d None Phase 1: Data selection and preparation. Our inclusion criteria for documents were described in Section 3. - A large effort went into ensuring that all documents are free to use. The data sources includ\n1cbffceb-fa16-434f-a168-54ad860b1c21 + A large effort went into ensuring that all documents are free to use. The data sources includ\nd583f0ed-c230-4f8e-988a-86875c0c633d None $_{Affiliation}$, as seen in DocBank, are often only distinguishable by discriminating on\nPreparation work - included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n44a5a4d6-7f42-46d2-9115-4dda6b6d62ee + included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n180df00a-2a5b-4e2a-bfcc-4cd67fbc9db0 None \nTable 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative - occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\nfe743e25-0740-423c-9f26-d42e658269f2 + occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\nf3c18411-85df-4c72-a784-f39f602745ec None Page-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 96-98. Page-header, Count = 58022. Page-header, - % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\ned730761-c984-419a-9fdd-06fd37aa632a - None 0.5-0.95 (%).Law = 87-94. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-73. Section-header, - triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 78-86. Table, Count = 34733. Table, % o\na669db88-1bd8-46f3-87ec-078b74151258 - None Caption, Count = 22524. Caption, % of Total.Train = 2.04. Caption, % of Total.Test = 1.77. Caption, % of Total.Val - = 2.32. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).All = 84-89. Caption, trip\n38cf7b9f-3947-45f2-a66a-5ef4d89a88c2 - None inter-annotator mAP @ 0.5-0.95 (%).Pat = 66-71. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 59-76. - Section-header, Count = 142884. Section-header, % of Total.Train = 12.60. Section-header\n26781f97-2e94-4270-85e7-84670a7971f2 - None n/a. Footnote, Count = 6318. Footnote, % of Total.Train = 0.60. Footnote, % of Total.Test = 0.31. Footnote, - % of Total.Val = 0.58. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-91. Foot\n1eb6520a-3d81-4113-a8b1-4a045bdd581d - None 185660. List-item, % of Total.Train = 17.19. List-item, % of Total.Test = 13.34. List-item, % of Total.Val - = 15.82. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).All = 87-88. List-item, triple \nec1f7ead-7b8c-46f7-926b-f0d6c942ad02 - None 0.5-0.95 (%).Ten = 93-95. Page-footer, Count = 70878. Page-footer, % of Total.Train = 6.51. Page-footer, % - of Total.Test = 5.58. Page-footer, % of Total.Val = 6.00. Page-footer, triple inter-annotator\n8e924800-19a6-4096-8e36-c211ac858ffa - None 89-93. Text, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 87-92. Text, triple inter-annotator mAP @ 0.5-0.95 - (%).Pat = 71-79. Text, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 87-95. Title, Cou\na36c2e35-4c45-462f-8997-d800667e2c5c - None 0.5-0.95 (%).Pat = 97-99. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 81-86. Picture, Count - = 45976. Picture, % of Total.Train = 4.21. Picture, % of Total.Test = 2.78. Picture, % of T\nd0d1d4b6-bd81-42f4-a9d1-443f8869defd - None (%).Sci = 94-100. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 82-96. Title, triple inter-annotator - mAP @ 0.5-0.95 (%).Pat = 68-79. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 24-\n3cdae3b8-d62e-497a-908b-dac06a9b87a8 - None inter-annotator mAP @ 0.5-0.95 (%).Law = 58-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 79-84. - Table, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 70-85. Text, Count = 510377. Text, \n525a5cca-93ee-45be-8d67-f3e54286a80f - None = 82-97. Formula, Count = 25027. Formula, % of Total.Train = 2.25. Formula, % of Total.Test = 1.90. Formula, - % of Total.Val = 2.96. Formula, triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-85. Form\n5667fabf-8421-4004-bccf-9ce1b100f49a - None mAP @ 0.5-0.95 (%).Sci = 89-94. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 86-91. Total, triple - inter-annotator mAP @ 0.5-0.95 (%).Pat = 71-76. Total, triple inter-annotator mAP @ 0.5-0.95\n","stderr":"","success":true}' + % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\n","stderr":"","success":true}' role: tool - tool_call_id: call_a5m383mw + tool_call_id: call_j7uw3i6n - content: null - reasoning: 'We need list of element types. From snippets: Page-footer, Page-header, Section-header, Table, Caption, - Picture, Footnote, List-item, Text, Title, Formula. Also maybe others like List, Equation? Let''s search for "element - types" keyword.' + reasoning: We need list all document element types or labels mentioned. Likely these are labels in documents like + "Page-header", "Page-footer", etc. Let's search more. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], - r[''document_title''], r[''content''][:200])\n"}' + arguments: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look + for uppercase words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", + r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n"}' name: execute_code - id: call_5sejw3xj + id: call_gzg850pl type: function - - content: '{"code":"results = search(\"element types\", limit=20)\nfor r in results:\n print(r[''chunk_id''], r[''document_title''], - r[''content''][:200])\n","stdout":"baf79253-e710-41e4-9afc-cd53a58b14e6 None Phase 2: Label selection and guideline. - We reviewed the collected documents and identified the most common structural features they exhibit. This was achieved - by identifying recurrent layout elements \n26781f97-2e94-4270-85e7-84670a7971f2 None n/a. Footnote, Count = 6318. - Footnote, % of Total.Train = 0.60. Footnote, % of Total.Test = 0.31. Footnote, % of Total.Val = 0.58. Footnote, - triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-91. Foot\nd0d1d4b6-bd81-42f4-a9d1-443f8869defd None (%).Sci = - 94-100. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 82-96. Title, triple inter-annotator mAP @ 0.5-0.95 - (%).Pat = 68-79. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 24-\na669db88-1bd8-46f3-87ec-078b74151258 - None Caption, Count = 22524. Caption, % of Total.Train = 2.04. Caption, % of Total.Test = 1.77. Caption, % of Total.Val - = 2.32. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).All = 84-89. Caption, trip\nfe743e25-0740-423c-9f26-d42e658269f2 - None Page-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 96-98. Page-header, Count = 58022. Page-header, - % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\ned730761-c984-419a-9fdd-06fd37aa632a - None 0.5-0.95 (%).Law = 87-94. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-73. Section-header, - triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 78-86. Table, Count = 34733. Table, % o\n1eb6520a-3d81-4113-a8b1-4a045bdd581d - None 185660. List-item, % of Total.Train = 17.19. List-item, % of Total.Test = 13.34. List-item, % of Total.Val - = 15.82. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).All = 87-88. List-item, triple \n525a5cca-93ee-45be-8d67-f3e54286a80f - None = 82-97. Formula, Count = 25027. Formula, % of Total.Train = 2.25. Formula, % of Total.Test = 1.90. Formula, - % of Total.Val = 2.96. Formula, triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-85. Form\n38cf7b9f-3947-45f2-a66a-5ef4d89a88c2 - None inter-annotator mAP @ 0.5-0.95 (%).Pat = 66-71. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 59-76. - Section-header, Count = 142884. Section-header, % of Total.Train = 12.60. Section-header\na36c2e35-4c45-462f-8997-d800667e2c5c - None 0.5-0.95 (%).Pat = 97-99. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 81-86. Picture, Count - = 45976. Picture, % of Total.Train = 4.21. Picture, % of Total.Test = 2.78. Picture, % of T\n8e924800-19a6-4096-8e36-c211ac858ffa - None 89-93. Text, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 87-92. Text, triple inter-annotator mAP @ 0.5-0.95 - (%).Pat = 71-79. Text, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 87-95. Title, Cou\nec1f7ead-7b8c-46f7-926b-f0d6c942ad02 - None 0.5-0.95 (%).Ten = 93-95. Page-footer, Count = 70878. Page-footer, % of Total.Train = 6.51. Page-footer, % - of Total.Test = 5.58. Page-footer, % of Total.Val = 6.00. Page-footer, triple inter-annotator\n44a5a4d6-7f42-46d2-9115-4dda6b6d62ee - None \nTable 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative - occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\n3cdae3b8-d62e-497a-908b-dac06a9b87a8 - None inter-annotator mAP @ 0.5-0.95 (%).Law = 58-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 79-84. - Table, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 70-85. Text, Count = 510377. Text, \n6377d621-2597-4bda-ba58-11dc72b87477 - None Phase 1: Data selection and preparation. Our inclusion criteria for documents were described in Section 3. - A large effort went into ensuring that all documents are free to use. The data sources includ\n1cbffceb-fa16-434f-a168-54ad860b1c21 - None $_{Affiliation}$, as seen in DocBank, are often only distinguishable by discriminating on\nPreparation work - included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n5667fabf-8421-4004-bccf-9ce1b100f49a - None mAP @ 0.5-0.95 (%).Sci = 89-94. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 86-91. Total, triple - inter-annotator mAP @ 0.5-0.95 (%).Pat = 71-76. Total, triple inter-annotator mAP @ 0.5-0.95\n","stderr":"","success":true}' + - content: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look for uppercase + words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' role: tool - tool_call_id: call_5sejw3xj + tool_call_id: call_gzg850pl model: gpt-oss reasoning_effort: low stream: false @@ -1051,7 +973,706 @@ interactions: response: headers: content-length: - - '1744' + - '704' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Import not allowed. Use string methods. Probably easier to manually inspect. Let's list results. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"label\" , limit=10)\nfor r in results:\n print(''---'')\n print(r[''chunk_id''], + r[''document_title''])\n print(r[''content''][:400])\n"}' + name: execute_code + id: call_4s9httx6 + index: 0 + type: function + created: 1771924572 + id: chatcmpl-410 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 92 + prompt_tokens: 2421 + total_tokens: 2513 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '75' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - label + 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: 2 + total_tokens: 2 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '15687' + 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. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Search for content about document element types or labels. What are all the different document element types + mentioned? List them all. + role: user + - content: null + reasoning: Need to search for document element types or labels. Use search. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r + in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''content''][:200])\n"}' + name: execute_code + id: call_j7uw3i6n + type: function + - content: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''chunk_id''], + r[''document_title''], r[''content''][:200])\n","stdout":"17\n7c3a912c-a769-49e9-a661-402d7d64ec9f None Phase 2: + Label selection and guideline. We reviewed the collected documents and identified the most common structural features + they exhibit. This was achieved by identifying recurrent layout elements \n62e98936-a694-4cb0-a1a4-409b0121e08d + None Phase 1: Data selection and preparation. Our inclusion criteria for documents were described in Section 3. + A large effort went into ensuring that all documents are free to use. The data sources includ\nd583f0ed-c230-4f8e-988a-86875c0c633d + None $_{Affiliation}$, as seen in DocBank, are often only distinguishable by discriminating on\nPreparation work + included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n180df00a-2a5b-4e2a-bfcc-4cd67fbc9db0 + None \nTable 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative + occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\nf3c18411-85df-4c72-a784-f39f602745ec + None Page-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 96-98. Page-header, Count = 58022. Page-header, + % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\n","stderr":"","success":true}' + role: tool + tool_call_id: call_j7uw3i6n + - content: null + reasoning: We need list all document element types or labels mentioned. Likely these are labels in documents like + "Page-header", "Page-footer", etc. Let's search more. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look + for uppercase words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", + r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n"}' + name: execute_code + id: call_gzg850pl + type: function + - content: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look for uppercase + words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' + role: tool + tool_call_id: call_gzg850pl + - content: null + reasoning: Import not allowed. Use string methods. Probably easier to manually inspect. Let's list results. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"label\" , limit=10)\nfor r in results:\n print(''---'')\n print(r[''chunk_id''], + r[''document_title''])\n print(r[''content''][:400])\n"}' + name: execute_code + id: call_4s9httx6 + type: function + - content: '{"code":"results = await search(\"label\" , limit=10)\nfor r in results:\n print(''---'')\n print(r[''chunk_id''], + r[''document_title''])\n print(r[''content''][:400])\n","stdout":"---\n7c3a912c-a769-49e9-a661-402d7d64ec9f None\nPhase + 2: Label selection and guideline. We reviewed the collected documents and identified the most common structural + features they exhibit. This was achieved by identifying recurrent layout elements and lead us to the definition + of 11 distinct class labels. These 11 class labels are $_{Caption}$, $_{Footnote}$, $_{Formula}$, $_{List-item}$, + Page-$_{footer}$, $_{Page-header}$, $_{Picture}$, $_{Sec\n---\n180df00a-2a5b-4e2a-bfcc-4cd67fbc9db0 None\n\nTable + 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative occurrence + (as % of row \"Total\") in the train, test and validation sets. The inter-annotator agreement is computed as the + mAP@0.5-0.95 metric between pairwise annotations from the triple-annotated pages, from which we obtain accuracy + ranges.\n---\n5c6655e2-40d8-4ea8-9741-de6a98895449 None\nCaption, Count = 22524. Caption, % of Total.Train = 2.04. + Caption, % of Total.Test = 1.77. Caption, % of Total.Val = 2.32. Caption, triple inter-annotator mAP @ 0.5-0.95 + (%).All = 84-89. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Fin = 40-61. Caption, triple inter-annotator + mAP @ 0.5-0.95 (%).Man = 86-92. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 94-99. Caption, triple + inter\n---\nf3c18411-85df-4c72-a784-f39f602745ec None\nPage-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + = 96-98. Page-header, Count = 58022. Page-header, % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. + Page-header, % of Total.Val = 5.06. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).All = 85-89. Page-header, + triple inter-annotator mAP @ 0.5-0.95 (%).Fin = 66-76. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ma\n---\n6cb167da-a385-4db9-af59-62f23b7cc7b5 + None\nmAP @ 0.5-0.95 (%).Sci = 89-94. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 86-91. Total, triple + inter-annotator mAP @ 0.5-0.95 (%).Pat = 71-76. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 68-85\nFigure + 3: Corpus Conversion Service annotation user interface. The PDF page is shown in the background, with overlaid text-cells + (in darker shades). The annotation boxes can be drawn \n---\nf647f4ad-7008-40f4-8fe4-f8460da0b5a7 None\n(%).Sci + = 94-100. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 82-96. Title, triple inter-annotator mAP @ 0.5-0.95 + (%).Pat = 68-79. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 24-56. Total, Count = 1107470. Total, % + of Total.Train = 941123. Total, % of Total.Test = 99816. Total, % of Total.Val = 66531. Total, triple inter-annotator + mAP @ 0.5-0.95 (%).All = 82-83. Total, triple\n---\n70c969da-7627-48d7-9944-5274284c4bdf None\nn/a. Footnote, Count + = 6318. Footnote, % of Total.Train = 0.60. Footnote, % of Total.Test = 0.31. Footnote, % of Total.Val = 0.58. Footnote, + triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-91. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Fin = + n/a. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Man = 100. Footnote, triple inter-annotator mAP @ 0.5-0.95 + (%).Sci = 62-88. Footnote, tri\n---\n002567d1-7967-4bbb-bf7e-93f6a1bf56d3 None\n0.5-0.95 (%).Law = 87-94. Section-header, + triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-73. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + = 78-86. Table, Count = 34733. Table, % of Total.Train = 3.20. Table, % of Total.Test = 2.27. Table, % of Total.Val + = 3.60. Table, triple inter-annotator mAP @ 0.5-0.95 (%).All = 77-81. Table, triple inter-annotator mAP @ 0.5-0.95 + (%).Fin = \n---\n6f761413-b87a-436c-b2bb-ec1997478dec None\ninter-annotator mAP @ 0.5-0.95 (%).Pat = 66-71. Picture, + triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 59-76. Section-header, Count = 142884. Section-header, % of Total.Train + = 12.60. Section-header, % of Total.Test = 15.77. Section-header, % of Total.Val = 12.85. Section-header, triple + inter-annotator mAP @ 0.5-0.95 (%).All = 83-84. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Fin \n---\neba63d0e-c89f-4ae8-a6ac-4f96788fdb21 + None\n0.5-0.95 (%).Pat = 97-99. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 81-86. Picture, Count + = 45976. Picture, % of Total.Train = 4.21. Picture, % of Total.Test = 2.78. Picture, % of Total.Val = 5.31. Picture, + triple inter-annotator mAP @ 0.5-0.95 (%).All = 69-71. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Fin = + 56-59. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Man \n","stderr":"","success":true}' + role: tool + tool_call_id: call_4s9httx6 + model: gpt-oss + reasoning_effort: low + stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '945' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: 'We see labels: Caption, Footnote, Formula, List-item, Page-footer, Page-header, Picture, Title, Table, + Section-header. Also maybe others. Probably 11 class labels mentioned: Caption, Footnote, Formula, List-item, + Page-footer, Page-header, Picture, Title, Table, Section-header, maybe ? Let''s list unique. Use manual extraction + maybe.' + role: assistant + tool_calls: + - function: + arguments: '{"code":"# extract manually known labels\nlabels=[''Caption'',''Footnote'',''Formula'',''List-item'',''Page-footer'',''Page-header'',''Picture'',''Title'',''Table'',''Section-header'']\nprint(labels)\n"}' + name: execute_code + id: call_2fnlhi2j + index: 0 + type: function + created: 1771924578 + id: chatcmpl-8 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 138 + prompt_tokens: 4309 + total_tokens: 4447 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '16793' + 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. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Search for content about document element types or labels. What are all the different document element types + mentioned? List them all. + role: user + - content: null + reasoning: Need to search for document element types or labels. Use search. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r + in results[:5]:\n print(r[''chunk_id''], r[''document_title''], r[''content''][:200])\n"}' + name: execute_code + id: call_j7uw3i6n + type: function + - content: '{"code":"results = await search(\"document element types\" , limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''chunk_id''], + r[''document_title''], r[''content''][:200])\n","stdout":"17\n7c3a912c-a769-49e9-a661-402d7d64ec9f None Phase 2: + Label selection and guideline. We reviewed the collected documents and identified the most common structural features + they exhibit. This was achieved by identifying recurrent layout elements \n62e98936-a694-4cb0-a1a4-409b0121e08d + None Phase 1: Data selection and preparation. Our inclusion criteria for documents were described in Section 3. + A large effort went into ensuring that all documents are free to use. The data sources includ\nd583f0ed-c230-4f8e-988a-86875c0c633d + None $_{Affiliation}$, as seen in DocBank, are often only distinguishable by discriminating on\nPreparation work + included uploading and parsing the sourced PDF documents in the Corpus Conversion Service (CC\n180df00a-2a5b-4e2a-bfcc-4cd67fbc9db0 + None \nTable 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative + occurrence (as % of row \"Total\") in the train, test and validation sets. The inter-annotator\nf3c18411-85df-4c72-a784-f39f602745ec + None Page-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 96-98. Page-header, Count = 58022. Page-header, + % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. Page-header, % of Total.Val =\n","stderr":"","success":true}' + role: tool + tool_call_id: call_j7uw3i6n + - content: null + reasoning: We need list all document element types or labels mentioned. Likely these are labels in documents like + "Page-header", "Page-footer", etc. Let's search more. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look + for uppercase words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", + r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n"}' + name: execute_code + id: call_gzg850pl + type: function + - content: '{"code":"results = await search(\"label\" , limit=30)\nlabels=set()\nfor r in results:\n # look for uppercase + words separated by hyphens\n import re\n for m in re.findall(r\"([A-Z][A-Za-z]+(?:-[A-Z][A-Za-z]+)*)\", r[''content'']):\n labels.add(m)\nprint(sorted(labels))\n","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' + role: tool + tool_call_id: call_gzg850pl + - content: null + reasoning: Import not allowed. Use string methods. Probably easier to manually inspect. Let's list results. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"label\" , limit=10)\nfor r in results:\n print(''---'')\n print(r[''chunk_id''], + r[''document_title''])\n print(r[''content''][:400])\n"}' + name: execute_code + id: call_4s9httx6 + type: function + - content: '{"code":"results = await search(\"label\" , limit=10)\nfor r in results:\n print(''---'')\n print(r[''chunk_id''], + r[''document_title''])\n print(r[''content''][:400])\n","stdout":"---\n7c3a912c-a769-49e9-a661-402d7d64ec9f None\nPhase + 2: Label selection and guideline. We reviewed the collected documents and identified the most common structural + features they exhibit. This was achieved by identifying recurrent layout elements and lead us to the definition + of 11 distinct class labels. These 11 class labels are $_{Caption}$, $_{Footnote}$, $_{Formula}$, $_{List-item}$, + Page-$_{footer}$, $_{Page-header}$, $_{Picture}$, $_{Sec\n---\n180df00a-2a5b-4e2a-bfcc-4cd67fbc9db0 None\n\nTable + 1: DocLayNet dataset overview. Along with the frequency of each class label, we present the relative occurrence + (as % of row \"Total\") in the train, test and validation sets. The inter-annotator agreement is computed as the + mAP@0.5-0.95 metric between pairwise annotations from the triple-annotated pages, from which we obtain accuracy + ranges.\n---\n5c6655e2-40d8-4ea8-9741-de6a98895449 None\nCaption, Count = 22524. Caption, % of Total.Train = 2.04. + Caption, % of Total.Test = 1.77. Caption, % of Total.Val = 2.32. Caption, triple inter-annotator mAP @ 0.5-0.95 + (%).All = 84-89. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Fin = 40-61. Caption, triple inter-annotator + mAP @ 0.5-0.95 (%).Man = 86-92. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 94-99. Caption, triple + inter\n---\nf3c18411-85df-4c72-a784-f39f602745ec None\nPage-footer, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + = 96-98. Page-header, Count = 58022. Page-header, % of Total.Train = 5.10. Page-header, % of Total.Test = 6.70. + Page-header, % of Total.Val = 5.06. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).All = 85-89. Page-header, + triple inter-annotator mAP @ 0.5-0.95 (%).Fin = 66-76. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ma\n---\n6cb167da-a385-4db9-af59-62f23b7cc7b5 + None\nmAP @ 0.5-0.95 (%).Sci = 89-94. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 86-91. Total, triple + inter-annotator mAP @ 0.5-0.95 (%).Pat = 71-76. Total, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 68-85\nFigure + 3: Corpus Conversion Service annotation user interface. The PDF page is shown in the background, with overlaid text-cells + (in darker shades). The annotation boxes can be drawn \n---\nf647f4ad-7008-40f4-8fe4-f8460da0b5a7 None\n(%).Sci + = 94-100. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 82-96. Title, triple inter-annotator mAP @ 0.5-0.95 + (%).Pat = 68-79. Title, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 24-56. Total, Count = 1107470. Total, % + of Total.Train = 941123. Total, % of Total.Test = 99816. Total, % of Total.Val = 66531. Total, triple inter-annotator + mAP @ 0.5-0.95 (%).All = 82-83. Total, triple\n---\n70c969da-7627-48d7-9944-5274284c4bdf None\nn/a. Footnote, Count + = 6318. Footnote, % of Total.Train = 0.60. Footnote, % of Total.Test = 0.31. Footnote, % of Total.Val = 0.58. Footnote, + triple inter-annotator mAP @ 0.5-0.95 (%).All = 83-91. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Fin = + n/a. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Man = 100. Footnote, triple inter-annotator mAP @ 0.5-0.95 + (%).Sci = 62-88. Footnote, tri\n---\n002567d1-7967-4bbb-bf7e-93f6a1bf56d3 None\n0.5-0.95 (%).Law = 87-94. Section-header, + triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-73. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + = 78-86. Table, Count = 34733. Table, % of Total.Train = 3.20. Table, % of Total.Test = 2.27. Table, % of Total.Val + = 3.60. Table, triple inter-annotator mAP @ 0.5-0.95 (%).All = 77-81. Table, triple inter-annotator mAP @ 0.5-0.95 + (%).Fin = \n---\n6f761413-b87a-436c-b2bb-ec1997478dec None\ninter-annotator mAP @ 0.5-0.95 (%).Pat = 66-71. Picture, + triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 59-76. Section-header, Count = 142884. Section-header, % of Total.Train + = 12.60. Section-header, % of Total.Test = 15.77. Section-header, % of Total.Val = 12.85. Section-header, triple + inter-annotator mAP @ 0.5-0.95 (%).All = 83-84. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Fin \n---\neba63d0e-c89f-4ae8-a6ac-4f96788fdb21 + None\n0.5-0.95 (%).Pat = 97-99. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Ten = 81-86. Picture, Count + = 45976. Picture, % of Total.Train = 4.21. Picture, % of Total.Test = 2.78. Picture, % of Total.Val = 5.31. Picture, + triple inter-annotator mAP @ 0.5-0.95 (%).All = 69-71. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Fin = + 56-59. Picture, triple inter-annotator mAP @ 0.5-0.95 (%).Man \n","stderr":"","success":true}' + role: tool + tool_call_id: call_4s9httx6 + - content: null + reasoning: 'We see labels: Caption, Footnote, Formula, List-item, Page-footer, Page-header, Picture, Title, Table, + Section-header. Also maybe others. Probably 11 class labels mentioned: Caption, Footnote, Formula, List-item, Page-footer, + Page-header, Picture, Title, Table, Section-header, maybe ? Let''s list unique. Use manual extraction maybe.' + role: assistant + tool_calls: + - function: + arguments: '{"code":"# extract manually known labels\nlabels=[''Caption'',''Footnote'',''Formula'',''List-item'',''Page-footer'',''Page-header'',''Picture'',''Title'',''Table'',''Section-header'']\nprint(labels)\n"}' + name: execute_code + id: call_2fnlhi2j + type: function + - content: '{"code":"# extract manually known labels\nlabels=[''Caption'',''Footnote'',''Formula'',''List-item'',''Page-footer'',''Page-header'',''Picture'',''Title'',''Table'',''Section-header'']\nprint(labels)\n","stdout":"[''Caption'', + ''Footnote'', ''Formula'', ''List-item'', ''Page-footer'', ''Page-header'', ''Picture'', ''Title'', ''Table'', ''Section-header'']\n","stderr":"","success":true}' + role: tool + tool_call_id: call_2fnlhi2j + model: gpt-oss + reasoning_effort: low + stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '1797' content-type: - application/json parsed_body: @@ -1059,28 +1680,28 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"The documents we examined list the following distinct document‑element types (each appearing - as a separate class label in the annotation datasets):\n\n1. Page‑header\n2. Page‑footer\n3. Section‑header\n4. - Table\n5. Caption\n6. Picture\n7. Footnote\n8. List‑item\n9. Text\n10. Title\n11. Formula\n\nThese are the unique - element categories identified across the datasets (DocLayNet/DocBank annotations).","program":"# Consolidated - program to extract unique document element types\n# Search for fragments that mention ‘label’ or ‘class’ to capture - element names\nresults = search(\"class label\", limit=50)\nunique_types = set()\nfor r in results:\n # find - capitalized words followed by ‘label’ or ‘class’\n chunk = get_chunk(r[''chunk_id''])\n if not chunk:\n continue\n text - = chunk[''content'']\n # simple split to capture possible types\n for word in text.split():\n if - word.lower().startswith((''page-header'',''page-footer'',''section-header'',''table'',''caption'',''picture'',''footnote'',''list-item'',''text'',''title'',''formula'')):\n unique_types.add(word.replace(\"–\",\"-\").strip('',''))\n# - print the unique element types\nprint(sorted(unique_types))"}' - reasoning: 'Thus list: Page-header, Page-footer, Section-header, Table, Caption, Picture, Footnote, List-item, Text, - Title, Formula. Also maybe "Formula" is element type. Provide list.' + content: '{"answer":"The documents describe a total of **10 distinct document element types (labels)**:\n\n1. **Caption**\n2. + **Footnote**\n3. **Formula**\n4. **List-item**\n5. **Page‑header**\n6. **Page‑footer**\n7. **Picture**\n8. **Title**\n9. + **Table**\n10. **Section‑header**","program":"# Consolidated script to discover document element types\nresults + = await search(\"document element types\", limit=50)\nlabels = set()\nfor r in results:\n # Search for capitalised + words that match the known classes\n # This uses simple string checks because imports are not allowed.\n if + \"Caption\" in r[''content'']:\n labels.add(\"Caption\")\n if \"Footnote\" in r[''content'']:\n labels.add(\"Footnote\")\n if + \"Formula\" in r[''content'']:\n labels.add(\"Formula\")\n if \"List-item\" in r[''content'']:\n labels.add(\"List-item\")\n if + \"Page-header\" in r[''content'']:\n labels.add(\"Page-header\")\n if \"Page-footer\" in r[''content'']:\n labels.add(\"Page-footer\")\n if + \"Picture\" in r[''content'']:\n labels.add(\"Picture\")\n if \"Title\" in r[''content'']:\n labels.add(\"Title\")\n if + \"Table\" in r[''content'']:\n labels.add(\"Table\")\n if \"Section-header\" in r[''content'']:\n labels.add(\"Section-header\")\nprint(\"Found + labels:\", sorted(labels))"}' + reasoning: Answer list. role: assistant - created: 1771336786 - id: chatcmpl-766 + created: 1771924587 + id: chatcmpl-392 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 346 - prompt_tokens: 5162 - total_tokens: 5508 + completion_tokens: 398 + prompt_tokens: 4547 + total_tokens: 4945 status: code: 200 message: OK diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_get_chunk.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_get_chunk.yaml index 6b9d38df..3e87ecb1 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_get_chunk.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_search_and_get_chunk.yaml @@ -48,7 +48,7 @@ interactions: connection: - keep-alive content-length: - - '7117' + - '7359' content-type: - application/json host: @@ -61,32 +61,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -103,7 +104,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -112,21 +113,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -136,9 +137,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -149,16 +150,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -234,7 +235,7 @@ interactions: response: headers: content-length: - - '593' + - '537' content-type: - application/json parsed_body: @@ -243,25 +244,24 @@ interactions: index: 0 message: content: '' - reasoning: We need to search. + reasoning: We need to search for animals. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' + arguments: '{"code":"results = await search(\"animals\", limit=10)\nprint(results)\n"}' name: execute_code - id: call_tpl0j81u + id: call_pvjujvr9 index: 0 type: function - created: 1771336724 - id: chatcmpl-115 + created: 1771924521 + id: chatcmpl-217 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 67 - prompt_tokens: 1568 - total_tokens: 1635 + completion_tokens: 46 + prompt_tokens: 1629 + total_tokens: 1675 status: code: 200 message: OK @@ -314,7 +314,7 @@ interactions: connection: - keep-alive content-length: - - '7760' + - '8119' content-type: - application/json host: @@ -327,32 +327,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -369,7 +370,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -378,21 +379,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -402,9 +403,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -415,16 +416,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -451,19 +452,20 @@ interactions: - content: Search for content about animals and tell me which document it came from. role: user - content: null - reasoning: We need to search. + reasoning: We need to search for animals. role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' + arguments: '{"code":"results = await search(\"animals\", limit=10)\nprint(results)\n"}' name: execute_code - id: call_tpl0j81u + id: call_pvjujvr9 type: function - - content: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], r[''chunk_id''], - r[''page_numbers''][:5])\n","stdout":"Animal Facts eb0e1e92-35b2-4f5e-8011-2a67f3dadf00 []\n","stderr":"","success":true}' + - content: '{"code":"results = await search(\"animals\", limit=10)\nprint(results)\n","stdout":"[{''chunk_id'': ''503004ae-51ea-4953-93c3-48acac4a929c'', + ''content'': ''The quick brown fox jumps over the lazy dog.'', ''document_id'': ''ee57cfe6-fe82-4162-afd9-f206002ae08e'', + ''document_title'': ''Animal Facts'', ''document_uri'': None, ''score'': 0.016393441706895828, ''page_numbers'': + [], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_tpl0j81u + tool_call_id: call_pvjujvr9 model: gpt-oss reasoning_effort: low stream: false @@ -514,567 +516,7 @@ interactions: response: headers: content-length: - - '613' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: Only one result? Let's list maybe more. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results = search(\"animals\", limit=20)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' - name: execute_code - id: call_orialuab - index: 0 - type: function - created: 1771336725 - id: chatcmpl-99 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 71 - prompt_tokens: 1730 - total_tokens: 1801 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '77' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - animals - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: c1YDOcck2zxYaqa8Lu0RvZMWKDr+LgA9RY1TPUhmIThQTQ89fxacPGMbT7xvkjg84j2HuGR5IL1XDFs9PJcGu2WjXD1IfUK9MCJEvd7UDbznYuO8a4ePPLSlJr3uFrg8dXIQPVLN/LxYsMy8lZhJvcv/tTziShi7FAH7vNRolLynnDI9iQJFvMw3yzsv+IW8K+qGPKQDzbvNEDQ8OZeGvXe9CbvLghS9goL1PCLKBrsMoiS801mqvG7Zu7pL6q08BPDOvDa2t7ytn/E7jbqAO/5u4zv0vpC8fg9MPOg5ALwweZw9//z2u4C4sry8+QY9JVs7vHW417z+MJW8MpyCvLdtI7vWWUK8/6e5vE/Qjr03cpM8hc8qvL4mUrwZNlQ9twpFvJKmYbwoJeI8RmCIvB9MvrrhUgE9d8DMOyOm0Tyqi588Mv3vOXbuXjsnhTw98ZkgPUfRnjwJG7I8qUa2O3EAVby6Kho7BUqfOy8e3Tr3C8q7boGoPAhQA7w5MgI8sl2Gu3uwkrxTHZu7LtBjPARIHru1AqS7b0iCPRRC4rt4sCI8rUUfvZx3i7xh/ok71hhFvErUmLzmuHG8uvHOu8XJmjwxy928QYU0vHWhx7s2oK+69fI+PVbHdLvzYpY8+Tr6u7/eejwcxtM7D9ayPOQNizu4xmS8M2kyvOLMTbxpvqi8dJJ1PD4EkTz1gcu8dR5xPCvUsrsP4js6jnyEvIMYl7s8LKW6krH4uog3JTy8rRy6I1umuxgbDDj29nE8YH1UvEwqFTllQmm8E6i/uCy9mTyuCwu8l3sTPKS5K7yHvyw8T0G4PKphTTxkSus7c5FxvNQVBrs2yc87qg5mu2irBzxWauE7KgXiuwDrHT2LqX08RES7OweuCr2ZxSq86pz8u9efAb3aVUK8N4q/vKS9TzyABJu8SWutvJc8qTubCbq8cpDNOworSbnHaRq8J2STu5vINbtG35i81WbBu9ZzgzyYQF+6rBYTu3nrIzzrKxo8YkZVunQDbb0LGYQ7+i3bOraTtbxz3M67SvCzvNAkw7xI61Q8U2VVvLTK8Dy08ZK8anhFvMd+8ryrNvS7EYAYPMcARbvSFMk4/yGcvKIJEjo0F4w4ML8uPHgPPrzfEnK8yLbivD77JDzYaCO8PbanvLqqyryBGwM9dCzsPLFRKzsYILk8B8LDu3Q9zjsmUB+947KFOoUJeTzVPF08QBugPAVaEDupSXm7V7svPE8sBrxnL+y7pcW5u2ZOjDuhbNg76T80uzAqSbyb9ou8uVRvvFgmdLxE/we83uzPu3/j3jvME7e8ZbqJuPzWa7xIhhK8F9QHvNjvQryKONM6fjN/PJSh0rw1Vdu8HwMRPJkqqrxVaiK9K6KRvJ6kpDy0u2S7pz4nPLl/j7sJZOe7bPvMO9o2CrxexAS8BuOwvCBXqzwW1Z68guAtPTDYvDsva9I7uk8APOJWeLsW6Ia8Mo8nu92ot7tPd/U79bn/PFo067y4hC+8AwcHO0NNmDy8OMs7YdKKPADoSLwIpNc8yTGPvC7BiDvWn0A83tBhOz3iFD069J+7E4UJOx/zQTwTl6E7z1ECuqEjmTyymte88NOKvP/UJLyg7Qm71f6oPONZSzzj5/o8QiKluu9WjDwmDGy9ntspOvBcgLvnkwq9KHWeOxC5DDt+ef675FEOvUkshLwGRaw7idvmvEw7lLw41/I7AA3TvGRO67v40Q87R6iUO3d08brhTig8GOmFPJxt4jtTnaY9yiXgvDvAbzwxpni8XqhsvMYI/bt84d+8ngS8u+XyNjw50vg8cHNlPBMdJDw7h2C8eMOhPEYbDL0wrSy8e82CPOcUILtfwKI7T1sxvYx/xLy42hm8rlfmvFMRkTsaNZI7092zOEcV1Tu7K9Q6/P5pPGryOrwGqIq8fGF/vPtlpTobDz67/5I0O/nzQTxMnyU8REEKvAtBHD0lli29Fth1PLvSLbzvsFm8/OfxPBqYAbzfWDY8jXjCvIaIyDwO9hu91fKru//OQLts6Ms8/NS9PC38E7z8UlY8NZiEuyEFvzy4wKE7l0EzO++iS73SBG67nBpeu+iNCDzk9Su7g8UjPdhBJDwjvNM8Uv4jPNmYAT2Qfa482Kelu8Zn17wuUz68QR8HvScN1LwLIeQ7C/DbvPoSl7zgyZI9RjgLPKo6oDuNYh68BMq1PCzkNTz25WC8lL2DPCsBwzvM9ac8msTqvHp59rw6a38804B6PAM1FLzvQhy7Qv8bPBgcNj27YBs4esxmvJUCzjuFg6e7wQ2qvFPFFzpwPEm8GcXLPPrk8zz/qjy8B1g4O9lWYLsPPo68tFvCul/04DibiKC662Q/vCtaLTy0GYA8ykMUvBNJ8rtV19I7pCgOvAW5aTzTyia94HlIvHQAn7zlpJu7z4ALPB14Pr12aZ08DcoqPNT65bzJmKC7h2R2vD7psb0qHZw83YCouuVKpLrFfGK8n7z+vPJu57yktWS8yFPKPFuCbDtywxu9QL0fvQPluLwb5208K/+lughPsDsVL2k8A+iIO86T4TuYT5s87ANtuf477TmtrXo8yAvZPKyxUzxjlos8Mt/4O3LXkjzizRq924JZvPSfhTwJ/167sp32OxRmvzz01+g7PSqAOVPHMT2MsLi8qidkvJ36g7t6U9a5EffQPPEllrnBBJ+8OovXvBvywTzResM8GfIaPMGlBzz8/HQ8S7atPC2tOzwnxIu7viFIvVEbAj1STS69IIZKvJcrTLuNd4w6l+SYu2nMpbww62C8vRsNPMVZhrzAVQW9kFWTPAWDuzxUtR29invJOx2eKjznnsq8yCeWvJaPwLy1R5o7CoH+vAvENzwLy3Q88zxSPZrFojyyjIY6OmsAPfyQH7wYYQs9TdvXO4Wz1juuwUM8UR1hPObAQ7scFBS9WtKhPH9RNrxejxC8VoxEO+CKqbw5Wr08s161O7jXMDz8N6g8daHCvJ1jfDy1S9a87EtUPG/Bhrxk5ei7EE/KvK0FRr0dGvu7dn87vLyYwLziCd87FYVzu5X0gTwquaA8gqRtvJMH1LwUHRW9TxAsPIJbkbw66US9Ckbju7AjaTzIHqE7Xxk0POEsqrwPhiY9+IdDPGqRIj0dP2W7WYiAPLyCI7x/4Ki80i4iPc7zTLxkNTS9TECJPIed7LqWHDi62uCpu14QUDrjjg87eyStPNnaG70AMI+8WdHLPIUnO7zoMBC7koxxvNXuuDxmGrS7FuMlPDxHjLwQ8fS8ZZCcu/Gq6zxSnn88/ergvPbiA716d1y7XbC6vF1ADr1zXdu8fM4ovGhgibuWNxE9+eOJPEn+sL0RGrE7GU+GPKmMmTx1kRy9dlKWOtYiYruo2Dg9xGgHvHGeIjyGd1y8zK02vZreYT2g4DU7xljuuyYV8Dzd/C+8ybMxPPbwAbwjZQU9dodYvPjIDb3F6hu8nJP6uj6CzrsqwSI7UngWvIzAgrwel4M8f+xAPPYArjwuy5M8wvhrPMusiDzvCeQ68uvkvCptgjx0QBs8wzI5u6qBIbxNEr03LbnMvOqhbLyrLAM8fdiJOxQ2dzyfC+Y7k1Gbu91ZoLvETTK72/lbvZOjBzy13n27qsA1PD2MajudRTS9ejbau/gzWjxfya28unhcOlbfwDxuMxA8zFlnun0GaTuNcrA8PxxUPFTwULyhHxa8MsP0Oz2JPzw+dSa8M8DMO62B0jwCVxe99MFZvP/aibw145M4HexiPMDOCLiRkCW8/3X+PEkArjxKHkU9k1eePKBNQD3GY6y8yFD4vDXxFL2XSOA7Rh65O0eDpLw1cyK91WCJvHU/wruXIvy6Q7tqu54uMTwNK8K8xvNtuxsn+Lv6YSo8Hb6RPHOtZrtkjy28bGMTPeCoODzwHmK7lB/zuwMa5jwJ1a686+pUPFZ/sDvEo6w8f/9wPM1oJLuv1AG6COfLu3OUZ7srO/08q3cKveA11rruS4C9OEW0uXazLLr43mQ83ZWcPFt1cDx8QHq5Y69LvD+pb7wxmmW610VbPB5KM7y+R948gUwEvNmY6rx04Cu8SGTduZqt7ryPc6e8V6J9OwSQgDy7dvM6eKcIvWHKqDyRGQG9ociFPJjmWrwV7xw8GcqhPO/pczzvh5i77OZqvIMJEz0As8y8MT5KPAWo9rxZHBO8dYQJvCTnj7wnZxq8Vq1qvJS8Cr2Oc8y8W4oovYKWmTwU8Ys9XrbNvDPNN71g/vg80MdVvI5/kbzgwLa8Vbvqu46VYDwmz9e7njxRvIlQt7u0Tzs9EaVXu5MiHT0VZ0q7QnRZPLCxdbx6b648/nr2ugYyFLxXjLK7x+3iO/E0IDy7lYa8q+rEuPq1kby/DZE7kk2PPBxR7zxLaqg8hayJu+mVAzzpaoi8lhajO3ZDarzfUIe7VWBVPN1Fuju2giY8faDAvMcAajyvI9e7sT7tvGrPpLwOnh88Z+HuvFsFB71d3qO8FJEnPMIqJzxVQW+7gwehvE68mjsasLq8P1zVOnFBhbyOQS06MdbwPCySLDzfgEk9rxGHPA+0sTiCXBE8ZWRVPP/PzTwSeIQ8xI9dvMlMEjw301w8pdkHvX/qU7zXWAC9t+xEPC8rUbw32Da8rJf+O7m0LzzcSKa8LXKWPFGK/zxNoAQ9UgIOvKX9izyI5Gi8G4bQPB05HbyfKpI8vkSCvEJ7mjz+is47tfnPPH78jzxdcHu8NuNVPLBHObrrWlK8RvsMPJS17bxDrMQ8Nm6Du2DcA71gPxs7ldrcvGe7trzRT2g8jf0bve6W3zyz8da8B+CMvFaIxjtvEQg8MiMrPNHF5DwYOSO89AcCvRSio7yItFc8OEVnu/j2tLwjqrK8+t77POSkA7zwZ0m9xisqPFbkOTwmTLk8SuCHvJwh6rusvy08lKYkvYJkCb0K8g+9XQGzPeU+WrwuHpQ8UDg3ure1Z701HOi7Xpx6vHFpnTqi1iM8BHIFO4Vhs7y1Vhc8tvnpurdek7wzHgM90iFLvGmI/rvTO9+7enDAOlJccj0vNg687eYLPdskgzwK0Y68NSYJPEkdzjvRNpE7IyQtOopWurx91h28H0xAPPnsv7ylYJk8xI+wPKr3hrzBEoQ8iXh6O5Qz7rtuUGS6U3uwO4WBZLxDzD48EXumPI8CejuViDS8r+3MuwoX9zskuC68XwzKPKjBJzyTMiU8bnLFPHIBjrywLfE8hObePMPp3DzqMM+8Ht0tvLZ+UjxMmmO88w17vP1j87w1+ro7XyaOvNcznjzqHui7GkCwvN090bvG3Uw8/+51vPUIGjwcZuA8SBGMu1y67Lsh3/E6Msz7u/L1O714u4w8crU2PPJVvTtZCRE7ngi3vMyu3jxYFKC8vNr0PEIM7Lx/F3+8a3wLPIlq0LziHsW6L3MUuRiZgDzlLci7E0AMvFNfF70w0B89A2TkvGDN2TtE5pM8g1nRO5Y/SDx4PDO9hu9CPbjCirxzIf68xNfQuymHFDwxvJ285thevAfnFD2jx7k828rwvPhGwjt6y627UBwqOplLFrhW1c68RwJxvLXLhrxc3xI7HE8APGzWWDwhUTU7jiydO0gHWLwXGDK8BJ7HvANu9Dz9bue7H73cu0m11btGiGi7HUlqPMG2RTzDWPY8u8NqPLb2UjzOyPW8zqYIvBbgb7vwS8w8U38LvEqxnDu3vne8pb61vH/CCT3zWYi8KO2YPBVLOTi3WEw63nKxOvwQ/7tuH2k6vR9BvZ6nt7sdQ0m9TH6JvAzqrDr4CGa8QiGIvBmbrTuaPB27z2axPEB8nrwU5i8875EYPUNVhzxL/5i85fiCvCrQtzwjrx49uR/QuVBhY7z4Y2m74cynPI+BO7wk3DQ9Bh/2usemTbxBO2K8LUsWPMA3Cb0gcrm7ydnlO1UuRjuBJtQ80aDfuPO2FLzWbgK8YUzLPJYmnby2jJE86HaXu50VgrxAxlq8CxHfvLLEGblDiD+7jn4rvG7iWrxrMXk8XoyPu9iA6zzvVVi7Vs0dvV64DjwWAik8dPsmPHMRJDzbnvA6qbNuPZx83rtRjDe7DQecO9J1ajxgZoA8Ykl0vPd+rju0cwO9zjxsPDy2gTsnQmk8+cvyuxqmjDxdhZc8S3Q0PCmC/ryGCWM8e7LqOsvhiDz4LAg8XV5zvBrkfzwyBZQ7y0s8vF7Y1DyGPtS80J8UO54ImLy75PY8cz3YvEBP9Ty5K566BANAPaiiqDtL7PC8fCq+PEy/zrvTibK7RRiZu1CnWbzep+Q8I1YXPdEAWDz4ZD286A0NvZDJuzwUxyg7HKMYPUFoybxvgKa8+NnEu4rsIT1i31o8teZvvMIffDyhVOc8Al/9uwUChLyi9i68eK4muyairruM1OO80uRLvEQXY7uc4L28crY6vD9J7bxBuFa7pDfou4qImbxGxyS8Xuitu8At/zz+Riq69aQ2PVV987xNTUM8FoQ0PE/wuTwcOyO89N7GOiSBqjwQ2oU4Mda2PHv1Fjus6oe84rWTOwlFYjy50jK8B3KPO6EV8jtndIC8Lay6PCGeBjxlH7y8sRB1vCnYTzyw4te7KMkVPR5dAb2Btw08bLvOvGrB87xsZ5s8ewWfPCdFjrvU73I8rhYPOyLjNDx62HY9lFZuOrDKJbzl4u67Rja8u/XE8LsMp2m8RvlSvHX8nLxmMGA8WsJAO3YcYLw9fBw8g4PmOoUUWrv9iD48qGFHPMwLubuftHy8qogpvNSdzLqZZ6m7bkAPPJxNyTpKpom5itAkOxZKGTwIi5o876QvPJSMUbzYD4Y78HQTvGsAhzyrYja9Xjfhu8ZZobzy6aE8yz0sPbPGnTmX1qi7x4IMvQeqqLzwJvW7b+hEPahlgjuO0QC9F+4jPOwYDzzQAmC8QaNsvEUVIrw9C928jZlWO9i6Ab2G06e6dp5NvMHyi7ykoqc66vkbvNZD+Lu+qRk8nuPsvEZiyzs9GcM7QEL5vOUHpbwqsCs8PrUIvD/GfbwTl6A7ujKkPHUpGDxqT4y8jh4bvFKPiTxAy748XrqEPGF9vTtVCGs8svDXPELCnjycxZC8RO9yPG5vILz32sk7QSAHPT04izulpaE8n8YLvMc7Cj2w0uC4s49cvOIQwzw7Ws48tYEHPU4S1bwSdHK8LhtwvOp8P7wsTks8m6OFu3knarwDQz+9/wAkPfnxijzAOEu82D+6u1st47rDKKc8EtCcPDzvXjx9oRg8f428PGLnaTpHkI88mQ2lPIN6WTwlFdC8dQcLPCu/XrwTU467M4BLutI5Yrrhs/s61yE3PMHw0TyMJxM8HFmwPHBkNrxMtJQ6sykSPBWWC7wFKoa8O2P7vFbspLyYhQG9gSElvPHhr7y7GQq8EmKDutCp1TtmQW48aA7dvPOtVzs31rg8/k0LPU+ggDpogLA7mtOdvOn1Dj2xRBS8meyEO2k6fzxdNoa87uqTvP01bTzqEig8fb12O4uYuLrEqII80CmSu3tqELxt92k8c5fNPFI/a7xcVPK8I9a8PCypVj3Y0qq6tVcNvTEAE706lpe8TJyEPIUsrbxTyKK8Ki1GPBIpJjxeV2E7EFHIvDV497sQE/G7v8K9u7johjxI4JI8wMikvOF+5rsDgp889RA8u9LuCbxNGSq8eyoBPFkokjx90AQ9H8jQPAX8JD0Q/BW7dMU/vAViI71NZpE8W7gMvQZjmbwm2mO87wnuOdLezTtqZro87q8MO71onryPjTS8m8oNPc2uQTxuZYw7+wGVO0PgjLoiGAU8lpKbPMbVpbuEjFm8Wy/dugbE+jsLlKw77Jc4PDsuwLxZgjC8i1Pgu47InzxWsQS9WDJKvP4fJLxwELS7akYgvDW8iLsMrPM8UcFrPKe54LyBTZM7KYWmvBITtDysKwc96jjzOxbb8zsis1m9Av54u3TEl7wKkh68mQq2PEDefTzoSX88uRDxvDuOL7w28ps7hfdQPQ01FzyBXIO8v0jrugRFMr2CTNW8vhI5vP3AvLwEOHy8nZ0BPLl297ydfC29FOLHPKoM7rz2wK48saPzuzpVUTum4Mo7omSwPDy9V7vgViq8rIWCPG7POLue2uU6n4Tqu0SAybvak4a8MU9svcHIFbz+Akm6kNaCPJCaBTxIa4Q8tzTnO9RDWDxsoWC8EHmjOyKKy7vwXRo9Th52vJ6KPjtWQj66jG34uzB6mroLTNK8qgbWPKtoBrxNyoE829H+OnC9jjzRw5Y8ZYlFvLUED7xS+PM8Onbpu6CwxzznbCe8CPsHPSEIJjspBDe8DXogvaP35TyfrQM9CHIxPGbKbbzOluw7LsmCvBQCbjpDuKC80/OKvEBtMDxhK4M7/hiqvDp2OT18Pwe8csoQPFc7G70Ik2e7uM1JPAYmQDy4hDC8doncu3S+6TsMXJI8ZN8DuxEOfrxHrTM7bXJHvLqghDzG9fe7cpWKPMmvirzWSwQ7PxM2PI010DuLaFm8aWTBO5X3ET0LigU4O7mTvO7lh7lIBii9lpzyvKJIyDx8Lke8WhhSPKkpYLpR8oi7vLE5O+/uWTwbNfW8tFrqvFAXETz+jcm86ENivJlcOLsGkRQ743Z6PLc4zjsydc68DzvkPFJLybw6OLe8JQ7NPCHwrjxZwco4MScZPZFofDyBp227UEH8u8610TyIuec8cuM3vDw1CD1K4I+8cuihu/+C9zyk9Mk7goDDPM5siLwML228EUchvOj+ML1lnjI9vY8nvHf+q7xZaAG9tTppOTI0eDzKnEC9gCTiulz7gDuLAQc8KDlKPBSNfzxd1T06aHPOPMDRarwJK0083JxZucIOlzvq8mo8j6/HOR1eHLzgdC48I68wvCkgkrxZyTC804BoPdF7kzy7iky8YbpBvJzIurwpcKG8LcOmvJQudbv6brs7WAYQvXLeTTw3LI67g19fvGmALbzcMW+8iikpPBHLzLwGY+y7SodUu0wcUbxBxhi9PCx6ujp/Izwyrum8YEgavGI6Hzwmj467TOfTu1lzQbvjVC28r1t4u5ifHT2GEKK8BNVxPAC9BL3lRYi82QVNvFTDDTyfdQq9/oLcPGjmozwKikA8Z2m8PAaGIjzuF3i8nF3wvKDV/LyxRxo786dWPGfwvTzF6da7xYSYOjCOdjvEwhE5odoDvPlqibwtcRM7eX2+PIV/gDyOBO07baHcvDSKRTxFtQy8JlT5PMhmNz0pCV08txWKvD/aNT1TTwA9e4DMvO0whTuyQjm9/7ZWu1HWK7u+lFg8KbJ7PHUyBLzT6S28bgPSvALx5TwkSea7HE3jO6AGPruljkm7QgVSu7X9BD3rilU8CynHu8fSZrvLX2Y8MI/ruzAUiL3SdYy7Xb2JO4MzmjwqkMU8lsntuxKevrw03Ba8OZdhu5ZSzLtQhyW99Mbiu2KkhztQoue89ITlPOwekjwGfoi8j3WHOimPyzsLeOs8pBxXvDy1+Dy4lIS8QJ+HvC0Shzx4lNK8ivlnu81yLrwBoAK7EvMGvaLV8rosyjy8alXlPBUVerzT70U8HUk2PFBDkby9/jW9lOCTPMfRuLiTnTY8lKjGO8rCET2YowG8D6zpPJZaMrzcufM8wzCfO7XPKb0nPHQ8f2ERO9g9dj01M1e7uYoMPGwbDzx5XPu7XRsHOsMomzzYHyY9NY0QPDRe4LxC5Ly8W7aBO1KSgbuQ9yw8XhQ6vIXRHrzXU/K7cckBve9C97suxee8hKXdvK7N6LxGzMG8OtwKu/4e0zgMX6k8h4EgOtQBNzykwNy8H2ZgO6UMrzrb/j48oqk3ukr00LygjMy6wmtJPBvbMT03VEm63RcSvEHQOTzHfNG8oJLJO/7g1rzx9GY5wbK6u76gAbwH86Q7PFtCOpa5XLwUnMy8BT/tu3KyzbzFYQ+6uaiAPDWXmTyTdlM6MeQwvMGWNjwqVaC7bg96PDNCrzuRCV88SV6SukZr3zz2yF+9v0T6OtZ8WbvQGaK8O1nIvF4hE71FTLS8pCu8PFR1ibtMxJU8/t8WPW+gELzoJiA8nsd3PIV0IzyX/6Q8JMfDO2jdjLq/LEc8jx1jPfdp9bpwrNC84MKdvGp8vzx7fjE8txWiutnEzbpa9so8uMFAOwtRU7vXCFi84QkEvCzbtLy0CUQ8dg5HveLOKrzTBxw70CkYPIIa7bhH0m68CQWQufG4KD0Wpcy790zqPGIK4jyPLsu8g7qLu/Hf6DyCHrU70p4QvPeSQz3wyqS8UnUOPAmNUzx+iH48PHIIPJ2axTwQUMI8LFmBPL39eTtVTJA7s3a+uwuJHrzU/6S8OVqwOwiDwzs1N+s7K+Tvu4oygzwf58Y8P+eJvF4v5DvkrTM7LHUmPf7MfDsGwcw8jHaIPDj8Gr2YzhM8bggAuc75FjzKqfG65h66vFQ2pbykvBS8epWcvII+FL2UxG88F0KCu0eAfzw5+lk8P84kPOCrgDxbFiS8tvDmvP5lWb1MBoA7grgyvPz1Iz1hSwy9WOOUPAUlwLxIbxI7TDUFO0P9trtlaXs8Nl+tvIhVjTuBrC88k7e1O3nxwbthixO9XhDpPARoI7xhJBm8flGAPAg64zzjrsa7wJ3pPBNHCL0E0Ge8B4BRPZcPHjt2+bu7Atd1vAI/17w0i908nyjDvDqtoDz9uBo8cEffuji8r7u25Qe9WAGbPND4Hz1d71w6RQ0FvfY4eLxm6Xu8ugieuryM1ryVtl28GRVjPFTGO73Fa4E8vrWVvHYsWr19NHC7jLxSO8t6wDtD+2s6NNmpvBhWkrt06P47ZQTAvDfKsrsg/2I7FeLnvKSI5Dz/IK08V5o2PMI6DzzvDbW6dbmgOttugTv5rhI7v13GPAWFAL3a9ha9RzsWvSjdFbxUpqy8niXKu0A4ijuNf626Ct7ZPN9o9bpjfgK987N/PKkyibu+Eac85L5Tu+mtrLxWVhM8ixkVuqrhDDtM2aK8W8dtvKmFrjxciRK86RBSu9+F7TxPXB29hs5FvNW4Nj2qEkK9u8vmOlufMLy4YJO8s/gIuhJFyDx8UNS7PxjBvPsCibx0LPy8rJwQPCFO9bx71IA8zTD7u0CN8zzWXJa8leaCvKvXvLwWKce8w2uZu70GcTyWsPm8KLF6vOudUjzqBHI8R959PKGo3rwgoOO6MbbCPJNhgzphOqc8fQRaPKxxdzwaw9m72Q5uvFP377wdrD+80KNnPC6VdjzwlYO8ewxyuuYP4TstjcY82jpDPEKFhzwOj8i7ANN1vGbDnbtSdcu8k4qzu9mTCrwwB5w79jISPcflhLpENbg7cFWJPGKejjyOHFk7s6DRvHUwjbz3eNQ8KbyHux+bi7txoPE7UC16O4DRzrxrSr07OPB4ul2lUbz/MoI8xOQbvOapyDyLUMq7a94TvJ8Mdjx/Dbm8YPC8PAUdErx7DRE7p7mSvHmKgTxqWya7Y98BPd6xBj1i+/S7Q4bUPImp0jxkaCo8tSO0PC46krwFy9e7izwTu7j3E7waksY7kmsYveCWkbwQ8oU6TbzEOwVXy7upnoe7DjIcOhGczTwZ/Le8RHc9PKy1e7w8x5G80UaqO2UmhLtS4R68j+lHvOPc3zwC7Si8FCKxvEv+qbyRGhw6A2r5umAIijwTAiG7b+JVvOR+J71XFuo8drTvuyR7jLtuBd86PAGNO2bKQDvaaA49hQ0fOwPGxDu/TB28lrlpO6Nl0btQ8qg8zFcFPV1bCDxMJaS80BnJuytGk7w5OKo8qWq4vDgqN7qX5ba8xMGIvI3ASzy6uM68oZYJPDzmbLspC7+7vXueu72G8Ly30+o8rkRCvCZfIrtk77k8MJR0vBQp0TvHG0w7AWobPffErjzOeOs7MttBvcsvTLqjQuU8d+CCPOVvsDzlu6a7l52KOqaqgDsOSA27MRXzuysLx7xls5o8n/iNPHKukDvyZ4A7/sH3u4qoEzysOOs7H8usvDR9sDvHrvq8UoruuiF7dDxvmFg8T7EMPUp5rjyYVsy7ODbbvL6nAb03pAe9qAsZvaPvpDxi/a45sbD3u5ARZzskugU9sbqXvOApl7nr6HM8DxhUPBEQYDyZOku8M1KhvLeYFTxg7uy8mGHQvC+YtbwmHAi9bhpDumH+87xroYw8K+azPFif5jtfSNC8oNLyOq3hcjwt3c07/DuqPGGj0DtWuCg7tM2cusitBzzPnaG8RpMUu0m+Nz0i70U8Xte8PBW4LrxWGvA8oFMIPO2q3TttXC48rsbeO50sW7xQHya8iy+IO4KowTqVZQu8yrJtO0EUtjy4IGM8bZLBPApPAzwL/va65ijsvOT1r7wr/nO8HTQbPEjImbvjHYq7grhUvZ5fWLy/oDo8VTMSu4T9CT1FYDo6OqzKu1DoCLzu6vs88r0MvGMXuTxopz27j1i7vPVKCz05/rm6xWg/vBEVDjwqdXS8RTMhPKQHozwc0n46uAn6vCQGKLy6GPo7yiboO9xR/Tzqchi8nsxMus5NYzskXqS8IUx8vGjejjtmRz88X6lYPMy9IDxdpRa9QXDnPAO0P7vhWJ87d5TdO5/3Aj3Fm3A8shTqvEi4BrzqJAW90D4KvFlQ/rwW5Qi9VPeBvOhRHry66K06g4fMO/IzRjy6pse8MAdvPPbD1rlv16g6zF9iOz6vwLvxF/y8pEnPPPVak7wAz1M8/TnGudGluLrhu2q6b8GmOw/bcDxPtbo8t8WNPDxazTyIkpi8tv2tvNC4kzxQtE48UzJAPHcghbuwDcs8qsArPBA4V7zSXgA9gKUyPASR+Dw5yvI8lXWhvFiogLxN42684aHFPI+TsrsgUk26w8i3vO3etryhT9I7o6I5vYZpHzxYmec5q9XaPFoUjjwnlZe77EU6u97gDz2dYx68t1NZPJMztjzJq6Y79wN3PIi3r7wVaY68N9ksvJJAhbtUEwE8AenWvP/WFL2GoJc80fc5vdus0rvuq9k7IFW2vOscubxE4SM9AYiIvKaAGzwB6Lw79p80PHKnsrxMve+8IEG1unCvVry+8HE8u/yEPEhNPzvdnsa7Pmr6PEq2sTzN4T+5gR4zvOYc4Dv2Nx08qSOMu9Xo6Ls21FO8r6niO+YgX7tIPps6ErrevLOydjst8S27IIJ+OiwLj7wf6cA7KXWBPEf97zwY6CA8MsVuPMbM3Lwd2ZU8ZCd9vEHys7tdxIW5ZR00PF9y6jxVz6u8mZtkPPxu1zq/w/q8tBWBPGzXKDxiR0Q7kI+lOyf03Lx7V5q6l+UuuusuELw8Gcu7EZ/zvKKQzTxsf5m8GF4RvG1FhrzxXhw9tQCaPFSJoznKl0S86zSePC29rbyEIL46JSZhPKrrx7vBbvy728zCvLMjkzxwhQK8Sy2NPIZGVbxIjxq8wiq3vF9fEr10VXG8PAR3PMi4+TtgFKI82iLKOwT/jbwxdZa77AzpO76rjTktVd+717xOPDk/LryANmS8nkc6uwWYVbqNrUg7dHwlPMAA17wwHxw8msNoO5jZf7wczU48slzYPCHHqzw+tLE7Zg6RPLV8q7xkmIS8z9VYvBc/GTs1lLy7zykLPOKssbyXDLO8TH7gummH8juPu5c5YiOIPHb8HL23zxY7bdSevGvltrvJKdI778T2O0REXjjKkci7tfzoO0mhHTwVxk28D/8huw== - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 2 - total_tokens: 2 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '8424' - 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. - - IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT - - from haiku.rag import search ✗ WRONG - will fail - - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): - - ## Available Functions - - ### 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 - - ### 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 - - ### 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. - - ### 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. - - ### 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: `if 'documents' in dir(): ...` - - ## Available Python Features - - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. - - Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. - - For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. - 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. - 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). - 7. **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 = list_documents(limit=100) - count = 0 - for doc in docs: - content = 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 llm() - ```python - numbers = [] - results = search("financial data", limit=20) - for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") - for part in extracted.split(','): - part = part.strip().replace(',', '') - if part.isdigit(): - numbers.append(int(part)) - if numbers: - print(f"Average: {sum(numbers) / len(numbers)}") - ``` - - ### Using search results with get_chunk for citations - ```python - results = search("safety requirements", limit=5) - for r in results: - chunk = get_chunk(r['chunk_id']) - print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") - ``` - - ### Using llm() for classification - ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") - print(sentiment) - ``` - - ## Workflow - - 1. **ALWAYS start by using execute_code** to explore the knowledge base - 2. Run multiple code blocks as needed to gather information - 3. After collecting data, provide your final answer - - ## Output Format - - CRITICAL: 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": "..."} - - CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. - role: system - - content: Search for content about animals and tell me which document it came from. - role: user - - content: null - reasoning: We need to search. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' - name: execute_code - id: call_tpl0j81u - type: function - - content: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], r[''chunk_id''], - r[''page_numbers''][:5])\n","stdout":"Animal Facts eb0e1e92-35b2-4f5e-8011-2a67f3dadf00 []\n","stderr":"","success":true}' - role: tool - tool_call_id: call_tpl0j81u - - content: null - reasoning: Only one result? Let's list maybe more. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results = search(\"animals\", limit=20)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' - name: execute_code - id: call_orialuab - type: function - - content: '{"code":"results = search(\"animals\", limit=20)\nfor r in results:\n print(r[''document_title''], r[''chunk_id''], - r[''page_numbers''][:5])\n","stdout":"Animal Facts eb0e1e92-35b2-4f5e-8011-2a67f3dadf00 []\n","stderr":"","success":true}' - role: tool - tool_call_id: call_orialuab - model: gpt-oss - reasoning_effort: low - stream: false - 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 - - function: - description: Result from RLM agent execution. - name: final_result - parameters: - 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: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '583' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: Only that document returned. Let's get chunk details. - role: assistant - tool_calls: - - function: - arguments: '{"code":"chunk = get_chunk(''eb0e1e92-35b2-4f5e-8011-2a67f3dadf00'')\nprint(chunk[''content''][:200])\n"}' - name: execute_code - id: call_hgxkkg8j - index: 0 - type: function - created: 1771336727 - id: chatcmpl-276 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 78 - prompt_tokens: 1896 - total_tokens: 1974 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '9004' - 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. - - IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT - - from haiku.rag import search ✗ WRONG - will fail - - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): - - ## Available Functions - - ### 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 - - ### 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 - - ### 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. - - ### 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. - - ### 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: `if 'documents' in dir(): ...` - - ## Available Python Features - - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. - - Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. - - For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. - 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. - 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). - 7. **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 = list_documents(limit=100) - count = 0 - for doc in docs: - content = 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 llm() - ```python - numbers = [] - results = search("financial data", limit=20) - for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") - for part in extracted.split(','): - part = part.strip().replace(',', '') - if part.isdigit(): - numbers.append(int(part)) - if numbers: - print(f"Average: {sum(numbers) / len(numbers)}") - ``` - - ### Using search results with get_chunk for citations - ```python - results = search("safety requirements", limit=5) - for r in results: - chunk = get_chunk(r['chunk_id']) - print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") - ``` - - ### Using llm() for classification - ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") - print(sentiment) - ``` - - ## Workflow - - 1. **ALWAYS start by using execute_code** to explore the knowledge base - 2. Run multiple code blocks as needed to gather information - 3. After collecting data, provide your final answer - - ## Output Format - - CRITICAL: 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": "..."} - - CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. - role: system - - content: Search for content about animals and tell me which document it came from. - role: user - - content: null - reasoning: We need to search. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' - name: execute_code - id: call_tpl0j81u - type: function - - content: '{"code":"results = search(\"animals\", limit=10)\nfor r in results:\n print(r[''document_title''], r[''chunk_id''], - r[''page_numbers''][:5])\n","stdout":"Animal Facts eb0e1e92-35b2-4f5e-8011-2a67f3dadf00 []\n","stderr":"","success":true}' - role: tool - tool_call_id: call_tpl0j81u - - content: null - reasoning: Only one result? Let's list maybe more. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results = search(\"animals\", limit=20)\nfor r in results:\n print(r[''document_title''], - r[''chunk_id''], r[''page_numbers''][:5])\n"}' - name: execute_code - id: call_orialuab - type: function - - content: '{"code":"results = search(\"animals\", limit=20)\nfor r in results:\n print(r[''document_title''], r[''chunk_id''], - r[''page_numbers''][:5])\n","stdout":"Animal Facts eb0e1e92-35b2-4f5e-8011-2a67f3dadf00 []\n","stderr":"","success":true}' - role: tool - tool_call_id: call_orialuab - - content: null - reasoning: Only that document returned. Let's get chunk details. - role: assistant - tool_calls: - - function: - arguments: '{"code":"chunk = get_chunk(''eb0e1e92-35b2-4f5e-8011-2a67f3dadf00'')\nprint(chunk[''content''][:200])\n"}' - name: execute_code - id: call_hgxkkg8j - type: function - - content: '{"code":"chunk = get_chunk(''eb0e1e92-35b2-4f5e-8011-2a67f3dadf00'')\nprint(chunk[''content''][:200])\n","stdout":"The - quick brown fox jumps over the lazy dog.\n","stderr":"","success":true}' - role: tool - tool_call_id: call_hgxkkg8j - model: gpt-oss - reasoning_effort: low - stream: false - 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 - - function: - description: Result from RLM agent execution. - name: final_result - parameters: - 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: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '1164' + - '888' content-type: - application/json parsed_body: @@ -1082,24 +524,21 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"The search for the term \"animals\" returned content from a single document titled **\"Animal - Facts\"** (document ID: eb0e1e92-35b2-4f5e-8011-2a67f3dadf00). The retrieved chunk contains the sentence: \"The - quick brown fox jumps over the lazy dog.\".","program":"# Search for the term \"animals\"\nresults = search(\"animals\", - limit=20)\n# Print document titles and chunk IDs for reference\nfor r in results:\n print(f\"Document: {r[''document_title'']}, - Chunk ID: {r[''chunk_id'']}\")\n\n# Retrieve the first (and only) chunk from the search results\nchunk = get_chunk(results[0][''chunk_id''])\nprint(\"\\n--- - Chunk Content ---\")\nprint(chunk[''content''][:200])\n# The chunk content is the source of the animal-related - text.\n"}' - reasoning: 'Content is placeholder. So answer: document "Animal Facts". Provide code.' + content: '{"answer":"The document containing content about animals is \"Animal Facts\" (document ID: ee57cfe6-fe82-4162-afd9-f206002ae08e).","program":"import + asyncio\n\nasync def main():\n results = await search(\"animals\", limit=10)\n if results:\n chunk + = results[0]\n print(f\"Document: {chunk[''document_title'']} (ID: {chunk[''document_id'']})\")\n else:\n print(\"No + animal-related content found.\")\n\nasyncio.run(main())"}' + reasoning: It found one chunk. We need to give answer with source. Provide program that searches and prints answer. role: assistant - created: 1771336733 - id: chatcmpl-589 + created: 1771924525 + id: chatcmpl-109 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 240 - prompt_tokens: 2054 - total_tokens: 2294 + completion_tokens: 168 + prompt_tokens: 1822 + total_tokens: 1990 status: code: 200 message: OK diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_semantic_analysis_with_llm.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_semantic_analysis_with_llm.yaml index 0141f655..375c1602 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_semantic_analysis_with_llm.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_semantic_analysis_with_llm.yaml @@ -131,7 +131,7 @@ interactions: connection: - keep-alive content-length: - - '7145' + - '7387' content-type: - application/json host: @@ -144,32 +144,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -186,7 +187,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -195,21 +196,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -219,9 +220,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -232,16 +233,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -317,7 +318,7 @@ interactions: response: headers: content-length: - - '636' + - '677' content-type: - application/json parsed_body: @@ -326,25 +327,25 @@ interactions: index: 0 message: content: '' - reasoning: Need to find quarterly updates documents. Search. + reasoning: Need to search for quarterly updates. Let's search "quarterly update". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n"}' + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n"}' name: execute_code - id: call_19rh51up + id: call_dwvve4zt index: 0 type: function - created: 1771336739 - id: chatcmpl-862 + created: 1771924530 + id: chatcmpl-596 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 71 - prompt_tokens: 1573 - total_tokens: 1644 + completion_tokens: 84 + prompt_tokens: 1634 + total_tokens: 1718 status: code: 200 message: OK @@ -397,7 +398,7 @@ interactions: connection: - keep-alive content-length: - - '7837' + - '8302' content-type: - application/json host: @@ -410,32 +411,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -452,7 +454,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -461,21 +463,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -485,9 +487,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -498,16 +500,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -534,19 +536,21 @@ interactions: - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? role: user - content: null - reasoning: Need to find quarterly updates documents. Search. + reasoning: Need to search for quarterly updates. Let's search "quarterly update". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n"}' + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n"}' name: execute_code - id: call_19rh51up + id: call_dwvve4zt type: function - - content: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n","stdout":"3\nQ3 Update []\nQ2 Update []\nQ1 Update []\n","stderr":"","success":true}' + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n","stdout":"3\nQ3 Update d651d341-7708-4ff3-9d4b-54bcd41694bf 0.032786883413791656\nQ2 + Update f7a3ff56-4f07-479b-8320-62a3b6a2bf48 0.032258063554763794\nQ1 Update 6797ade9-2418-4487-a3ee-bcebb62b9cf9 + 0.01587301678955555\n","stderr":"","success":true}' role: tool - tool_call_id: call_19rh51up + tool_call_id: call_dwvve4zt model: gpt-oss reasoning_effort: low stream: false @@ -597,7 +601,7 @@ interactions: response: headers: content-length: - - '884' + - '707' content-type: - application/json parsed_body: @@ -606,27 +610,25 @@ interactions: index: 0 message: content: '' - reasoning: We have three documents. Use get_document. Need sentiment. Use llm. + reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. role: assistant tool_calls: - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in - docs:\n content = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment - of this quarterly update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] - = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n"}' + arguments: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n"}' name: execute_code - id: call_5w95m1y7 + id: call_juxb6n6o index: 0 type: function - created: 1771336741 - id: chatcmpl-639 + created: 1771924532 + id: chatcmpl-716 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 141 - prompt_tokens: 1727 - total_tokens: 1868 + completion_tokens: 97 + prompt_tokens: 1901 + total_tokens: 1998 status: code: 200 message: OK @@ -639,7 +641,7 @@ interactions: connection: - keep-alive content-length: - - '9000' + - '9074' content-type: - application/json host: @@ -652,32 +654,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -694,7 +697,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -703,21 +706,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -727,9 +730,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -740,16 +743,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -776,36 +779,35 @@ interactions: - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? role: user - content: null - reasoning: Need to find quarterly updates documents. Search. + reasoning: Need to search for quarterly updates. Let's search "quarterly update". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n"}' + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n"}' name: execute_code - id: call_19rh51up + id: call_dwvve4zt type: function - - content: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n","stdout":"3\nQ3 Update []\nQ2 Update []\nQ1 Update []\n","stderr":"","success":true}' + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n","stdout":"3\nQ3 Update d651d341-7708-4ff3-9d4b-54bcd41694bf 0.032786883413791656\nQ2 + Update f7a3ff56-4f07-479b-8320-62a3b6a2bf48 0.032258063554763794\nQ1 Update 6797ade9-2418-4487-a3ee-bcebb62b9cf9 + 0.01587301678955555\n","stderr":"","success":true}' role: tool - tool_call_id: call_19rh51up + tool_call_id: call_dwvve4zt - content: null - reasoning: We have three documents. Use get_document. Need sentiment. Use llm. + reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. role: assistant tool_calls: - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in docs:\n content - = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment of this quarterly - update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n"}' + arguments: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n"}' name: execute_code - id: call_5w95m1y7 + id: call_juxb6n6o type: function - - content: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in docs:\n content - = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment of this quarterly - update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n","stdout":"","stderr":"ModuleNotFoundError: - No module named ''json''","success":false}' + - content: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n","stdout":"","stderr":"NameError: name ''results'' is not defined","success":false}' role: tool - tool_call_id: call_5w95m1y7 + tool_call_id: call_juxb6n6o model: gpt-oss reasoning_effort: low stream: false @@ -856,7 +858,7 @@ interactions: response: headers: content-length: - - '963' + - '762' content-type: - application/json parsed_body: @@ -865,28 +867,25 @@ interactions: index: 0 message: content: '' - reasoning: json module not allowed? It said imports except json allowed. Actually json is allowed. But error? maybe - 'json' not imported? In environment we can do import json. Let's try without import? Use str conversion. We'll - just print. + reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. role: assistant tool_calls: - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nres = {}\nfor d in docs:\n c = get_document(d)\n if - c:\n s = llm(f\"Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', - or ''mixed'': {c[:2000]}\")\n res[d] = s.strip().lower()\nprint(res)\n"}' + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n"}' name: execute_code - id: call_1phm2kez + id: call_7m5eb6ho index: 0 type: function - created: 1771336745 - id: chatcmpl-227 + created: 1771924534 + id: chatcmpl-681 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 163 - prompt_tokens: 2005 - total_tokens: 2168 + completion_tokens: 102 + prompt_tokens: 2076 + total_tokens: 2178 status: code: 200 message: OK @@ -899,7 +898,47 @@ interactions: connection: - keep-alive content-length: - - '348' + - '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: + - '9968' content-type: - application/json host: @@ -907,37 +946,259 @@ interactions: method: POST parsed_body: messages: - - content: 'Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', or ''mixed'': The - new product launch exceeded expectations. Sales grew 40% and customer feedback has been overwhelmingly positive. - Team morale is at an all-time high.' + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: 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: Need to search for quarterly updates. Let's search "quarterly update". + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n"}' + name: execute_code + id: call_dwvve4zt + type: function + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n","stdout":"3\nQ3 Update d651d341-7708-4ff3-9d4b-54bcd41694bf 0.032786883413791656\nQ2 + Update f7a3ff56-4f07-479b-8320-62a3b6a2bf48 0.032258063554763794\nQ1 Update 6797ade9-2418-4487-a3ee-bcebb62b9cf9 + 0.01587301678955555\n","stderr":"","success":true}' + role: tool + tool_call_id: call_dwvve4zt + - content: null + reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n"}' + name: execute_code + id: call_juxb6n6o + type: function + - content: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n","stdout":"","stderr":"NameError: name ''results'' is not defined","success":false}' + role: tool + tool_call_id: call_juxb6n6o + - content: null + reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n"}' + name: execute_code + id: call_7m5eb6ho + type: function + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n","stdout":"Q3 + Update 168\nQ2 Update 161\nQ1 Update 152\n","stderr":"","success":true}' + role: tool + tool_call_id: call_7m5eb6ho model: gpt-oss reasoning_effort: low stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function uri: http://localhost:11434/v1/chat/completions response: headers: content-length: - - '335' + - '783' content-type: - application/json parsed_body: choices: - - finish_reason: stop + - finish_reason: tool_calls index: 0 message: - content: '**Sentiment:** Positive' - reasoning: All positive. + content: '' + reasoning: Now classify sentiment. role: assistant - created: 1771336746 - id: chatcmpl-350 + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of + the following quarterly update as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n"}' + name: execute_code + id: call_g7dz17hi + index: 0 + type: function + created: 1771924537 + id: chatcmpl-853 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 18 - prompt_tokens: 116 - total_tokens: 134 + completion_tokens: 102 + prompt_tokens: 2278 + total_tokens: 2380 status: code: 200 message: OK @@ -950,7 +1211,7 @@ interactions: connection: - keep-alive content-length: - - '357' + - '10969' content-type: - application/json host: @@ -958,37 +1219,278 @@ interactions: method: POST parsed_body: messages: - - content: 'Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', or ''mixed'': 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.' + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: 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: Need to search for quarterly updates. Let's search "quarterly update". + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n"}' + name: execute_code + id: call_dwvve4zt + type: function + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n","stdout":"3\nQ3 Update d651d341-7708-4ff3-9d4b-54bcd41694bf 0.032786883413791656\nQ2 + Update f7a3ff56-4f07-479b-8320-62a3b6a2bf48 0.032258063554763794\nQ1 Update 6797ade9-2418-4487-a3ee-bcebb62b9cf9 + 0.01587301678955555\n","stderr":"","success":true}' + role: tool + tool_call_id: call_dwvve4zt + - content: null + reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n"}' + name: execute_code + id: call_juxb6n6o + type: function + - content: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n","stdout":"","stderr":"NameError: name ''results'' is not defined","success":false}' + role: tool + tool_call_id: call_juxb6n6o + - content: null + reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n"}' + name: execute_code + id: call_7m5eb6ho + type: function + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n","stdout":"Q3 + Update 168\nQ2 Update 161\nQ1 Update 152\n","stderr":"","success":true}' + role: tool + tool_call_id: call_7m5eb6ho + - content: null + reasoning: Now classify sentiment. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of + the following quarterly update as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n"}' + name: execute_code + id: call_g7dz17hi + type: function + - content: '{"code":"sentiments = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of the following + quarterly update as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n","stdout":"","stderr":"NameError: name ''chunks'' + is not defined","success":false}' + role: tool + tool_call_id: call_g7dz17hi model: gpt-oss reasoning_effort: low stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function uri: http://localhost:11434/v1/chat/completions response: headers: content-length: - - '368' + - '1045' content-type: - application/json parsed_body: choices: - - finish_reason: stop + - finish_reason: tool_calls index: 0 message: - content: '**Sentiment: negative**' - reasoning: We detect negativity. So classify as negative. + content: '' + reasoning: We need to re-define chunks. Let's re-run entire block. role: assistant - created: 1771336747 - id: chatcmpl-663 + tool_calls: + - function: + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n\nsentiments + = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of the following quarterly update + as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n"}' + name: execute_code + id: call_knskt3jd + index: 0 + type: function + created: 1771924541 + id: chatcmpl-263 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 25 - prompt_tokens: 117 - total_tokens: 142 + completion_tokens: 171 + prompt_tokens: 2489 + total_tokens: 2660 status: code: 200 message: OK @@ -1001,7 +1503,47 @@ interactions: connection: - keep-alive content-length: - - '364' + - '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: + - '367' content-type: - application/json host: @@ -1009,7 +1551,7 @@ interactions: method: POST parsed_body: messages: - - content: 'Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', or ''mixed'': Mixed + - content: 'Classify the overall sentiment of the following quarterly update as positive, negative, or mixed: Mixed results this quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year but customer retention increased.' role: user @@ -1020,7 +1562,7 @@ interactions: response: headers: content-length: - - '394' + - '393' content-type: - application/json parsed_body: @@ -1028,18 +1570,120 @@ interactions: - finish_reason: stop index: 0 message: - content: '**Mixed**' - reasoning: Need to decide sentiment. Mixed results, some positive, some negative. Likely 'mixed'. + content: '**Overall sentiment:** Mixed.' + reasoning: 'We need to classify overall sentiment: mixed. Probably "mixed".' role: assistant - created: 1771336747 - id: chatcmpl-126 + created: 1771924541 + id: chatcmpl-966 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 32 + completion_tokens: 29 + prompt_tokens: 112 + total_tokens: 141 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '360' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: 'Classify the overall sentiment of the following quarterly update as positive, negative, or mixed: 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 + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '361' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: stop + index: 0 + message: + content: '**Negative**' + reasoning: 'We must classify sentiment overall: it''s negative.' + role: assistant + created: 1771924542 + id: chatcmpl-538 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 22 + prompt_tokens: 115 + total_tokens: 137 + 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: 'Classify the overall sentiment of the following quarterly update as positive, negative, or mixed: 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 + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '320' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: stop + index: 0 + message: + content: Positive + reasoning: All positive. + role: assistant + created: 1771924543 + id: chatcmpl-177 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 14 prompt_tokens: 114 - total_tokens: 146 + total_tokens: 128 status: code: 200 message: OK @@ -1052,7 +1696,7 @@ interactions: connection: - keep-alive content-length: - - '10226' + - '12558' content-type: - application/json host: @@ -1065,32 +1709,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -1107,7 +1752,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -1116,21 +1761,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -1140,9 +1785,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -1153,16 +1798,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -1189,55 +1834,88 @@ interactions: - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? role: user - content: null - reasoning: Need to find quarterly updates documents. Search. + reasoning: Need to search for quarterly updates. Let's search "quarterly update". role: assistant tool_calls: - function: - arguments: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n"}' + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n"}' name: execute_code - id: call_19rh51up + id: call_dwvve4zt type: function - - content: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n","stdout":"3\nQ3 Update []\nQ2 Update []\nQ1 Update []\n","stderr":"","success":true}' + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results[:5]:\n print(r[''document_title''], + r[''chunk_id''], r[''score''])\n","stdout":"3\nQ3 Update d651d341-7708-4ff3-9d4b-54bcd41694bf 0.032786883413791656\nQ2 + Update f7a3ff56-4f07-479b-8320-62a3b6a2bf48 0.032258063554763794\nQ1 Update 6797ade9-2418-4487-a3ee-bcebb62b9cf9 + 0.01587301678955555\n","stderr":"","success":true}' role: tool - tool_call_id: call_19rh51up + tool_call_id: call_dwvve4zt - content: null - reasoning: We have three documents. Use get_document. Need sentiment. Use llm. + reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. role: assistant tool_calls: - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in docs:\n content - = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment of this quarterly - update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n"}' + arguments: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n"}' name: execute_code - id: call_5w95m1y7 + id: call_juxb6n6o type: function - - content: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in docs:\n content - = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment of this quarterly - update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n","stdout":"","stderr":"ModuleNotFoundError: - No module named ''json''","success":false}' + - content: '{"code":"chunks = []\nfor r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], + len(chunk[''content'']))\n","stdout":"","stderr":"NameError: name ''results'' is not defined","success":false}' role: tool - tool_call_id: call_5w95m1y7 + tool_call_id: call_juxb6n6o - content: null - reasoning: json module not allowed? It said imports except json allowed. Actually json is allowed. But error? maybe - 'json' not imported? In environment we can do import json. Let's try without import? Use str conversion. We'll just - print. + reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. role: assistant tool_calls: - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nres = {}\nfor d in docs:\n c = get_document(d)\n if - c:\n s = llm(f\"Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', - or ''mixed'': {c[:2000]}\")\n res[d] = s.strip().lower()\nprint(res)\n"}' + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n"}' name: execute_code - id: call_1phm2kez + id: call_7m5eb6ho type: function - - content: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nres = {}\nfor d in docs:\n c = get_document(d)\n if - c:\n s = llm(f\"Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', or - ''mixed'': {c[:2000]}\")\n res[d] = s.strip().lower()\nprint(res)\n","stdout":"{''Q1 Update'': ''**sentiment:** - positive'', ''Q2 Update'': ''**sentiment: negative**'', ''Q3 Update'': ''**mixed**''}\n","stderr":"","success":true}' + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n","stdout":"Q3 + Update 168\nQ2 Update 161\nQ1 Update 152\n","stderr":"","success":true}' role: tool - tool_call_id: call_1phm2kez + tool_call_id: call_7m5eb6ho + - content: null + reasoning: Now classify sentiment. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of + the following quarterly update as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n"}' + name: execute_code + id: call_g7dz17hi + type: function + - content: '{"code":"sentiments = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of the following + quarterly update as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n","stdout":"","stderr":"NameError: name ''chunks'' + is not defined","success":false}' + role: tool + tool_call_id: call_g7dz17hi + - content: null + reasoning: We need to re-define chunks. Let's re-run entire block. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n\nsentiments + = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of the following quarterly update + as positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n"}' + name: execute_code + id: call_knskt3jd + type: function + - content: '{"code":"results = await search(\"quarterly update\", limit=20)\nchunks = []\nfor r in results:\n chunk + = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n print(chunk[''document_title''], len(chunk[''content'']))\n\nsentiments + = {}\nfor chunk in chunks:\n prompt = f\"Classify the overall sentiment of the following quarterly update as + positive, negative, or mixed: {chunk[''content'']}\"\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n print(chunk[''document_title''], resp.strip())\n","stdout":"Q3 Update 168\nQ2 Update 161\nQ1 + Update 152\nQ3 Update **Overall sentiment:** Mixed.\nQ2 Update **Negative**\nQ1 Update Positive\n","stderr":"","success":true}' + role: tool + tool_call_id: call_knskt3jd model: gpt-oss reasoning_effort: low stream: false @@ -1288,300 +1966,7 @@ interactions: response: headers: content-length: - - '753' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: tool_calls - index: 0 - message: - content: '' - reasoning: Parse results. Count. - role: assistant - tool_calls: - - function: - arguments: '{"code":"res={''Q1 Update'': ''**sentiment:** positive'', ''Q2 Update'': ''**sentiment: negative**'', - ''Q3 Update'': ''**mixed**''}\npos=sum(1 for v in res.values() if ''positive'' in v)\nneg=sum(1 for v in res.values() - if ''negative'' in v)\nmix=sum(1 for v in res.values() if ''mixed'' in v)\nprint(pos,neg,mix)\n"}' - name: execute_code - id: call_8y20oq7z - index: 0 - type: function - created: 1771336753 - id: chatcmpl-869 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 125 - prompt_tokens: 2318 - total_tokens: 2443 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '11137' - 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. - - IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT - - from haiku.rag import search ✗ WRONG - will fail - - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): - - ## Available Functions - - ### 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 - - ### 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 - - ### 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. - - ### 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. - - ### 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: `if 'documents' in dir(): ...` - - ## Available Python Features - - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. - - Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. - - For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. - 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. - 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). - 7. **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 = list_documents(limit=100) - count = 0 - for doc in docs: - content = 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 llm() - ```python - numbers = [] - results = search("financial data", limit=20) - for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") - for part in extracted.split(','): - part = part.strip().replace(',', '') - if part.isdigit(): - numbers.append(int(part)) - if numbers: - print(f"Average: {sum(numbers) / len(numbers)}") - ``` - - ### Using search results with get_chunk for citations - ```python - results = search("safety requirements", limit=5) - for r in results: - chunk = get_chunk(r['chunk_id']) - print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") - ``` - - ### Using llm() for classification - ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") - print(sentiment) - ``` - - ## Workflow - - 1. **ALWAYS start by using execute_code** to explore the knowledge base - 2. Run multiple code blocks as needed to gather information - 3. After collecting data, provide your final answer - - ## Output Format - - CRITICAL: 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": "..."} - - CRITICAL: 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: Need to find quarterly updates documents. Search. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n"}' - name: execute_code - id: call_19rh51up - type: function - - content: '{"code":"results = search(\"quarterly update\", limit=20)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], - r[''page_numbers''])\n","stdout":"3\nQ3 Update []\nQ2 Update []\nQ1 Update []\n","stderr":"","success":true}' - role: tool - tool_call_id: call_19rh51up - - content: null - reasoning: We have three documents. Use get_document. Need sentiment. Use llm. - role: assistant - tool_calls: - - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in docs:\n content - = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment of this quarterly - update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n"}' - name: execute_code - id: call_5w95m1y7 - type: function - - content: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nimport json\nresults = {}\nfor d in docs:\n content - = get_document(d)\n if content:\n sentiment = llm(f\"Classify the overall sentiment of this quarterly - update as ''positive'', ''negative'', or ''mixed'': {content[:2000]}\")\n results[d] = sentiment.strip().lower()\nprint(json.dumps(results,indent=2))\n","stdout":"","stderr":"ModuleNotFoundError: - No module named ''json''","success":false}' - role: tool - tool_call_id: call_5w95m1y7 - - content: null - reasoning: json module not allowed? It said imports except json allowed. Actually json is allowed. But error? maybe - 'json' not imported? In environment we can do import json. Let's try without import? Use str conversion. We'll just - print. - role: assistant - tool_calls: - - function: - arguments: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nres = {}\nfor d in docs:\n c = get_document(d)\n if - c:\n s = llm(f\"Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', - or ''mixed'': {c[:2000]}\")\n res[d] = s.strip().lower()\nprint(res)\n"}' - name: execute_code - id: call_1phm2kez - type: function - - content: '{"code":"docs = [''Q1 Update'',''Q2 Update'',''Q3 Update'']\nres = {}\nfor d in docs:\n c = get_document(d)\n if - c:\n s = llm(f\"Classify the overall sentiment of this quarterly update as ''positive'', ''negative'', or - ''mixed'': {c[:2000]}\")\n res[d] = s.strip().lower()\nprint(res)\n","stdout":"{''Q1 Update'': ''**sentiment:** - positive'', ''Q2 Update'': ''**sentiment: negative**'', ''Q3 Update'': ''**mixed**''}\n","stderr":"","success":true}' - role: tool - tool_call_id: call_1phm2kez - - content: null - reasoning: Parse results. Count. - role: assistant - tool_calls: - - function: - arguments: '{"code":"res={''Q1 Update'': ''**sentiment:** positive'', ''Q2 Update'': ''**sentiment: negative**'', - ''Q3 Update'': ''**mixed**''}\npos=sum(1 for v in res.values() if ''positive'' in v)\nneg=sum(1 for v in res.values() - if ''negative'' in v)\nmix=sum(1 for v in res.values() if ''mixed'' in v)\nprint(pos,neg,mix)\n"}' - name: execute_code - id: call_8y20oq7z - type: function - - content: '{"code":"res={''Q1 Update'': ''**sentiment:** positive'', ''Q2 Update'': ''**sentiment: negative**'', ''Q3 - Update'': ''**mixed**''}\npos=sum(1 for v in res.values() if ''positive'' in v)\nneg=sum(1 for v in res.values() - if ''negative'' in v)\nmix=sum(1 for v in res.values() if ''mixed'' in v)\nprint(pos,neg,mix)\n","stdout":"1 1 1\n","stderr":"","success":true}' - role: tool - tool_call_id: call_8y20oq7z - model: gpt-oss - reasoning_effort: low - stream: false - 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 - - function: - description: Result from RLM agent execution. - name: final_result - parameters: - 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: function - uri: http://localhost:11434/v1/chat/completions - response: - headers: - content-length: - - '1239' + - '1912' content-type: - application/json parsed_body: @@ -1589,24 +1974,30 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"Among the three quarterly updates examined, one quarter was classified as positive (Q1 Update), - one as negative (Q2 Update), and one as mixed (Q3 Update).","program":"docs = [''Q1 Update'', ''Q2 Update'', ''Q3 - Update'']\n# Retrieve document contents\ncontents = {}\nfor d in docs:\n c = get_document(d)\n if c:\n contents[d] - = c\n# Classify sentiment using the LLM\nsentiment = {}\nfor title, text in contents.items():\n s = llm(f\"Classify - the overall sentiment of this quarterly update as ''positive'', ''negative'', or ''mixed'': {text[:2000]}\")\n sentiment[title] - = s.strip().lower()\n# Count results\npositive = sum(1 for v in sentiment.values() if ''positive'' in v)\nnegative - = sum(1 for v in sentiment.values() if ''negative'' in v)\nmixed = sum(1 for v in sentiment.values() if ''mixed'' - in v)\nprint(\"Positive:\", positive)\nprint(\"Negative:\", negative)\nprint(\"Mixed:\", mixed)\n"}' + content: '{"answer":"There are three quarterly updates in the knowledge base. After classifying each update, we + find one positive, one negative, and one mixed (neutral) sentiment.","program":"#!/usr/bin/env python3\nimport + json\n\n# RLM workflow to count sentiments of quarterly updates\n\nasync def main():\n # Search for quarterly + updates\n results = await search(\"quarterly update\", limit=20)\n # Retrieve chunk details for each result\n chunks + = []\n for r in results:\n chunk = await get_chunk(r[''chunk_id''])\n chunks.append(chunk)\n # + Classify sentiment of each chunk using the LLM\n sentiments = {}\n for chunk in chunks:\n prompt + = (\n f\"Classify the overall sentiment of the following quarterly update as positive, negative, or + mixed: \"\n f\"{chunk[''content'']}\"\n )\n resp = await llm(prompt)\n sentiments[chunk[''document_title'']] + = resp.strip()\n # Count occurrences of each sentiment category\n counts = {\"positive\":0, \"negative\":0, + \"mixed\":0}\n for s in sentiments.values():\n key = s.lower()\n if key in counts:\n counts[key] + += 1\n else:\n # handle unexpected output\n pass\n # Output results\n print(\"Sentiment + counts:\")\n print(json.dumps(counts, indent=2))\n\n# Run the asynchronous main function\nif __name__ == \"__main__\":\n import + asyncio\n asyncio.run(main())\n"}' + reasoning: 'We have counts: Mixed 1, Negative 1, Positive 1. Provide answer and program.' role: assistant - created: 1771336759 - id: chatcmpl-715 + created: 1771924554 + id: chatcmpl-522 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 262 - prompt_tokens: 2568 - total_tokens: 2830 + completion_tokens: 386 + prompt_tokens: 2859 + total_tokens: 3245 status: code: 200 message: OK diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_filter.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_filter.yaml index 5f4bc8ac..3a9012b8 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_filter.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_filter.yaml @@ -128,7 +128,7 @@ interactions: connection: - keep-alive content-length: - - '7077' + - '7319' content-type: - application/json host: @@ -141,32 +141,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -183,7 +184,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -192,21 +193,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -216,9 +217,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -229,16 +230,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -314,7 +315,7 @@ interactions: response: headers: content-length: - - '526' + - '547' content-type: - application/json parsed_body: @@ -323,24 +324,24 @@ interactions: index: 0 message: content: '' - reasoning: Need to list_documents. + reasoning: Need to list documents. role: assistant tool_calls: - function: - arguments: '{"code":"docs=list_documents(limit=1000);print(len(docs)); print(docs[:3])"}' + arguments: '{"code":"docs = await list_documents(limit=1000, offset=0)\nprint(len(docs))\nprint(docs[:3])"}' name: execute_code - id: call_rpfcy176 + id: call_pu34e9fx index: 0 type: function - created: 1771336720 - id: chatcmpl-390 + created: 1771924517 + id: chatcmpl-236 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 47 - prompt_tokens: 1560 - total_tokens: 1607 + completion_tokens: 56 + prompt_tokens: 1621 + total_tokens: 1677 status: code: 200 message: OK @@ -353,7 +354,7 @@ interactions: connection: - keep-alive content-length: - - '7655' + - '7939' content-type: - application/json host: @@ -366,32 +367,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -408,7 +410,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -417,21 +419,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -441,9 +443,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -454,16 +456,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -490,18 +492,18 @@ interactions: - content: How many documents are available? role: user - content: null - reasoning: Need to list_documents. + reasoning: Need to list documents. role: assistant tool_calls: - function: - arguments: '{"code":"docs=list_documents(limit=1000);print(len(docs)); print(docs[:3])"}' + arguments: '{"code":"docs = await list_documents(limit=1000, offset=0)\nprint(len(docs))\nprint(docs[:3])"}' name: execute_code - id: call_rpfcy176 + id: call_pu34e9fx type: function - - content: '{"code":"docs=list_documents(limit=1000);print(len(docs)); print(docs[:3])","stdout":"1\n[{''id'': ''50c659ee-70c5-4854-a418-12ae2abdcf40'', - ''title'': ''Cats'', ''uri'': None, ''created_at'': ''2026-02-17 15:58:36.831575''}]\n","stderr":"","success":true}' + - content: '{"code":"docs = await list_documents(limit=1000, offset=0)\nprint(len(docs))\nprint(docs[:3])","stdout":"1\n[{''id'': + ''90fce4e5-0d68-4736-acaf-d075e3ed4822'', ''title'': ''Cats'', ''uri'': None, ''created_at'': ''2026-02-24 11:15:14.007278''}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_rpfcy176 + tool_call_id: call_pu34e9fx model: gpt-oss reasoning_effort: low stream: false @@ -552,7 +554,7 @@ interactions: response: headers: content-length: - - '453' + - '704' content-type: - application/json parsed_body: @@ -560,18 +562,20 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"There is 1 document available in the knowledge base.","program":"docs=list_documents(limit=1000)\nprint(f\"Number - of documents: {len(docs)}\")"}' + content: '{"answer":"There is 1 document available in the knowledge base.","program":"# List all documents\nimport + json\nasync def main():\n docs = await list_documents(limit=1000, offset=0)\n print(\"Number of documents:\", + len(docs))\n # Optional: print first few document titles for reference\n for d in docs[:5]:\n print(f\"ID: + {d[''id'']}, Title: {d[''title'']}\")\n\nawait main()"}' role: assistant - created: 1771336721 - id: chatcmpl-97 + created: 1771924519 + id: chatcmpl-487 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 46 - prompt_tokens: 1713 - total_tokens: 1759 + completion_tokens: 117 + prompt_tokens: 1793 + total_tokens: 1910 status: code: 200 message: OK diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_preloaded_documents.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_preloaded_documents.yaml index db862e9d..0a78fe8b 100644 --- a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_preloaded_documents.yaml +++ b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_with_preloaded_documents.yaml @@ -88,7 +88,7 @@ interactions: connection: - keep-alive content-length: - - '7148' + - '7390' content-type: - application/json host: @@ -101,32 +101,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -143,7 +144,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -152,21 +153,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -176,9 +177,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -189,16 +190,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -274,7 +275,7 @@ interactions: response: headers: content-length: - - '615' + - '610' content-type: - application/json parsed_body: @@ -283,25 +284,25 @@ interactions: index: 0 message: content: '' - reasoning: Need to inspect documents variable. + reasoning: Need check documents variable? role: assistant tool_calls: - function: arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' name: execute_code - id: call_9n7burqq + id: call_2ao52bvz index: 0 type: function - created: 1771336791 - id: chatcmpl-463 + created: 1771924591 + id: chatcmpl-850 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 69 - prompt_tokens: 1574 - total_tokens: 1643 + completion_tokens: 68 + prompt_tokens: 1635 + total_tokens: 1703 status: code: 200 message: OK @@ -314,7 +315,7 @@ interactions: connection: - keep-alive content-length: - - '7800' + - '8037' content-type: - application/json host: @@ -327,32 +328,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -369,7 +371,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -378,21 +380,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -402,9 +404,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -415,16 +417,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -451,19 +453,19 @@ interactions: - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? role: user - content: null - reasoning: Need to inspect documents variable. + reasoning: Need check documents variable? role: assistant tool_calls: - function: arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' name: execute_code - id: call_9n7burqq + id: call_2ao52bvz type: function - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' role: tool - tool_call_id: call_9n7burqq + tool_call_id: call_2ao52bvz model: gpt-oss reasoning_effort: low stream: false @@ -514,7 +516,7 @@ interactions: response: headers: content-length: - - '792' + - '524' content-type: - application/json parsed_body: @@ -523,27 +525,24 @@ interactions: index: 0 message: content: '' - reasoning: |+ - I used dir; but dir is builtin. Wait error: name 'dir' is not defined? Actually dir is builtin but not available? Probably need to use locals() or globals()? Let's just check if 'documents' in globals(). - + reasoning: The environment may not provide dir(); use globals(). Check. role: assistant tool_calls: - function: - arguments: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], - len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' + arguments: '{"code":"print(globals().keys())\n"}' name: execute_code - id: call_hiiwsnky + id: call_rjj1hq9z index: 0 type: function - created: 1771336794 - id: chatcmpl-665 + created: 1771924592 + id: chatcmpl-798 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 111 - prompt_tokens: 1717 - total_tokens: 1828 + completion_tokens: 43 + prompt_tokens: 1777 + total_tokens: 1820 status: code: 200 message: OK @@ -556,7 +555,7 @@ interactions: connection: - keep-alive content-length: - - '8636' + - '8486' content-type: - application/json host: @@ -569,32 +568,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -611,7 +611,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -620,21 +620,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -644,9 +644,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -657,16 +657,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -693,35 +693,31 @@ interactions: - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? role: user - content: null - reasoning: Need to inspect documents variable. + reasoning: Need check documents variable? role: assistant tool_calls: - function: arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' name: execute_code - id: call_9n7burqq + id: call_2ao52bvz type: function - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' role: tool - tool_call_id: call_9n7burqq + tool_call_id: call_2ao52bvz - content: null - reasoning: |+ - I used dir; but dir is builtin. Wait error: name 'dir' is not defined? Actually dir is builtin but not available? Probably need to use locals() or globals()? Let's just check if 'documents' in globals(). - + reasoning: The environment may not provide dir(); use globals(). Check. role: assistant tool_calls: - function: - arguments: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], - len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' + arguments: '{"code":"print(globals().keys())\n"}' name: execute_code - id: call_hiiwsnky + id: call_rjj1hq9z type: function - - content: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no - documents'')\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' role: tool - tool_call_id: call_hiiwsnky + tool_call_id: call_rjj1hq9z model: gpt-oss reasoning_effort: low stream: false @@ -772,7 +768,7 @@ interactions: response: headers: content-length: - - '633' + - '581' content-type: - application/json parsed_body: @@ -781,25 +777,25 @@ interactions: index: 0 message: content: '' - reasoning: Globals also not defined. Probably environment only has defined functions. We might just attempt to run - list_documents to see if any loaded. + reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could + search. role: assistant tool_calls: - function: - arguments: '{"code":"docs = list_documents(limit=10)\nprint(''docs'',docs)\n"}' - name: execute_code - id: call_yc5ml52o + arguments: '{"query":"founded mission company","limit":5}' + name: search + id: call_ee8bn4w7 index: 0 type: function - created: 1771336796 - id: chatcmpl-948 + created: 1771924593 + id: chatcmpl-566 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 65 - prompt_tokens: 1902 - total_tokens: 1967 + completion_tokens: 51 + prompt_tokens: 1862 + total_tokens: 1913 status: code: 200 message: OK @@ -812,7 +808,7 @@ interactions: connection: - keep-alive content-length: - - '9458' + - '8970' content-type: - application/json host: @@ -825,32 +821,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -867,7 +864,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -876,21 +873,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -900,9 +897,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -913,16 +910,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -949,50 +946,47 @@ interactions: - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? role: user - content: null - reasoning: Need to inspect documents variable. + reasoning: Need check documents variable? role: assistant tool_calls: - function: arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' name: execute_code - id: call_9n7burqq + id: call_2ao52bvz type: function - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' role: tool - tool_call_id: call_9n7burqq + tool_call_id: call_2ao52bvz - content: null - reasoning: |+ - I used dir; but dir is builtin. Wait error: name 'dir' is not defined? Actually dir is builtin but not available? Probably need to use locals() or globals()? Let's just check if 'documents' in globals(). + reasoning: The environment may not provide dir(); use globals(). Check. + role: assistant + tool_calls: + - function: + arguments: '{"code":"print(globals().keys())\n"}' + name: execute_code + id: call_rjj1hq9z + type: function + - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + role: tool + tool_call_id: call_rjj1hq9z + - content: null + reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could + search. + role: assistant + tool_calls: + - function: + arguments: '{"query":"founded mission company","limit":5}' + name: search + id: call_ee8bn4w7 + type: function + - content: |- + Unknown tool name: 'search'. Available tools: 'final_result', 'execute_code' - role: assistant - tool_calls: - - function: - arguments: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], - len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' - name: execute_code - id: call_hiiwsnky - type: function - - content: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no - documents'')\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + Fix the errors and try again. role: tool - tool_call_id: call_hiiwsnky - - content: null - reasoning: Globals also not defined. Probably environment only has defined functions. We might just attempt to run - list_documents to see if any loaded. - role: assistant - tool_calls: - - function: - arguments: '{"code":"docs = list_documents(limit=10)\nprint(''docs'',docs)\n"}' - name: execute_code - id: call_yc5ml52o - type: function - - content: '{"code":"docs = list_documents(limit=10)\nprint(''docs'',docs)\n","stdout":"docs [{''id'': ''57c6d445-39c8-4eb5-8a5e-b6c2c3096f66'', - ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-02-17 15:59:48.355566''}, {''id'': ''19662d84-5066-402c-a6c1-6eb0e1e1d715'', - ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-02-17 15:59:50.423989''}]\n","stderr":"","success":true}' - role: tool - tool_call_id: call_yc5ml52o + tool_call_id: call_ee8bn4w7 model: gpt-oss reasoning_effort: low stream: false @@ -1043,7 +1037,7 @@ interactions: response: headers: content-length: - - '625' + - '550' content-type: - application/json parsed_body: @@ -1052,27 +1046,24 @@ interactions: index: 0 message: content: '' - reasoning: |+ - Now get these docs. - + reasoning: The tool is execute_code. Use that. role: assistant tool_calls: - function: - arguments: '{"code":"content1 = get_document(''Company History'')\ncontent2 = get_document(''Mission Statement'')\nprint(''History:'', - content1[:200])\nprint(''Mission:'', content2[:200])\n"}' + arguments: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n"}' name: execute_code - id: call_0vdkrldb + id: call_o1c87rz5 index: 0 type: function - created: 1771336798 - id: chatcmpl-879 + created: 1771924595 + id: chatcmpl-37 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 72 - prompt_tokens: 2137 - total_tokens: 2209 + completion_tokens: 51 + prompt_tokens: 1948 + total_tokens: 1999 status: code: 200 message: OK @@ -1085,7 +1076,47 @@ interactions: connection: - keep-alive content-length: - - '10232' + - '93' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - founded mission 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: 5 + total_tokens: 5 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '10069' content-type: - application/json host: @@ -1098,32 +1129,33 @@ interactions: IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just use them directly: - - search("query") ✓ CORRECT + CRITICAL: 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 - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await - You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly, no imports needed): + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): ## Available Functions - ### search(query, limit=10) -> list[dict] + ### 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 - ### list_documents(limit=10, offset=0) -> list[dict] + ### 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 - ### get_document(id_or_title) -> str | None + ### 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. - ### get_chunk(chunk_id) -> dict | None + ### 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. - ### llm(prompt) -> str + ### 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. @@ -1140,7 +1172,7 @@ interactions: ## Available Python Features - The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, try/except, and the `json` module. + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. @@ -1149,21 +1181,21 @@ interactions: ## Strategy Guide 1. **Explore First**: Start by listing documents or searching to understand what's available. Document names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). - 2. **If get_document returns None**: Use `list_documents()` to see actual document titles, or `search()` to find relevant content. + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. - 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use llm(). + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. 7. **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 = list_documents(limit=100) + docs = await list_documents(limit=100) count = 0 for doc in docs: - content = get_document(doc['id']) + content = await get_document(doc['id']) if content and 'keyword' in content.lower(): count += 1 print(f"Found in: {doc['title']}") @@ -1173,9 +1205,9 @@ interactions: ### Extracting data with llm() ```python numbers = [] - results = search("financial data", limit=20) + results = await search("financial data", limit=20) for r in results: - extracted = llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") for part in extracted.split(','): part = part.strip().replace(',', '') if part.isdigit(): @@ -1186,16 +1218,16 @@ interactions: ### Using search results with get_chunk for citations ```python - results = search("safety requirements", limit=5) + results = await search("safety requirements", limit=5) for r in results: - chunk = get_chunk(r['chunk_id']) + chunk = await get_chunk(r['chunk_id']) print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") ``` ### Using llm() for classification ```python - content = get_document("Q1 Report") - sentiment = llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") print(sentiment) ``` @@ -1222,67 +1254,65 @@ interactions: - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? role: user - content: null - reasoning: Need to inspect documents variable. + reasoning: Need check documents variable? role: assistant tool_calls: - function: arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' name: execute_code - id: call_9n7burqq + id: call_2ao52bvz type: function - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' role: tool - tool_call_id: call_9n7burqq + tool_call_id: call_2ao52bvz - content: null - reasoning: |+ - I used dir; but dir is builtin. Wait error: name 'dir' is not defined? Actually dir is builtin but not available? Probably need to use locals() or globals()? Let's just check if 'documents' in globals(). + reasoning: The environment may not provide dir(); use globals(). Check. + role: assistant + tool_calls: + - function: + arguments: '{"code":"print(globals().keys())\n"}' + name: execute_code + id: call_rjj1hq9z + type: function + - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + role: tool + tool_call_id: call_rjj1hq9z + - content: null + reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could + search. + role: assistant + tool_calls: + - function: + arguments: '{"query":"founded mission company","limit":5}' + name: search + id: call_ee8bn4w7 + type: function + - content: |- + Unknown tool name: 'search'. Available tools: 'final_result', 'execute_code' - role: assistant - tool_calls: - - function: - arguments: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], - len(doc[''content'']))\nelse:\n print(''no documents'')\n"}' - name: execute_code - id: call_hiiwsnky - type: function - - content: '{"code":"if ''documents'' in globals():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no - documents'')\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + Fix the errors and try again. role: tool - tool_call_id: call_hiiwsnky + tool_call_id: call_ee8bn4w7 - content: null - reasoning: Globals also not defined. Probably environment only has defined functions. We might just attempt to run - list_documents to see if any loaded. + reasoning: The tool is execute_code. Use that. role: assistant tool_calls: - function: - arguments: '{"code":"docs = list_documents(limit=10)\nprint(''docs'',docs)\n"}' + arguments: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n"}' name: execute_code - id: call_yc5ml52o + id: call_o1c87rz5 type: function - - content: '{"code":"docs = list_documents(limit=10)\nprint(''docs'',docs)\n","stdout":"docs [{''id'': ''57c6d445-39c8-4eb5-8a5e-b6c2c3096f66'', - ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-02-17 15:59:48.355566''}, {''id'': ''19662d84-5066-402c-a6c1-6eb0e1e1d715'', - ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-02-17 15:59:50.423989''}]\n","stderr":"","success":true}' + - content: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n","stdout":"[{''chunk_id'': + ''d43b4445-8425-4434-bc36-e28e6f7e573e'', ''content'': ''Our mission is to make technology accessible to everyone.'', + ''document_id'': ''bb1e588e-8014-4607-bf9b-a077c16d1cdf'', ''document_title'': ''Mission Statement'', ''document_uri'': + None, ''score'': 0.032522473484277725, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''6d291645-590e-498c-a22c-0883e7b1582b'', + ''content'': ''The company was founded in 1985 by Jane Smith.'', ''document_id'': ''a2e1e13c-e316-4198-a5a8-49353eaa7337'', + ''document_title'': ''Company History'', ''document_uri'': None, ''score'': 0.032522473484277725, ''page_numbers'': + [], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_yc5ml52o - - content: null - reasoning: |+ - Now get these docs. - - role: assistant - tool_calls: - - function: - arguments: '{"code":"content1 = get_document(''Company History'')\ncontent2 = get_document(''Mission Statement'')\nprint(''History:'', - content1[:200])\nprint(''Mission:'', content2[:200])\n"}' - name: execute_code - id: call_0vdkrldb - type: function - - content: '{"code":"content1 = get_document(''Company History'')\ncontent2 = get_document(''Mission Statement'')\nprint(''History:'', - content1[:200])\nprint(''Mission:'', content2[:200])\n","stdout":"History: The company was founded in 1985 by Jane - Smith.\nMission: Our mission is to make technology accessible to everyone.\n","stderr":"","success":true}' - role: tool - tool_call_id: call_0vdkrldb + tool_call_id: call_o1c87rz5 model: gpt-oss reasoning_effort: low stream: false @@ -1333,7 +1363,949 @@ interactions: response: headers: content-length: - - '1089' + - '509' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Got info. Need citation get_chunk. + role: assistant + tool_calls: + - function: + arguments: '{"chunk_id":"6d291645-590e-498c-a22c-0883e7b1582b"}' + name: get_chunk + id: call_b0i0vdgg + index: 0 + type: function + created: 1771924596 + id: chatcmpl-840 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 54 + prompt_tokens: 2265 + total_tokens: 2319 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '10484' + 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. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? + role: user + - content: null + reasoning: Need check documents variable? + role: assistant + tool_calls: + - function: + arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no + documents'')\n"}' + name: execute_code + id: call_2ao52bvz + type: function + - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no + documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' + role: tool + tool_call_id: call_2ao52bvz + - content: null + reasoning: The environment may not provide dir(); use globals(). Check. + role: assistant + tool_calls: + - function: + arguments: '{"code":"print(globals().keys())\n"}' + name: execute_code + id: call_rjj1hq9z + type: function + - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + role: tool + tool_call_id: call_rjj1hq9z + - content: null + reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could + search. + role: assistant + tool_calls: + - function: + arguments: '{"query":"founded mission company","limit":5}' + name: search + id: call_ee8bn4w7 + type: function + - content: |- + Unknown tool name: 'search'. Available tools: 'final_result', 'execute_code' + + Fix the errors and try again. + role: tool + tool_call_id: call_ee8bn4w7 + - content: null + reasoning: The tool is execute_code. Use that. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n"}' + name: execute_code + id: call_o1c87rz5 + type: function + - content: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n","stdout":"[{''chunk_id'': + ''d43b4445-8425-4434-bc36-e28e6f7e573e'', ''content'': ''Our mission is to make technology accessible to everyone.'', + ''document_id'': ''bb1e588e-8014-4607-bf9b-a077c16d1cdf'', ''document_title'': ''Mission Statement'', ''document_uri'': + None, ''score'': 0.032522473484277725, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''6d291645-590e-498c-a22c-0883e7b1582b'', + ''content'': ''The company was founded in 1985 by Jane Smith.'', ''document_id'': ''a2e1e13c-e316-4198-a5a8-49353eaa7337'', + ''document_title'': ''Company History'', ''document_uri'': None, ''score'': 0.032522473484277725, ''page_numbers'': + [], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_o1c87rz5 + - content: null + reasoning: Got info. Need citation get_chunk. + role: assistant + tool_calls: + - function: + arguments: '{"chunk_id":"6d291645-590e-498c-a22c-0883e7b1582b"}' + name: get_chunk + id: call_b0i0vdgg + type: function + - content: |- + Unknown tool name: 'get_chunk'. Available tools: 'final_result', 'execute_code' + + Fix the errors and try again. + role: tool + tool_call_id: call_b0i0vdgg + model: gpt-oss + reasoning_effort: low + stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '573' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: We need to call get_chunk via execute_code. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunk = await get_chunk(''6d291645-590e-498c-a22c-0883e7b1582b'')\nprint(chunk[''content''])\n"}' + name: execute_code + id: call_1o9o5lvl + index: 0 + type: function + created: 1771924598 + id: chatcmpl-401 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 71 + prompt_tokens: 2356 + total_tokens: 2427 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '11056' + 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. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? + role: user + - content: null + reasoning: Need check documents variable? + role: assistant + tool_calls: + - function: + arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no + documents'')\n"}' + name: execute_code + id: call_2ao52bvz + type: function + - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no + documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' + role: tool + tool_call_id: call_2ao52bvz + - content: null + reasoning: The environment may not provide dir(); use globals(). Check. + role: assistant + tool_calls: + - function: + arguments: '{"code":"print(globals().keys())\n"}' + name: execute_code + id: call_rjj1hq9z + type: function + - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + role: tool + tool_call_id: call_rjj1hq9z + - content: null + reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could + search. + role: assistant + tool_calls: + - function: + arguments: '{"query":"founded mission company","limit":5}' + name: search + id: call_ee8bn4w7 + type: function + - content: |- + Unknown tool name: 'search'. Available tools: 'final_result', 'execute_code' + + Fix the errors and try again. + role: tool + tool_call_id: call_ee8bn4w7 + - content: null + reasoning: The tool is execute_code. Use that. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n"}' + name: execute_code + id: call_o1c87rz5 + type: function + - content: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n","stdout":"[{''chunk_id'': + ''d43b4445-8425-4434-bc36-e28e6f7e573e'', ''content'': ''Our mission is to make technology accessible to everyone.'', + ''document_id'': ''bb1e588e-8014-4607-bf9b-a077c16d1cdf'', ''document_title'': ''Mission Statement'', ''document_uri'': + None, ''score'': 0.032522473484277725, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''6d291645-590e-498c-a22c-0883e7b1582b'', + ''content'': ''The company was founded in 1985 by Jane Smith.'', ''document_id'': ''a2e1e13c-e316-4198-a5a8-49353eaa7337'', + ''document_title'': ''Company History'', ''document_uri'': None, ''score'': 0.032522473484277725, ''page_numbers'': + [], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_o1c87rz5 + - content: null + reasoning: Got info. Need citation get_chunk. + role: assistant + tool_calls: + - function: + arguments: '{"chunk_id":"6d291645-590e-498c-a22c-0883e7b1582b"}' + name: get_chunk + id: call_b0i0vdgg + type: function + - content: |- + Unknown tool name: 'get_chunk'. Available tools: 'final_result', 'execute_code' + + Fix the errors and try again. + role: tool + tool_call_id: call_b0i0vdgg + - content: null + reasoning: We need to call get_chunk via execute_code. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunk = await get_chunk(''6d291645-590e-498c-a22c-0883e7b1582b'')\nprint(chunk[''content''])\n"}' + name: execute_code + id: call_1o9o5lvl + type: function + - content: '{"code":"chunk = await get_chunk(''6d291645-590e-498c-a22c-0883e7b1582b'')\nprint(chunk[''content''])\n","stdout":"The + company was founded in 1985 by Jane Smith.\n","stderr":"","success":true}' + role: tool + tool_call_id: call_1o9o5lvl + model: gpt-oss + reasoning_effort: low + stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '550' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Similarly mission. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunk2 = await get_chunk(''d43b4445-8425-4434-bc36-e28e6f7e573e'')\nprint(chunk2[''content''])\n"}' + name: execute_code + id: call_dn2a823n + index: 0 + type: function + created: 1771924600 + id: chatcmpl-408 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 67 + prompt_tokens: 2502 + total_tokens: 2569 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '11618' + 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. + + IMPORTANT: You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside the execute_code tool - you cannot access them any other way. Always execute code to answer questions; do not just describe what code would do. + + CRITICAL: 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 + - from haiku.rag import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + You have access to a sandboxed Python interpreter with these haiku.rag functions (use them directly with `await`, no imports needed): + + ## 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 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: `if 'documents' in dir(): ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `sorted()`/`.sort(key=...)`, try/except, and the `json` module. + + Not supported: imports (other than `json`), class definitions, generators/yield, match statements, decorators, `with` statements. + + For pattern matching or text extraction, use 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 names may differ from filenames (e.g., "tbmed593.pdf" might be stored as "TB MED 593" or similar). + 2. **If get_document returns None**: Use `await list_documents()` to see actual document titles, or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use print() Liberally**: The sandbox captures stdout - print intermediate results to see what you're working with. + 5. **Aggregate with Code**: For counting, averaging, or comparing across documents, write loops and data structures. + 6. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 7. **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 llm() + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + extracted = await llm(f"Extract all dollar amounts from this text as a comma-separated list of numbers (no $ signs): {r['content']}") + for part in extracted.split(','): + part = part.strip().replace(',', '') + if part.isdigit(): + numbers.append(int(part)) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Using search results with get_chunk for citations + ```python + results = await search("safety requirements", limit=5) + for r in results: + chunk = await get_chunk(r['chunk_id']) + print(f"From '{chunk['document_title']}', page {chunk['page_numbers']}: {chunk['content'][:100]}") + ``` + + ### Using llm() for classification + ```python + content = await get_document("Q1 Report") + sentiment = await llm(f"Classify the sentiment as positive, negative, or mixed: {content}") + print(sentiment) + ``` + + ## Workflow + + 1. **ALWAYS start by using execute_code** to explore the knowledge base + 2. Run multiple code blocks as needed to gather information + 3. After collecting data, provide your final answer + + ## Output Format + + CRITICAL: 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": "..."} + + CRITICAL: You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Using the pre-loaded documents variable, tell me when was the company founded and what is their mission? + role: user + - content: null + reasoning: Need check documents variable? + role: assistant + tool_calls: + - function: + arguments: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no + documents'')\n"}' + name: execute_code + id: call_2ao52bvz + type: function + - content: '{"code":"if ''documents'' in dir():\n for doc in documents:\n print(doc[''title''], len(doc[''content'']))\nelse:\n print(''no + documents'')\n","stdout":"","stderr":"NameError: name ''dir'' is not defined","success":false}' + role: tool + tool_call_id: call_2ao52bvz + - content: null + reasoning: The environment may not provide dir(); use globals(). Check. + role: assistant + tool_calls: + - function: + arguments: '{"code":"print(globals().keys())\n"}' + name: execute_code + id: call_rjj1hq9z + type: function + - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + role: tool + tool_call_id: call_rjj1hq9z + - content: null + reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could + search. + role: assistant + tool_calls: + - function: + arguments: '{"query":"founded mission company","limit":5}' + name: search + id: call_ee8bn4w7 + type: function + - content: |- + Unknown tool name: 'search'. Available tools: 'final_result', 'execute_code' + + Fix the errors and try again. + role: tool + tool_call_id: call_ee8bn4w7 + - content: null + reasoning: The tool is execute_code. Use that. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n"}' + name: execute_code + id: call_o1c87rz5 + type: function + - content: '{"code":"results = await search(''founded mission company'', limit=5)\nprint(results)\n","stdout":"[{''chunk_id'': + ''d43b4445-8425-4434-bc36-e28e6f7e573e'', ''content'': ''Our mission is to make technology accessible to everyone.'', + ''document_id'': ''bb1e588e-8014-4607-bf9b-a077c16d1cdf'', ''document_title'': ''Mission Statement'', ''document_uri'': + None, ''score'': 0.032522473484277725, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''6d291645-590e-498c-a22c-0883e7b1582b'', + ''content'': ''The company was founded in 1985 by Jane Smith.'', ''document_id'': ''a2e1e13c-e316-4198-a5a8-49353eaa7337'', + ''document_title'': ''Company History'', ''document_uri'': None, ''score'': 0.032522473484277725, ''page_numbers'': + [], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_o1c87rz5 + - content: null + reasoning: Got info. Need citation get_chunk. + role: assistant + tool_calls: + - function: + arguments: '{"chunk_id":"6d291645-590e-498c-a22c-0883e7b1582b"}' + name: get_chunk + id: call_b0i0vdgg + type: function + - content: |- + Unknown tool name: 'get_chunk'. Available tools: 'final_result', 'execute_code' + + Fix the errors and try again. + role: tool + tool_call_id: call_b0i0vdgg + - content: null + reasoning: We need to call get_chunk via execute_code. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunk = await get_chunk(''6d291645-590e-498c-a22c-0883e7b1582b'')\nprint(chunk[''content''])\n"}' + name: execute_code + id: call_1o9o5lvl + type: function + - content: '{"code":"chunk = await get_chunk(''6d291645-590e-498c-a22c-0883e7b1582b'')\nprint(chunk[''content''])\n","stdout":"The + company was founded in 1985 by Jane Smith.\n","stderr":"","success":true}' + role: tool + tool_call_id: call_1o9o5lvl + - content: null + reasoning: Similarly mission. + role: assistant + tool_calls: + - function: + arguments: '{"code":"chunk2 = await get_chunk(''d43b4445-8425-4434-bc36-e28e6f7e573e'')\nprint(chunk2[''content''])\n"}' + name: execute_code + id: call_dn2a823n + type: function + - content: '{"code":"chunk2 = await get_chunk(''d43b4445-8425-4434-bc36-e28e6f7e573e'')\nprint(chunk2[''content''])\n","stdout":"Our + mission is to make technology accessible to everyone.\n","stderr":"","success":true}' + role: tool + tool_call_id: call_dn2a823n + model: gpt-oss + reasoning_effort: low + stream: false + 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 + - function: + description: Result from RLM agent execution. + name: final_result + parameters: + 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: function + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '1388' content-type: - application/json parsed_body: @@ -1341,23 +2313,26 @@ interactions: - finish_reason: stop index: 0 message: - content: '{"answer":"The company was founded in 1985. Its mission is to make technology accessible to everyone.","program":"# - Retrieve the pre‑loaded documents and extract the founding year and mission statement\n\n# Get the two relevant - documents by title\nhistory_text = get_document(''Company History'')\nmission_text = get_document(''Mission Statement'')\n\n# - Extract founding year from history document\nimport re\nfounding_year = re.search(r''\\b(\\d{4})\\b'', history_text)\nif - founding_year:\n founding_year = founding_year.group(1)\nelse:\n founding_year = ''unknown''\n\n# Clean - mission statement to plain text\nmission = mission_text.strip()\n\n# Prepare answer\nanswer = f\"The company was - founded in {founding_year}. Its mission is to {mission}\"\nprint(answer)"}' + content: '{"answer":"The company was founded in 1985 by Jane Smith. Its mission is to make technology accessible + to everyone.","program":"# Consolidated program to extract founding year and mission\nimport json\n\nasync def + main():\n # Search for founding year and mission statements\n results = await search(''founded mission company'', + limit=10)\n founded = None\n mission = None\n for r in results:\n if ''founded'' in r[''content''].lower() + and not founded:\n founded = r[''content'']\n if ''mission'' in r[''content''].lower() and not + mission:\n mission = r[''content'']\n # Fallback to chunk extraction if needed\n if not founded:\n chunk + = await get_chunk(''6d291645-590e-498c-a22c-0883e7b1582b'')\n founded = chunk[''content'']\n if not + mission:\n chunk2 = await get_chunk(''d43b4445-8425-4434-bc36-e28e6f7e573e'')\n mission = chunk2[''content'']\n # + Print results\n print(''Founded:'', founded)\n print(''Mission:'', mission)\n\n# Run the async main function\nimport + asyncio\nasyncio.run(main())"}' role: assistant - created: 1771336802 - id: chatcmpl-19 + created: 1771924607 + id: chatcmpl-899 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 189 - prompt_tokens: 2305 - total_tokens: 2494 + completion_tokens: 296 + prompt_tokens: 2645 + total_tokens: 2941 status: code: 200 message: OK diff --git a/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml b/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml index 63535627..f3dae05e 100644 --- a/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml +++ b/tests/cassettes/test_sandbox/TestSandboxLLM.test_llm_function.yaml @@ -25,7 +25,7 @@ interactions: response: headers: content-length: - - '307' + - '311' content-type: - application/json parsed_body: @@ -34,17 +34,17 @@ interactions: index: 0 message: content: '4' - reasoning: Answer 4. + reasoning: Just reply 4. role: assistant - created: 1771338974 - id: chatcmpl-199 + created: 1771924616 + id: chatcmpl-525 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 15 + completion_tokens: 16 prompt_tokens: 81 - total_tokens: 96 + total_tokens: 97 status: code: 200 message: OK diff --git a/uv.lock b/uv.lock index e36540e7..8de6854d 100644 --- a/uv.lock +++ b/uv.lock @@ -180,7 +180,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.79.0" +version = "0.83.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -192,9 +192,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/b1/91aea3f8fd180d01d133d931a167a78a3737b3fd39ccef2ae8d6619c24fd/anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871", size = 509825, upload-time = "2026-02-07T18:06:18.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/e5/02cd2919ec327b24234abb73082e6ab84c451182cc3cc60681af700f4c63/anthropic-0.83.0.tar.gz", hash = "sha256:a8732c68b41869266c3034541a31a29d8be0f8cd0a714f9edce3128b351eceb4", size = 534058, upload-time = "2026-02-19T19:26:38.904Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b2/cc0b8e874a18d7da50b0fda8c99e4ac123f23bf47b471827c5f6f3e4a767/anthropic-0.79.0-py3-none-any.whl", hash = "sha256:04cbd473b6bbda4ca2e41dd670fe2f829a911530f01697d0a1e37321eb75f3cf", size = 405918, upload-time = "2026-02-07T18:06:20.246Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/b9d58e4e2a4b1fc3e75ffbab978f999baf8b7c4ba9f96e60edb918ba386b/anthropic-0.83.0-py3-none-any.whl", hash = "sha256:f069ef508c73b8f9152e8850830d92bd5ef185645dbacf234bb213344a274810", size = 456991, upload-time = "2026-02-19T19:26:40.114Z" }, ] [[package]] @@ -1092,7 +1092,7 @@ name = "ffmpeg-python" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future" }, + { name = "future", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } wheels = [ @@ -1306,30 +1306,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] -[[package]] -name = "griffe" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffecli" }, - { name = "griffelib" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, -] - -[[package]] -name = "griffecli" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "griffelib" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, -] - [[package]] name = "griffelib" version = "2.0.0" @@ -1978,14 +1954,14 @@ name = "langchain-core" version = "1.2.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonpatch" }, - { name = "langsmith" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, + { name = "jsonpatch", marker = "python_full_version < '3.14'" }, + { name = "langsmith", marker = "python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "tenacity", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "uuid-utils", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/bb/c501ca60556c11ac80d1454bdcac63cb33583ce4e64fc4535ad5a7d5c6ba/langchain_core-1.2.13.tar.gz", hash = "sha256:d2773d0d0130a356378db9a858cfeef64c3d64bc03722f1d4d6c40eb46fdf01b", size = 831612, upload-time = "2026-02-15T07:45:57.014Z" } wheels = [ @@ -1997,7 +1973,7 @@ name = "langchain-text-splitters" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, + { name = "langchain-core", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/42/c178dcdc157b473330eb7cc30883ea69b8ec60078c7b85e2d521054c4831/langchain_text_splitters-1.1.0.tar.gz", hash = "sha256:75e58acb7585dc9508f3cd9d9809cb14751283226c2d6e21fb3a9ae57582ca22", size = 272230, upload-time = "2025-12-14T01:15:38.659Z" } wheels = [ @@ -2009,15 +1985,15 @@ name = "langsmith" version = "0.7.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, - { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "uuid-utils" }, - { name = "xxhash" }, - { name = "zstandard" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, + { name = "orjson", marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "packaging", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "requests-toolbelt", marker = "python_full_version < '3.14'" }, + { name = "uuid-utils", marker = "python_full_version < '3.14'" }, + { name = "xxhash", marker = "python_full_version < '3.14'" }, + { name = "zstandard", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/bc/8172fefad4f2da888a6d564a27d1fb7d4dbf3c640899c2b40c46235cbe98/langsmith-0.7.3.tar.gz", hash = "sha256:0223b97021af62d2cf53c8a378a27bd22e90a7327e45b353e0069ae60d5d6f9e", size = 988575, upload-time = "2026-02-13T23:25:32.916Z" } wheels = [ @@ -3626,20 +3602,20 @@ email = [ [[package]] name = "pydantic-ai-slim" -version = "1.60.0" +version = "1.63.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "genai-prices" }, - { name = "griffe" }, + { name = "griffelib" }, { name = "httpx" }, { name = "opentelemetry-api" }, { name = "pydantic" }, { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/97/f73f439f3d415d43f38250f76852121188d0ef6114ec702e84e7d69c301d/pydantic_ai_slim-1.60.0.tar.gz", hash = "sha256:12ba3e6ef933fcb9fc6a307dbdaa43ca15bbc1b8ec77521afd1b7a526d12330f", size = 418839, upload-time = "2026-02-17T00:33:29.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/2b5c0c60b42e6af49830f6a09b5d38fecdb1f20d9659152691eba95613b4/pydantic_ai_slim-1.63.0.tar.gz", hash = "sha256:9377afecdfe4bc17f5c9ed72c758e460703ac5876931aa2f18ace8ac0e69312a", size = 426862, upload-time = "2026-02-23T17:56:36.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/40/8cb494a4d2ba62b5f92ae8bc79a2abbbf8509cb692edd4bc841695187780/pydantic_ai_slim-1.60.0-py3-none-any.whl", hash = "sha256:6865188a225a2979c82bb022a299d438d805d258c6d3f9810f7fe4e3c86af80a", size = 546410, upload-time = "2026-02-17T00:33:21.901Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ca/c4e39eec1cff5a294b64313a8a959b38d326819e0f0a41f48e61ce019a22/pydantic_ai_slim-1.63.0-py3-none-any.whl", hash = "sha256:ed393b0f871b748171f65bec5191c3025b5abb8a4fc616afee17eb9dc2dfa15d", size = 554190, upload-time = "2026-02-23T17:56:29.533Z" }, ] [package.optional-dependencies] @@ -3683,7 +3659,7 @@ vertexai = [ { name = "requests" }, ] voyageai = [ - { name = "voyageai" }, + { name = "voyageai", marker = "python_full_version < '3.14'" }, ] [[package]] @@ -3759,7 +3735,7 @@ wheels = [ [[package]] name = "pydantic-evals" -version = "1.60.0" +version = "1.63.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -3769,14 +3745,14 @@ dependencies = [ { name = "pyyaml" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/2c/bed606a726b09adc9ee414bdb919ffe499edf7f8c631ba03b0ff3aa34435/pydantic_evals-1.60.0.tar.gz", hash = "sha256:ae3edd6667075acd8ef04c0d6fffb1ebe72c37ff077295fdbd6319e59284580b", size = 54214, upload-time = "2026-02-17T00:33:31.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/21b6ddf65b56f7401c344f98e4e6258a02d2868c8a52a8b79c0e0e701029/pydantic_evals-1.63.0.tar.gz", hash = "sha256:eed56a7192e07c8be8cf16e53bb2ef652b4f7f7b8527650ac45fde865a4ecf9d", size = 56365, upload-time = "2026-02-23T17:56:37.71Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/a2/60790b2c971f6ce78fea2db2b65800489982c341ad9aa6db750070a93dcd/pydantic_evals-1.60.0-py3-none-any.whl", hash = "sha256:7a7414535002cae63ba0d0d9b15c6e72c28252cf120290b18372c9852c91fcfe", size = 65278, upload-time = "2026-02-17T00:33:23.411Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/7174ad6abca2457e35a1b902ca4fa78aa8ee72e4ec2e9cd5dc8904014ec9/pydantic_evals-1.63.0-py3-none-any.whl", hash = "sha256:2e92a3af579a5670b2babf2044081d0ef99ab5a9ef141972616d71fd7e5bfd0e", size = 67279, upload-time = "2026-02-23T17:56:31.008Z" }, ] [[package]] name = "pydantic-graph" -version = "1.60.0" +version = "1.63.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3784,9 +3760,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/e6/1cae7cd39ab29f2eebc87c0a82c7ffcdfbe88492d2fb4afaaad91534e1ff/pydantic_graph-1.60.0.tar.gz", hash = "sha256:9710e457c2f8c113fd63629f05174e45bdca917d90c69ec8cf558649f995505f", size = 58492, upload-time = "2026-02-17T00:33:32.66Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/c8/aa3cb56552562b799f31e9de291c8bd88306308cfc9647d220dfff2bea18/pydantic_graph-1.63.0.tar.gz", hash = "sha256:5fd98bb22fa6181f0357a6ffad38a3214af12868bd46492d6456c5db434466b4", size = 58528, upload-time = "2026-02-23T17:56:39.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/6c/ce1c0eca77c6efbf7c7168c05a244c2756bde876a52575f750471d522024/pydantic_graph-1.60.0-py3-none-any.whl", hash = "sha256:741fa1e48424b0def86079a01100ad0652e75882f0352cd157232b75ace468a5", size = 72345, upload-time = "2026-02-17T00:33:25.077Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1c/8dcae24c824dd2690fbe7375083b369b10ed1ad773e2b9d1122bb6c0fcdc/pydantic_graph-1.63.0-py3-none-any.whl", hash = "sha256:d9b7a387116f358d470c042b07aa08125cadfcfa8c08ef01769746a489aef0d5", size = 72353, upload-time = "2026-02-23T17:56:32.304Z" }, ] [[package]] @@ -4398,7 +4374,7 @@ name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ @@ -5371,16 +5347,16 @@ name = "voyageai" version = "0.3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, - { name = "aiolimiter" }, - { name = "ffmpeg-python" }, - { name = "langchain-text-splitters" }, + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "aiolimiter", marker = "python_full_version < '3.14'" }, + { name = "ffmpeg-python", marker = "python_full_version < '3.14'" }, + { name = "langchain-text-splitters", marker = "python_full_version < '3.14'" }, { name = "numpy", marker = "python_full_version < '3.14'" }, - { name = "pillow" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "tenacity" }, - { name = "tokenizers" }, + { name = "pillow", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "tenacity", marker = "python_full_version < '3.14'" }, + { name = "tokenizers", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/16/1b46b3cd401e1717a68197c1fe336d7bb4e0a1833f8105e1738f5b1add05/voyageai-0.3.7.tar.gz", hash = "sha256:826cd97f97223f42b5babc5c459c9c80f3a8215ce5c0e007b0b276550f790d24", size = 26485, upload-time = "2025-12-16T18:43:05.26Z" } wheels = [