From 034ee27daf7a6656d70c1fa2a9b051a7d6724c29 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 3 Mar 2026 14:18:28 +0200 Subject: [PATCH 1/2] Use ToolOutput for structured output. --- haiku_rag_slim/haiku/rag/agents/qa/agent.py | 4 ++-- haiku_rag_slim/haiku/rag/agents/research/graph.py | 6 ++---- haiku_rag_slim/haiku/rag/agents/rlm/agent.py | 3 ++- tests/agents/rlm/test_agent.py | 4 +++- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/agents/qa/agent.py b/haiku_rag_slim/haiku/rag/agents/qa/agent.py index 9e5ea2c0..11de13ce 100644 --- a/haiku_rag_slim/haiku/rag/agents/qa/agent.py +++ b/haiku_rag_slim/haiku/rag/agents/qa/agent.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from pydantic_ai import Agent +from pydantic_ai.output import ToolOutput from haiku.rag.agents.qa.prompts import QA_SYSTEM_PROMPT from haiku.rag.agents.research.models import ( @@ -59,8 +60,7 @@ class QuestionAnswerAgent: agent: Agent[_QARunDeps, RawSearchAnswer] = Agent( # ty: ignore[invalid-assignment] model=get_model(self._model_config, self._config), deps_type=_QARunDeps, - output_type=RawSearchAnswer, - output_retries=3, + output_type=ToolOutput(RawSearchAnswer, max_retries=3), instructions=self._system_prompt, toolsets=[search_toolset], retries=3, diff --git a/haiku_rag_slim/haiku/rag/agents/research/graph.py b/haiku_rag_slim/haiku/rag/agents/research/graph.py index e445622e..d6996dcb 100644 --- a/haiku_rag_slim/haiku/rag/agents/research/graph.py +++ b/haiku_rag_slim/haiku/rag/agents/research/graph.py @@ -68,10 +68,9 @@ async def _iterative_plan_logic( plan_agent: Agent[ResearchDependencies, IterativePlanResult] = Agent( # type: ignore[assignment] model=get_model(model_config, config), - output_type=IterativePlanResult, + output_type=ToolOutput(IterativePlanResult, max_retries=3), instructions=effective_prompt, retries=3, - output_retries=3, deps_type=ResearchDependencies, ) @@ -219,10 +218,9 @@ def build_research_graph( agent: Agent[ResearchDependencies, ResearchReport] = Agent( # type: ignore[assignment] model=get_model(model_config, config), - output_type=ResearchReport, + output_type=ToolOutput(ResearchReport, max_retries=3), instructions=synthesis_prompt, retries=3, - output_retries=3, deps_type=ResearchDependencies, ) diff --git a/haiku_rag_slim/haiku/rag/agents/rlm/agent.py b/haiku_rag_slim/haiku/rag/agents/rlm/agent.py index 4c234832..19ab3c4a 100644 --- a/haiku_rag_slim/haiku/rag/agents/rlm/agent.py +++ b/haiku_rag_slim/haiku/rag/agents/rlm/agent.py @@ -1,4 +1,5 @@ from pydantic_ai import Agent, RunContext +from pydantic_ai.output import ToolOutput from haiku.rag.agents.rlm.dependencies import RLMDeps from haiku.rag.agents.rlm.models import CodeExecution, RLMResult @@ -25,7 +26,7 @@ def create_rlm_agent(config: AppConfig) -> Agent[RLMDeps, RLMResult]: agent: Agent[RLMDeps, RLMResult] = Agent( # type: ignore[invalid-assignment] model, deps_type=RLMDeps, - output_type=RLMResult, + output_type=ToolOutput(RLMResult, max_retries=3), instructions=RLM_SYSTEM_PROMPT, retries=3, ) diff --git a/tests/agents/rlm/test_agent.py b/tests/agents/rlm/test_agent.py index f78af4e1..ba1cdf6c 100644 --- a/tests/agents/rlm/test_agent.py +++ b/tests/agents/rlm/test_agent.py @@ -2,6 +2,7 @@ from pathlib import Path import pytest from pydantic_ai import Agent +from pydantic_ai.output import ToolOutput from haiku.rag.agents.rlm.agent import create_rlm_agent from haiku.rag.agents.rlm.dependencies import RLMDeps @@ -19,7 +20,8 @@ class TestCreateRLMAgent: agent = create_rlm_agent(Config) assert isinstance(agent, Agent) assert agent.deps_type is RLMDeps - assert agent.output_type is RLMResult + assert isinstance(agent.output_type, ToolOutput) + assert agent.output_type.output is RLMResult def test_agent_has_execute_code_tool(self): agent = create_rlm_agent(Config) From c9f63b9ab600c4be4dd59acab39e3b090753f7b7 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 3 Mar 2026 16:59:35 +0200 Subject: [PATCH 2/2] Re-record RLM vcrs --- ...ntRLMIntegration.test_rlm_aggregation.yaml | 1047 +++- ...MIntegration.test_rlm_count_documents.yaml | 463 +- ...tegration.test_rlm_search_and_extract.yaml | 1629 ++++--- ...gration.test_rlm_search_and_get_chunk.yaml | 510 +- ...n.test_rlm_semantic_analysis_with_llm.yaml | 4303 ++++++++++++++--- ...ntRLMIntegration.test_rlm_with_filter.yaml | 475 +- ...ion.test_rlm_with_preloaded_documents.yaml | 2188 +++++---- 7 files changed, 7667 insertions(+), 2948 deletions(-) diff --git a/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_aggregation.yaml b/tests/cassettes/test_rlm/TestClientRLMIntegration.test_rlm_aggregation.yaml index f2b1b2ce..c248b869 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: - - '7341' + - '7720' content-type: - application/json host: @@ -139,15 +139,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -167,6 +165,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -180,7 +198,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -188,17 +206,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -214,44 +230,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -261,7 +270,7 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: What is the total revenue across all quarterly reports? role: user @@ -315,7 +324,7 @@ interactions: response: headers: content-length: - - '809' + - '683' content-type: - application/json parsed_body: @@ -324,27 +333,25 @@ interactions: index: 0 message: content: '' - reasoning: We need to find quarterly reports documents and extract revenue figures. Likely documents include Q1, - Q2, etc. Search for "quarterly report revenue". + reasoning: We need total revenue across all quarterly reports. Likely documents contain quarterly reports. Let's + search for "quarterly report revenue" or "Revenue" with limit. role: assistant tool_calls: - function: - 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"}' + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)"}' name: execute_code - id: call_00l27zrg + id: call_l8kk2in2 index: 0 type: function - created: 1771924503 - id: chatcmpl-643 + created: 1772549330 + id: chatcmpl-995 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 110 - prompt_tokens: 1625 - total_tokens: 1735 + completion_tokens: 76 + prompt_tokens: 1732 + total_tokens: 1808 status: code: 200 message: OK @@ -397,7 +404,7 @@ interactions: connection: - keep-alive content-length: - - '8336' + - '9191' content-type: - application/json host: @@ -408,15 +415,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -436,6 +441,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -449,7 +474,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -457,17 +482,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -483,44 +506,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -530,27 +546,31 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: What is the total revenue across all quarterly reports? role: user - content: null - reasoning: We need to find quarterly reports documents and extract revenue figures. Likely documents include Q1, Q2, - etc. Search for "quarterly report revenue". + reasoning: We need total revenue across all quarterly reports. Likely documents contain quarterly reports. Let's search + for "quarterly report revenue" or "Revenue" with limit. role: assistant tool_calls: - function: - 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"}' + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)"}' name: execute_code - id: call_00l27zrg + id: call_l8kk2in2 type: function - - 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}' + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)","stdout":"[{''chunk_id'': + ''4caa01a9-80d3-4d7d-bfab-ca11a66318d7'', ''content'': ''Sales report Q1: Revenue was $100,000.'', ''document_id'': + ''330c5184-910f-4515-b99b-72e3e8a229bf'', ''document_title'': ''Q1 Report'', ''document_uri'': None, ''score'': + 0.03226645663380623, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''93295467-8f49-4268-8701-d0f84a17e5d9'', + ''content'': ''Sales report Q2: Revenue was $150,000.'', ''document_id'': ''30e9d013-29b1-46bb-b578-bfc260572dda'', + ''document_title'': ''Q2 Report'', ''document_uri'': None, ''score'': 0.03226645663380623, ''page_numbers'': [], + ''headings'': None}, {''chunk_id'': ''efc76976-d951-4c74-a881-979e1382a6ae'', ''content'': ''Sales report Q3: Revenue + was $200,000.'', ''document_id'': ''3a6a60f5-3467-40cf-80b0-1d7a76bea1dd'', ''document_title'': ''Q3 Report'', ''document_uri'': + None, ''score'': 0.032258063554763794, ''page_numbers'': [], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_00l27zrg + tool_call_id: call_l8kk2in2 model: gpt-oss reasoning_effort: low stream: false @@ -601,7 +621,7 @@ interactions: response: headers: content-length: - - '567' + - '710' content-type: - application/json parsed_body: @@ -610,24 +630,25 @@ interactions: index: 0 message: content: '' - reasoning: Found Q1, Q2, Q3. Probably Q4 too. Search for Q4. + reasoning: We see 3 chunks. Need total revenue sum. Extract amounts. role: assistant tool_calls: - function: - arguments: '{"code":"results4 = await search(\"Q4 Report revenue\", limit=5)\nprint(results4)\n"}' + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor + r in results:\n m = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)"}' name: execute_code - id: call_s2cf4xgs + id: call_5erv45jj index: 0 type: function - created: 1771924505 - id: chatcmpl-466 + created: 1772549332 + id: chatcmpl-421 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 64 - prompt_tokens: 1858 - total_tokens: 1922 + completion_tokens: 103 + prompt_tokens: 2185 + total_tokens: 2288 status: code: 200 message: OK @@ -640,7 +661,7 @@ interactions: connection: - keep-alive content-length: - - '87' + - '94' content-type: - application/json host: @@ -649,7 +670,7 @@ interactions: parsed_body: encoding_format: base64 input: - - Q4 Report revenue + - quarterly report revenue model: qwen3-embedding:4b uri: http://localhost:11434/v1/embeddings response: @@ -660,7 +681,7 @@ interactions: - chunked parsed_body: data: - - embedding: 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 + - embedding: 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 index: 0 object: embedding model: qwen3-embedding:4b @@ -680,7 +701,7 @@ interactions: connection: - keep-alive content-length: - - '9694' + - '10014' content-type: - application/json host: @@ -691,15 +712,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -719,6 +738,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -732,7 +771,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -740,17 +779,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -766,44 +803,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -813,47 +843,46 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: What is the total revenue across all quarterly reports? role: user - content: null - reasoning: We need to find quarterly reports documents and extract revenue figures. Likely documents include Q1, Q2, - etc. Search for "quarterly report revenue". + reasoning: We need total revenue across all quarterly reports. Likely documents contain quarterly reports. Let's search + for "quarterly report revenue" or "Revenue" with limit. role: assistant tool_calls: - function: - 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"}' + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)"}' name: execute_code - id: call_00l27zrg + id: call_l8kk2in2 type: function - - 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_00l27zrg - - content: null - reasoning: Found Q1, Q2, Q3. Probably Q4 too. Search for Q4. - role: assistant - tool_calls: - - function: - arguments: '{"code":"results4 = await search(\"Q4 Report revenue\", limit=5)\nprint(results4)\n"}' - name: execute_code - id: call_s2cf4xgs - type: function - - 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'', + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)","stdout":"[{''chunk_id'': + ''4caa01a9-80d3-4d7d-bfab-ca11a66318d7'', ''content'': ''Sales report Q1: Revenue was $100,000.'', ''document_id'': + ''330c5184-910f-4515-b99b-72e3e8a229bf'', ''document_title'': ''Q1 Report'', ''document_uri'': None, ''score'': + 0.03226645663380623, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''93295467-8f49-4268-8701-d0f84a17e5d9'', + ''content'': ''Sales report Q2: Revenue was $150,000.'', ''document_id'': ''30e9d013-29b1-46bb-b578-bfc260572dda'', ''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'': + ''headings'': None}, {''chunk_id'': ''efc76976-d951-4c74-a881-979e1382a6ae'', ''content'': ''Sales report Q3: Revenue + was $200,000.'', ''document_id'': ''3a6a60f5-3467-40cf-80b0-1d7a76bea1dd'', ''document_title'': ''Q3 Report'', ''document_uri'': None, ''score'': 0.032258063554763794, ''page_numbers'': [], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_s2cf4xgs + tool_call_id: call_l8kk2in2 + - content: null + reasoning: We see 3 chunks. Need total revenue sum. Extract amounts. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor r + in results:\n m = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)"}' + name: execute_code + id: call_5erv45jj + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor r in results:\n m + = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' + role: tool + tool_call_id: call_5erv45jj model: gpt-oss reasoning_effort: low stream: false @@ -904,7 +933,333 @@ interactions: response: headers: content-length: - - '1421' + - '747' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: We cannot import re. Use regex_findall etc. Use await regex_findall. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\ntotal=0\nfor r in results:\n amounts + = await regex_findall(r''\\$([\\d,]+)'', r[''content''])\n for a in amounts:\n total+=int(a.replace('','',''''))\nprint(total)"}' + name: execute_code + id: call_otkvyqic + index: 0 + type: function + created: 1772549335 + id: chatcmpl-944 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 109 + prompt_tokens: 2391 + total_tokens: 2500 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '94' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - quarterly report revenue + 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: + - '10867' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: What is the total revenue across all quarterly reports? + role: user + - content: null + reasoning: We need total revenue across all quarterly reports. Likely documents contain quarterly reports. Let's search + for "quarterly report revenue" or "Revenue" with limit. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)"}' + name: execute_code + id: call_l8kk2in2 + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)","stdout":"[{''chunk_id'': + ''4caa01a9-80d3-4d7d-bfab-ca11a66318d7'', ''content'': ''Sales report Q1: Revenue was $100,000.'', ''document_id'': + ''330c5184-910f-4515-b99b-72e3e8a229bf'', ''document_title'': ''Q1 Report'', ''document_uri'': None, ''score'': + 0.03226645663380623, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''93295467-8f49-4268-8701-d0f84a17e5d9'', + ''content'': ''Sales report Q2: Revenue was $150,000.'', ''document_id'': ''30e9d013-29b1-46bb-b578-bfc260572dda'', + ''document_title'': ''Q2 Report'', ''document_uri'': None, ''score'': 0.03226645663380623, ''page_numbers'': [], + ''headings'': None}, {''chunk_id'': ''efc76976-d951-4c74-a881-979e1382a6ae'', ''content'': ''Sales report Q3: Revenue + was $200,000.'', ''document_id'': ''3a6a60f5-3467-40cf-80b0-1d7a76bea1dd'', ''document_title'': ''Q3 Report'', ''document_uri'': + None, ''score'': 0.032258063554763794, ''page_numbers'': [], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_l8kk2in2 + - content: null + reasoning: We see 3 chunks. Need total revenue sum. Extract amounts. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor r + in results:\n m = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)"}' + name: execute_code + id: call_5erv45jj + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor r in results:\n m + = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' + role: tool + tool_call_id: call_5erv45jj + - content: null + reasoning: We cannot import re. Use regex_findall etc. Use await regex_findall. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\ntotal=0\nfor r in results:\n amounts + = await regex_findall(r''\\$([\\d,]+)'', r[''content''])\n for a in amounts:\n total+=int(a.replace('','',''''))\nprint(total)"}' + name: execute_code + id: call_otkvyqic + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\ntotal=0\nfor r in results:\n amounts + = await regex_findall(r''\\$([\\d,]+)'', r[''content''])\n for a in amounts:\n total+=int(a.replace('','',''''))\nprint(total)","stdout":"450000\n","stderr":"","success":true}' + role: tool + tool_call_id: call_otkvyqic + 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: + - '766' content-type: - application/json parsed_body: @@ -912,27 +1267,321 @@ interactions: - finish_reason: stop index: 0 message: - 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. + content: '{"answer":"The total revenue reported across all quarterly reports is $450,000.","program":"# Find all + quarterly report revenue snippets and sum them\nresults = await search(''quarterly report revenue'', limit=10)\ntotal + = 0\nfor r in results:\n # Find dollar amounts, allowing commas\n amounts = await regex_findall(r''\\$([\\d,]+)'', + r[''content''])\n for a in amounts:\n total += int(a.replace('','', ''''))\nprint(f\"Total revenue: + ${total}\")"}' role: assistant - created: 1771924512 - id: chatcmpl-210 + created: 1772549338 + id: chatcmpl-206 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 367 - prompt_tokens: 2311 - total_tokens: 2678 + completion_tokens: 121 + prompt_tokens: 2596 + total_tokens: 2717 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '11508' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: What is the total revenue across all quarterly reports? + role: user + - content: null + reasoning: We need total revenue across all quarterly reports. Likely documents contain quarterly reports. Let's search + for "quarterly report revenue" or "Revenue" with limit. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)"}' + name: execute_code + id: call_l8kk2in2 + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nprint(results)","stdout":"[{''chunk_id'': + ''4caa01a9-80d3-4d7d-bfab-ca11a66318d7'', ''content'': ''Sales report Q1: Revenue was $100,000.'', ''document_id'': + ''330c5184-910f-4515-b99b-72e3e8a229bf'', ''document_title'': ''Q1 Report'', ''document_uri'': None, ''score'': + 0.03226645663380623, ''page_numbers'': [], ''headings'': None}, {''chunk_id'': ''93295467-8f49-4268-8701-d0f84a17e5d9'', + ''content'': ''Sales report Q2: Revenue was $150,000.'', ''document_id'': ''30e9d013-29b1-46bb-b578-bfc260572dda'', + ''document_title'': ''Q2 Report'', ''document_uri'': None, ''score'': 0.03226645663380623, ''page_numbers'': [], + ''headings'': None}, {''chunk_id'': ''efc76976-d951-4c74-a881-979e1382a6ae'', ''content'': ''Sales report Q3: Revenue + was $200,000.'', ''document_id'': ''3a6a60f5-3467-40cf-80b0-1d7a76bea1dd'', ''document_title'': ''Q3 Report'', ''document_uri'': + None, ''score'': 0.032258063554763794, ''page_numbers'': [], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_l8kk2in2 + - content: null + reasoning: We see 3 chunks. Need total revenue sum. Extract amounts. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor r + in results:\n m = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)"}' + name: execute_code + id: call_5erv45jj + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\nimport re\ntotal=0\nfor r in results:\n m + = re.search(r''\\$(\\d+)'', r[''content''])\n if m:\n total+=int(m.group(1))\nprint(total)","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' + role: tool + tool_call_id: call_5erv45jj + - content: null + reasoning: We cannot import re. Use regex_findall etc. Use await regex_findall. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''quarterly report revenue'', limit=10)\ntotal=0\nfor r in results:\n amounts + = await regex_findall(r''\\$([\\d,]+)'', r[''content''])\n for a in amounts:\n total+=int(a.replace('','',''''))\nprint(total)"}' + name: execute_code + id: call_otkvyqic + type: function + - content: '{"code":"results = await search(''quarterly report revenue'', limit=10)\ntotal=0\nfor r in results:\n amounts + = await regex_findall(r''\\$([\\d,]+)'', r[''content''])\n for a in amounts:\n total+=int(a.replace('','',''''))\nprint(total)","stdout":"450000\n","stderr":"","success":true}' + role: tool + tool_call_id: call_otkvyqic + - content: '{"answer":"The total revenue reported across all quarterly reports is $450,000.","program":"# Find all quarterly + report revenue snippets and sum them\nresults = await search(''quarterly report revenue'', limit=10)\ntotal = 0\nfor + r in results:\n # Find dollar amounts, allowing commas\n amounts = await regex_findall(r''\\$([\\d,]+)'', + r[''content''])\n for a in amounts:\n total += int(a.replace('','', ''''))\nprint(f\"Total revenue: ${total}\")"}' + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '954' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to wrap answer in tool call. Use final_result. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"The total revenue reported across all quarterly reports is $450,000.","program":"# Find + all quarterly report revenue snippets and sum them\nresults = await search(''quarterly report revenue'', limit=10)\ntotal + = 0\nfor r in results:\n # Find dollar amounts, allowing commas\n amounts = await regex_findall(r''\\$([\\d,]+)'', + r[''content''])\n for a in amounts:\n total += int(a.replace('','', ''''))\nprint(f\"Total revenue: + ${total}\")"}' + name: final_result + id: call_ne7i4w3g + index: 0 + type: function + created: 1772549342 + id: chatcmpl-743 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 148 + prompt_tokens: 2661 + total_tokens: 2809 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 ae352e94..1d7205df 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: - - '7325' + - '7704' content-type: - application/json host: @@ -139,15 +139,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -167,6 +165,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -180,7 +198,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -188,17 +206,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -214,44 +230,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -261,7 +270,7 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: How many documents are in the database? role: user @@ -315,7 +324,7 @@ interactions: response: headers: content-length: - - '514' + - '516' content-type: - application/json parsed_body: @@ -324,24 +333,24 @@ interactions: index: 0 message: content: '' - reasoning: Need to list docs. + reasoning: Need list_documents. role: assistant tool_calls: - function: arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' name: execute_code - id: call_stp0fimx + id: call_oyaoz18v index: 0 type: function - created: 1771924497 - id: chatcmpl-750 + created: 1772549310 + id: chatcmpl-325 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 44 - prompt_tokens: 1623 - total_tokens: 1667 + completion_tokens: 43 + prompt_tokens: 1730 + total_tokens: 1773 status: code: 200 message: OK @@ -354,7 +363,7 @@ interactions: connection: - keep-alive content-length: - - '7759' + - '8140' content-type: - application/json host: @@ -365,15 +374,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -393,6 +400,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -406,7 +433,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -414,17 +441,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -440,44 +465,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -487,22 +505,22 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: How many documents are in the database? role: user - content: null - reasoning: Need to list docs. + reasoning: Need list_documents. role: assistant tool_calls: - function: arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' name: execute_code - id: call_stp0fimx + id: call_oyaoz18v type: function - content: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_stp0fimx + tool_call_id: call_oyaoz18v model: gpt-oss reasoning_effort: low stream: false @@ -563,15 +581,270 @@ interactions: message: content: '{"answer":"There are 3 documents in the database.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' role: assistant - created: 1771924498 - id: chatcmpl-945 + created: 1772549311 + id: chatcmpl-670 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: completion_tokens: 38 - prompt_tokens: 1709 - total_tokens: 1747 + prompt_tokens: 1815 + total_tokens: 1853 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '8432' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: How many documents are in the database? + role: user + - content: null + reasoning: Need list_documents. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' + name: execute_code + id: call_oyaoz18v + type: function + - content: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_oyaoz18v + - content: '{"answer":"There are 3 documents in the database.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '610' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to return via tool call? We should use final_result. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"There are 3 documents in the database.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' + name: final_result + id: call_46d25765 + index: 0 + type: function + created: 1772549312 + id: chatcmpl-412 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 63 + prompt_tokens: 1865 + total_tokens: 1928 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 04962a11..76741f2b 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: - - '7420' + - '7799' content-type: - application/json host: @@ -193,15 +193,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -221,6 +219,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -234,7 +252,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -242,17 +260,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -268,44 +284,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -315,7 +324,7 @@ interactions: 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. + 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. @@ -370,7 +379,7 @@ interactions: response: headers: content-length: - - '683' + - '756' content-type: - application/json parsed_body: @@ -379,25 +388,26 @@ interactions: index: 0 message: content: '' - reasoning: Need to search for document element types or labels. Use search. + reasoning: We need to search for content about document element types or labels. Likely many documents mention e.g. + "title", "section_header", "list_item", etc. We should search for terms like "element types" or "labels". Let's + 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"}' + arguments: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n"}' name: execute_code - id: call_j7uw3i6n + id: call_4k3t2n10 index: 0 type: function - created: 1771924566 - id: chatcmpl-52 + created: 1772548402 + id: chatcmpl-870 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 87 - prompt_tokens: 1639 - total_tokens: 1726 + completion_tokens: 92 + prompt_tokens: 1746 + total_tokens: 1838 status: code: 200 message: OK @@ -450,7 +460,7 @@ interactions: connection: - keep-alive content-length: - - '9384' + - '18052' content-type: - application/json host: @@ -461,15 +471,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -489,6 +497,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -502,7 +530,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -510,17 +538,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -536,44 +562,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -583,35 +602,106 @@ interactions: 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. + 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. + reasoning: We need to search for content about document element types or labels. Likely many documents mention e.g. + "title", "section_header", "list_item", etc. We should search for terms like "element types" or "labels". Let's + 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"}' + arguments: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n"}' name: execute_code - id: call_j7uw3i6n + id: call_4k3t2n10 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}' + - content: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n","stdout":"[{''chunk_id'': + ''4b9cb6a5-f203-4070-8b2f-ab3ef12dde1b'', ''content'': ''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 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}$, $_{Section-header}$, + $_{Table}$, $_{Text}$, and $_{Title}$. Critical factors that were considered for the choice of these class labels + were (1) the overall occurrence of the label, (2) the specificity of the label, (3) recognisability on a single + page (i.e. no need for context from previous or next page) and (4) overall coverage of the page. Specificity ensures + that the choice of label is not ambiguous, while coverage ensures that all meaningful items on a page can be annotated. + We refrained from class labels that are very specific to a document category, such as Abstract in the Scientific + Articles category. We also avoided class labels that are tightly linked to the semantics of the text. Labels such + as Author and'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.032786883413791656, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''fdc2266a-b812-48c4-a49a-ece08a348ead'', ''content'': + ''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 include publication repositories + such as arXiv$^{3}$, government offices, company websites as well as data directory services for financial reports + and patents. Scanned documents were excluded wherever possible because they can be rotated or skewed. This would + not allow us to perform annotation with rectangular bounding-boxes and therefore complicate the annotation process.'', + ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.0320020467042923, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''0b6492ef-bece-4486-98ce-85280c3b2667'', + ''content'': ''$_{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 (CCS) [22], a cloud-native + platform which provides a visual annotation interface and allows for dataset inspection and analysis. The annotation + interface of CCS is shown in Figure 3. The desired balance of pages between the different document categories was + achieved by selective subsampling of pages with certain desired properties. For example, we made sure to include + the title page of each document and bias the remaining page selection to those with figures or tables. The latter + was achieved by leveraging pre-trained object detection models from PubLayNet, which helped us estimate how many + figures and tables a given page contains.\\n$^{3}$https://arxiv.org/'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.03036576882004738, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''940f11c1-5028-4dd4-9515-781f1b9cdc2a'', + ''content'': ''\\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.'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.016129031777381897, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''eba516e5-277f-487b-bcb9-3caea945ac54'', + ''content'': ''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 = 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 (%).Man = 90-94. Page-header, triple inter-annotator mAP @ 0.5-0.95 + (%).Sci = 98-100. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 91-92. Page-header, triple inter-annotator + mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.015625, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''63d5d7a0-e9e6-4258-9c76-d97689acffb0'', ''content'': + ''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, % 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 = 75-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Man + = 83-86. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 98-99. Table, triple'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.015384615398943424, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''48c416f5-db1e-47c9-9f5c-0caf7b36e568'', + ''content'': ''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, 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-annotator mAP @ 0.5-0.95 (%).Law = 95-99. + Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-78. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + ='', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.01515151560306549, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''f931ae1e-5413-4b26-a8cc-14ce5ce7bdc1'', + ''content'': ''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, % 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 = 76-81. Section-header, triple inter-annotator + mAP @ 0.5-0.95 (%).Man = 90-92. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 94-95. Section-header, + triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.014925372786819935, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''b8056bae-cc3b-4674-a7df-44d918eb4aea'', + ''content'': ''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. 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, triple inter-annotator mAP @ 0.5-0.95 (%).Law = + 85-94. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = n/a. Footnote, triple inter-annotator mAP @ 0.5-0.95 + (%).Ten'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.014705882407724857, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''917de523-f069-49fa-ad71-1f0886db460c'', ''content'': + ''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 inter-annotator mAP @ 0.5-0.95 + (%).Fin = 74-83. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Man = 90-92. List-item, triple inter-annotator + mAP @ 0.5-0.95 (%).Sci = 97-97. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 81-85. List-item, triple + inter-annotator mAP @ 0.5-0.95 (%).Pat = 75-88. List-item, triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.014285714365541935, ''page_numbers'': [1], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_j7uw3i6n + tool_call_id: call_4k3t2n10 model: gpt-oss reasoning_effort: low stream: false @@ -662,7 +752,7 @@ interactions: response: headers: content-length: - - '891' + - '1473' content-type: - application/json parsed_body: @@ -671,27 +761,32 @@ interactions: index: 0 message: content: '' - 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. + reasoning: 'We got list of chunks with labels info. The element types mentioned in that chunk: Caption, Footnote, + Formula, List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also maybe others like + Affiliation but not part of 11 class labels. So answer list unique types. Let''s list: Caption, Footnote, Formula, + List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also mention that these are + the 11 distinct class labels. Provide answer and program summarizing retrieval of labels. Let''s write program: + search for that chunk, extract from content using regex to find labels. Use regex pattern r''\b[_\{]([A-Za-z-]+)[_\}]'' + or simpler from known labels list. But we can hardcode extraction: search patterns like ''$_{Caption}$'', etc. + We''ll collect unique.' 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"}' + arguments: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n"}' name: execute_code - id: call_gzg850pl + id: call_jxgy76dr index: 0 type: function - created: 1771924570 - id: chatcmpl-449 + created: 1772548413 + id: chatcmpl-986 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 142 - prompt_tokens: 2158 - total_tokens: 2300 + completion_tokens: 275 + prompt_tokens: 5108 + total_tokens: 5383 status: code: 200 message: OK @@ -704,7 +799,7 @@ interactions: connection: - keep-alive content-length: - - '75' + - '85' content-type: - application/json host: @@ -713,7 +808,7 @@ interactions: parsed_body: encoding_format: base64 input: - - label + - 11 class labels model: qwen3-embedding:4b uri: http://localhost:11434/v1/embeddings response: @@ -724,14 +819,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: 2 - total_tokens: 2 + prompt_tokens: 5 + total_tokens: 5 status: code: 200 message: OK @@ -744,7 +839,7 @@ interactions: connection: - keep-alive content-length: - - '10466' + - '19656' content-type: - application/json host: @@ -755,15 +850,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -783,6 +876,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -796,7 +909,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -804,17 +917,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -830,44 +941,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -877,52 +981,128 @@ interactions: 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. + 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. + reasoning: We need to search for content about document element types or labels. Likely many documents mention e.g. + "title", "section_header", "list_item", etc. We should search for terms like "element types" or "labels". Let's + 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"}' + arguments: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n"}' name: execute_code - id: call_j7uw3i6n + id: call_4k3t2n10 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}' + - content: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n","stdout":"[{''chunk_id'': + ''4b9cb6a5-f203-4070-8b2f-ab3ef12dde1b'', ''content'': ''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 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}$, $_{Section-header}$, + $_{Table}$, $_{Text}$, and $_{Title}$. Critical factors that were considered for the choice of these class labels + were (1) the overall occurrence of the label, (2) the specificity of the label, (3) recognisability on a single + page (i.e. no need for context from previous or next page) and (4) overall coverage of the page. Specificity ensures + that the choice of label is not ambiguous, while coverage ensures that all meaningful items on a page can be annotated. + We refrained from class labels that are very specific to a document category, such as Abstract in the Scientific + Articles category. We also avoided class labels that are tightly linked to the semantics of the text. Labels such + as Author and'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.032786883413791656, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''fdc2266a-b812-48c4-a49a-ece08a348ead'', ''content'': + ''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 include publication repositories + such as arXiv$^{3}$, government offices, company websites as well as data directory services for financial reports + and patents. Scanned documents were excluded wherever possible because they can be rotated or skewed. This would + not allow us to perform annotation with rectangular bounding-boxes and therefore complicate the annotation process.'', + ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.0320020467042923, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''0b6492ef-bece-4486-98ce-85280c3b2667'', + ''content'': ''$_{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 (CCS) [22], a cloud-native + platform which provides a visual annotation interface and allows for dataset inspection and analysis. The annotation + interface of CCS is shown in Figure 3. The desired balance of pages between the different document categories was + achieved by selective subsampling of pages with certain desired properties. For example, we made sure to include + the title page of each document and bias the remaining page selection to those with figures or tables. The latter + was achieved by leveraging pre-trained object detection models from PubLayNet, which helped us estimate how many + figures and tables a given page contains.\\n$^{3}$https://arxiv.org/'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.03036576882004738, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''940f11c1-5028-4dd4-9515-781f1b9cdc2a'', + ''content'': ''\\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.'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.016129031777381897, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''eba516e5-277f-487b-bcb9-3caea945ac54'', + ''content'': ''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 = 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 (%).Man = 90-94. Page-header, triple inter-annotator mAP @ 0.5-0.95 + (%).Sci = 98-100. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 91-92. Page-header, triple inter-annotator + mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.015625, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''63d5d7a0-e9e6-4258-9c76-d97689acffb0'', ''content'': + ''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, % 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 = 75-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Man + = 83-86. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 98-99. Table, triple'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.015384615398943424, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''48c416f5-db1e-47c9-9f5c-0caf7b36e568'', + ''content'': ''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, 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-annotator mAP @ 0.5-0.95 (%).Law = 95-99. + Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-78. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + ='', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.01515151560306549, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''f931ae1e-5413-4b26-a8cc-14ce5ce7bdc1'', + ''content'': ''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, % 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 = 76-81. Section-header, triple inter-annotator + mAP @ 0.5-0.95 (%).Man = 90-92. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 94-95. Section-header, + triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.014925372786819935, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''b8056bae-cc3b-4674-a7df-44d918eb4aea'', + ''content'': ''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. 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, triple inter-annotator mAP @ 0.5-0.95 (%).Law = + 85-94. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = n/a. Footnote, triple inter-annotator mAP @ 0.5-0.95 + (%).Ten'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.014705882407724857, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''917de523-f069-49fa-ad71-1f0886db460c'', ''content'': + ''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 inter-annotator mAP @ 0.5-0.95 + (%).Fin = 74-83. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Man = 90-92. List-item, triple inter-annotator + mAP @ 0.5-0.95 (%).Sci = 97-97. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 81-85. List-item, triple + inter-annotator mAP @ 0.5-0.95 (%).Pat = 75-88. List-item, triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.014285714365541935, ''page_numbers'': [1], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_j7uw3i6n + tool_call_id: call_4k3t2n10 - 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. + reasoning: 'We got list of chunks with labels info. The element types mentioned in that chunk: Caption, Footnote, + Formula, List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also maybe others like + Affiliation but not part of 11 class labels. So answer list unique types. Let''s list: Caption, Footnote, Formula, + List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also mention that these are the + 11 distinct class labels. Provide answer and program summarizing retrieval of labels. Let''s write program: search + for that chunk, extract from content using regex to find labels. Use regex pattern r''\b[_\{]([A-Za-z-]+)[_\}]'' + or simpler from known labels list. But we can hardcode extraction: search patterns like ''$_{Caption}$'', etc. We''ll + collect unique.' 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"}' + arguments: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n"}' name: execute_code - id: call_gzg850pl + id: call_jxgy76dr 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: + - content: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n","stdout":"","stderr":"ModuleNotFoundError: No module named ''re''","success":false}' role: tool - tool_call_id: call_gzg850pl + tool_call_id: call_jxgy76dr model: gpt-oss reasoning_effort: low stream: false @@ -973,7 +1153,7 @@ interactions: response: headers: content-length: - - '704' + - '721' content-type: - application/json parsed_body: @@ -982,25 +1162,25 @@ interactions: index: 0 message: content: '' - reasoning: Import not allowed. Use string methods. Probably easier to manually inspect. Let's list results. + reasoning: Cannot import re. Use regex_* functions. Use regex_findall. 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"}' + arguments: '{"code":"results = await search(''11 class labels'', limit=5)\nlabels=set()\nfor r in results:\n matches + = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(sorted(labels))\n"}' name: execute_code - id: call_4s9httx6 + id: call_oyc93cba index: 0 type: function - created: 1771924572 - id: chatcmpl-410 + created: 1772548416 + id: chatcmpl-210 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 92 - prompt_tokens: 2421 - total_tokens: 2513 + completion_tokens: 103 + prompt_tokens: 5486 + total_tokens: 5589 status: code: 200 message: OK @@ -1013,7 +1193,7 @@ interactions: connection: - keep-alive content-length: - - '75' + - '85' content-type: - application/json host: @@ -1022,7 +1202,7 @@ interactions: parsed_body: encoding_format: base64 input: - - label + - 11 class labels model: qwen3-embedding:4b uri: http://localhost:11434/v1/embeddings response: @@ -1033,14 +1213,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: 2 - total_tokens: 2 + prompt_tokens: 5 + total_tokens: 5 status: code: 200 message: OK @@ -1053,7 +1233,7 @@ interactions: connection: - keep-alive content-length: - - '15687' + - '20587' content-type: - application/json host: @@ -1064,15 +1244,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -1092,6 +1270,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -1105,7 +1303,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -1113,17 +1311,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -1139,44 +1335,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -1186,104 +1375,144 @@ interactions: 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. + 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. + reasoning: We need to search for content about document element types or labels. Likely many documents mention e.g. + "title", "section_header", "list_item", etc. We should search for terms like "element types" or "labels". Let's + 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"}' + arguments: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n"}' name: execute_code - id: call_j7uw3i6n + id: call_4k3t2n10 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}' + - content: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n","stdout":"[{''chunk_id'': + ''4b9cb6a5-f203-4070-8b2f-ab3ef12dde1b'', ''content'': ''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 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}$, $_{Section-header}$, + $_{Table}$, $_{Text}$, and $_{Title}$. Critical factors that were considered for the choice of these class labels + were (1) the overall occurrence of the label, (2) the specificity of the label, (3) recognisability on a single + page (i.e. no need for context from previous or next page) and (4) overall coverage of the page. Specificity ensures + that the choice of label is not ambiguous, while coverage ensures that all meaningful items on a page can be annotated. + We refrained from class labels that are very specific to a document category, such as Abstract in the Scientific + Articles category. We also avoided class labels that are tightly linked to the semantics of the text. Labels such + as Author and'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.032786883413791656, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''fdc2266a-b812-48c4-a49a-ece08a348ead'', ''content'': + ''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 include publication repositories + such as arXiv$^{3}$, government offices, company websites as well as data directory services for financial reports + and patents. Scanned documents were excluded wherever possible because they can be rotated or skewed. This would + not allow us to perform annotation with rectangular bounding-boxes and therefore complicate the annotation process.'', + ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.0320020467042923, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''0b6492ef-bece-4486-98ce-85280c3b2667'', + ''content'': ''$_{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 (CCS) [22], a cloud-native + platform which provides a visual annotation interface and allows for dataset inspection and analysis. The annotation + interface of CCS is shown in Figure 3. The desired balance of pages between the different document categories was + achieved by selective subsampling of pages with certain desired properties. For example, we made sure to include + the title page of each document and bias the remaining page selection to those with figures or tables. The latter + was achieved by leveraging pre-trained object detection models from PubLayNet, which helped us estimate how many + figures and tables a given page contains.\\n$^{3}$https://arxiv.org/'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.03036576882004738, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''940f11c1-5028-4dd4-9515-781f1b9cdc2a'', + ''content'': ''\\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.'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.016129031777381897, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''eba516e5-277f-487b-bcb9-3caea945ac54'', + ''content'': ''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 = 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 (%).Man = 90-94. Page-header, triple inter-annotator mAP @ 0.5-0.95 + (%).Sci = 98-100. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 91-92. Page-header, triple inter-annotator + mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.015625, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''63d5d7a0-e9e6-4258-9c76-d97689acffb0'', ''content'': + ''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, % 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 = 75-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Man + = 83-86. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 98-99. Table, triple'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.015384615398943424, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''48c416f5-db1e-47c9-9f5c-0caf7b36e568'', + ''content'': ''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, 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-annotator mAP @ 0.5-0.95 (%).Law = 95-99. + Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-78. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + ='', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.01515151560306549, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''f931ae1e-5413-4b26-a8cc-14ce5ce7bdc1'', + ''content'': ''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, % 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 = 76-81. Section-header, triple inter-annotator + mAP @ 0.5-0.95 (%).Man = 90-92. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 94-95. Section-header, + triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.014925372786819935, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''b8056bae-cc3b-4674-a7df-44d918eb4aea'', + ''content'': ''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. 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, triple inter-annotator mAP @ 0.5-0.95 (%).Law = + 85-94. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = n/a. Footnote, triple inter-annotator mAP @ 0.5-0.95 + (%).Ten'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.014705882407724857, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''917de523-f069-49fa-ad71-1f0886db460c'', ''content'': + ''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 inter-annotator mAP @ 0.5-0.95 + (%).Fin = 74-83. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Man = 90-92. List-item, triple inter-annotator + mAP @ 0.5-0.95 (%).Sci = 97-97. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 81-85. List-item, triple + inter-annotator mAP @ 0.5-0.95 (%).Pat = 75-88. List-item, triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.014285714365541935, ''page_numbers'': [1], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_j7uw3i6n + tool_call_id: call_4k3t2n10 - 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. + reasoning: 'We got list of chunks with labels info. The element types mentioned in that chunk: Caption, Footnote, + Formula, List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also maybe others like + Affiliation but not part of 11 class labels. So answer list unique types. Let''s list: Caption, Footnote, Formula, + List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also mention that these are the + 11 distinct class labels. Provide answer and program summarizing retrieval of labels. Let''s write program: search + for that chunk, extract from content using regex to find labels. Use regex pattern r''\b[_\{]([A-Za-z-]+)[_\}]'' + or simpler from known labels list. But we can hardcode extraction: search patterns like ''$_{Caption}$'', etc. We''ll + collect unique.' 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"}' + arguments: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n"}' name: execute_code - id: call_gzg850pl + id: call_jxgy76dr 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: + - content: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n","stdout":"","stderr":"ModuleNotFoundError: No module named ''re''","success":false}' role: tool - tool_call_id: call_gzg850pl + tool_call_id: call_jxgy76dr - content: null - reasoning: Import not allowed. Use string methods. Probably easier to manually inspect. Let's list results. + reasoning: Cannot import re. Use regex_* functions. Use regex_findall. 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"}' + arguments: '{"code":"results = await search(''11 class labels'', limit=5)\nlabels=set()\nfor r in results:\n matches + = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(sorted(labels))\n"}' name: execute_code - id: call_4s9httx6 + id: call_oyc93cba 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}' + - content: '{"code":"results = await search(''11 class labels'', limit=5)\nlabels=set()\nfor r in results:\n matches + = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(sorted(labels))\n","stdout":"[''Caption'', + ''Footnote'', ''Formula'', ''List-item'', ''Page-header'', ''Picture'', ''Section-header'', ''Table'', ''Text'', + ''Title'', ''footer'']\n","stderr":"","success":true}' role: tool - tool_call_id: call_4s9httx6 + tool_call_id: call_oyc93cba model: gpt-oss reasoning_effort: low stream: false @@ -1334,345 +1563,7 @@ interactions: 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' + - '1530' content-type: - application/json parsed_body: @@ -1680,28 +1571,422 @@ interactions: - finish_reason: stop index: 0 message: - 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. + content: '{"answer":"The documents mention eleven distinct document element types (class labels). They are:\n1. + Caption\n2. Footnote\n3. Formula\n4. List‑item\n5. Page‑header\n6. Page‑footer\n7. Picture\n8. Section‑header\n9. + Table\n10. Text\n11. Title\n\nThese labels are used as the primary structural categories for annotating content + in the dataset.\n","program":"import json\n\n# Retrieve the relevant chunk that lists the 11 class labels\nresults + = await search(\"11 class labels\", limit=5)\n\n# Extract the label names using a regex that captures words inside + \\{\\}\\_\nlabels = set()\nfor r in results:\n matches = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', + r[''content''])\n labels.update(matches)\n\n# Convert to a sorted list for consistent display\nlabel_list = + sorted(labels)\n\n# Print the labels in a numbered list (JSON friendly)\noutput = \"\\n\".join(f\"{i+1}. {name}\" + for i, name in enumerate(label_list))\nprint(output)\n"}' + reasoning: 'It extracted ''footer'' separately. But Page-footer might be footer? Keep list: Caption, Footnote, Formula, + List-item, Page-header, Page-footer (footer), Picture, Section-header, Table, Text, Title. Provide answer.' role: assistant - created: 1771924587 - id: chatcmpl-392 + created: 1772548424 + id: chatcmpl-943 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 398 - prompt_tokens: 4547 - total_tokens: 4945 + completion_tokens: 316 + prompt_tokens: 5718 + total_tokens: 6034 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '21992' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: 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: We need to search for content about document element types or labels. Likely many documents mention e.g. + "title", "section_header", "list_item", etc. We should search for terms like "element types" or "labels". Let's + search. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n"}' + name: execute_code + id: call_4k3t2n10 + type: function + - content: '{"code":"results = await search(\"document element types\", limit=10)\nprint(results)\n","stdout":"[{''chunk_id'': + ''4b9cb6a5-f203-4070-8b2f-ab3ef12dde1b'', ''content'': ''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 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}$, $_{Section-header}$, + $_{Table}$, $_{Text}$, and $_{Title}$. Critical factors that were considered for the choice of these class labels + were (1) the overall occurrence of the label, (2) the specificity of the label, (3) recognisability on a single + page (i.e. no need for context from previous or next page) and (4) overall coverage of the page. Specificity ensures + that the choice of label is not ambiguous, while coverage ensures that all meaningful items on a page can be annotated. + We refrained from class labels that are very specific to a document category, such as Abstract in the Scientific + Articles category. We also avoided class labels that are tightly linked to the semantics of the text. Labels such + as Author and'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.032786883413791656, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''fdc2266a-b812-48c4-a49a-ece08a348ead'', ''content'': + ''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 include publication repositories + such as arXiv$^{3}$, government offices, company websites as well as data directory services for financial reports + and patents. Scanned documents were excluded wherever possible because they can be rotated or skewed. This would + not allow us to perform annotation with rectangular bounding-boxes and therefore complicate the annotation process.'', + ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.0320020467042923, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''0b6492ef-bece-4486-98ce-85280c3b2667'', + ''content'': ''$_{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 (CCS) [22], a cloud-native + platform which provides a visual annotation interface and allows for dataset inspection and analysis. The annotation + interface of CCS is shown in Figure 3. The desired balance of pages between the different document categories was + achieved by selective subsampling of pages with certain desired properties. For example, we made sure to include + the title page of each document and bias the remaining page selection to those with figures or tables. The latter + was achieved by leveraging pre-trained object detection models from PubLayNet, which helped us estimate how many + figures and tables a given page contains.\\n$^{3}$https://arxiv.org/'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.03036576882004738, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''940f11c1-5028-4dd4-9515-781f1b9cdc2a'', + ''content'': ''\\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.'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.016129031777381897, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''eba516e5-277f-487b-bcb9-3caea945ac54'', + ''content'': ''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 = 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 (%).Man = 90-94. Page-header, triple inter-annotator mAP @ 0.5-0.95 + (%).Sci = 98-100. Page-header, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 91-92. Page-header, triple inter-annotator + mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.015625, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''63d5d7a0-e9e6-4258-9c76-d97689acffb0'', ''content'': + ''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, % 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 = 75-80. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Man + = 83-86. Table, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 98-99. Table, triple'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.015384615398943424, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''48c416f5-db1e-47c9-9f5c-0caf7b36e568'', + ''content'': ''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, 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-annotator mAP @ 0.5-0.95 (%).Law = 95-99. + Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = 69-78. Caption, triple inter-annotator mAP @ 0.5-0.95 (%).Ten + ='', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.01515151560306549, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''f931ae1e-5413-4b26-a8cc-14ce5ce7bdc1'', + ''content'': ''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, % 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 = 76-81. Section-header, triple inter-annotator + mAP @ 0.5-0.95 (%).Man = 90-92. Section-header, triple inter-annotator mAP @ 0.5-0.95 (%).Sci = 94-95. Section-header, + triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, + ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': + 0.014925372786819935, ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''b8056bae-cc3b-4674-a7df-44d918eb4aea'', + ''content'': ''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. 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, triple inter-annotator mAP @ 0.5-0.95 (%).Law = + 85-94. Footnote, triple inter-annotator mAP @ 0.5-0.95 (%).Pat = n/a. Footnote, triple inter-annotator mAP @ 0.5-0.95 + (%).Ten'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', ''document_title'': None, ''document_uri'': + ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', ''score'': 0.014705882407724857, + ''page_numbers'': [1], ''headings'': None}, {''chunk_id'': ''917de523-f069-49fa-ad71-1f0886db460c'', ''content'': + ''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 inter-annotator mAP @ 0.5-0.95 + (%).Fin = 74-83. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Man = 90-92. List-item, triple inter-annotator + mAP @ 0.5-0.95 (%).Sci = 97-97. List-item, triple inter-annotator mAP @ 0.5-0.95 (%).Law = 81-85. List-item, triple + inter-annotator mAP @ 0.5-0.95 (%).Pat = 75-88. List-item, triple inter-annotator mAP @'', ''document_id'': ''b6b9012d-56d5-4aba-a9c7-e8ab027b40db'', + ''document_title'': None, ''document_uri'': ''file:///Users/ggozad/dev/open-source/haiku.rag-fix-tool-calls/tests/data/doclaynet.pdf'', + ''score'': 0.014285714365541935, ''page_numbers'': [1], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_4k3t2n10 + - content: null + reasoning: 'We got list of chunks with labels info. The element types mentioned in that chunk: Caption, Footnote, + Formula, List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also maybe others like + Affiliation but not part of 11 class labels. So answer list unique types. Let''s list: Caption, Footnote, Formula, + List-item, Page-footer, Page-header, Picture, Section-header, Table, Text, Title. Also mention that these are the + 11 distinct class labels. Provide answer and program summarizing retrieval of labels. Let''s write program: search + for that chunk, extract from content using regex to find labels. Use regex pattern r''\b[_\{]([A-Za-z-]+)[_\}]'' + or simpler from known labels list. But we can hardcode extraction: search patterns like ''$_{Caption}$'', etc. We''ll + collect unique.' + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n"}' + name: execute_code + id: call_jxgy76dr + type: function + - content: '{"code":"results = await search(\"11 class labels\" , limit=5)\nlabels=set()\nfor r in results:\n import + re\n matches=re.findall(r''\\b_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(labels)\n","stdout":"","stderr":"ModuleNotFoundError: + No module named ''re''","success":false}' + role: tool + tool_call_id: call_jxgy76dr + - content: null + reasoning: Cannot import re. Use regex_* functions. Use regex_findall. + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(''11 class labels'', limit=5)\nlabels=set()\nfor r in results:\n matches + = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(sorted(labels))\n"}' + name: execute_code + id: call_oyc93cba + type: function + - content: '{"code":"results = await search(''11 class labels'', limit=5)\nlabels=set()\nfor r in results:\n matches + = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\nprint(sorted(labels))\n","stdout":"[''Caption'', + ''Footnote'', ''Formula'', ''List-item'', ''Page-header'', ''Picture'', ''Section-header'', ''Table'', ''Text'', + ''Title'', ''footer'']\n","stderr":"","success":true}' + role: tool + tool_call_id: call_oyc93cba + - content: '{"answer":"The documents mention eleven distinct document element types (class labels). They are:\n1. Caption\n2. + Footnote\n3. Formula\n4. List‑item\n5. Page‑header\n6. Page‑footer\n7. Picture\n8. Section‑header\n9. Table\n10. + Text\n11. Title\n\nThese labels are used as the primary structural categories for annotating content in the dataset.\n","program":"import + json\n\n# Retrieve the relevant chunk that lists the 11 class labels\nresults = await search(\"11 class labels\", + limit=5)\n\n# Extract the label names using a regex that captures words inside \\{\\}\\_\nlabels = set()\nfor r + in results:\n matches = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', r[''content''])\n labels.update(matches)\n\n# + Convert to a sorted list for consistent display\nlabel_list = sorted(labels)\n\n# Print the labels in a numbered + list (JSON friendly)\noutput = \"\\n\".join(f\"{i+1}. {name}\" for i, name in enumerate(label_list))\nprint(output)\n"}' + reasoning: 'It extracted ''footer'' separately. But Page-footer might be footer? Keep list: Caption, Footnote, Formula, + List-item, Page-header, Page-footer (footer), Picture, Section-header, Table, Text, Title. Provide answer.' + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '1592' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to wrap answer in execute_code? We need to output JSON via tool call? Must return tool call. Use + function final_result. We'll provide final_result. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"The documents mention eleven distinct document element types (class labels). They are:\n1. + Caption\n2. Footnote\n3. Formula\n4. List‑item\n5. Page‑header\n6. Page‑footer\n7. Picture\n8. Section‑header\n9. + Table\n10. Text\n11. Title\n\nThese labels are used as the primary structural categories for annotating content + in the dataset.\n","program":"import json\n\n# Retrieve the relevant chunk that lists the 11 class labels\nresults + = await search(\"11 class labels\", limit=5)\n\n# Extract the label names using a regex that captures words + inside \\{\\}\\_\nlabels = set()\nfor r in results:\n matches = await regex_findall(r''_{\\s*([A-Za-z-]+)\\s*}'', + r[''content''])\n labels.update(matches)\n\n# Convert to a sorted list for consistent display\nlabel_list + = sorted(labels)\n\n# Print the labels in a numbered list (JSON friendly)\noutput = \"\\n\".join(f\"{i+1}. + {name}\" for i, name in enumerate(label_list))\nprint(output)\n"}' + name: final_result + id: call_sgf8xfq2 + index: 0 + type: function + created: 1772548439 + id: chatcmpl-437 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 308 + prompt_tokens: 5737 + total_tokens: 6045 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 3e87ecb1..2746a19c 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: - - '7359' + - '7738' content-type: - application/json host: @@ -59,15 +59,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -87,6 +85,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -100,7 +118,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -108,17 +126,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -134,44 +150,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -181,7 +190,7 @@ interactions: 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. + 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 @@ -235,7 +244,7 @@ interactions: response: headers: content-length: - - '537' + - '625' content-type: - application/json parsed_body: @@ -244,24 +253,25 @@ interactions: index: 0 message: content: '' - reasoning: We need to search for animals. + reasoning: Need to search for "animals". role: assistant tool_calls: - function: - arguments: '{"code":"results = await search(\"animals\", limit=10)\nprint(results)\n"}' + arguments: '{"code":"results = await search(\"animals\", limit=5)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], + r[''score''])\nprint(results[:5])"}' name: execute_code - id: call_pvjujvr9 + id: call_4vtaz637 index: 0 type: function - created: 1771924521 - id: chatcmpl-217 + created: 1772549356 + id: chatcmpl-763 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 46 - prompt_tokens: 1629 - total_tokens: 1675 + completion_tokens: 72 + prompt_tokens: 1736 + total_tokens: 1808 status: code: 200 message: OK @@ -314,7 +324,7 @@ interactions: connection: - keep-alive content-length: - - '8119' + - '8713' content-type: - application/json host: @@ -325,15 +335,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -353,6 +361,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -366,7 +394,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -374,17 +402,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -400,44 +426,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -447,25 +466,27 @@ interactions: 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. + 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 for animals. + reasoning: Need to search for "animals". role: assistant tool_calls: - function: - arguments: '{"code":"results = await search(\"animals\", limit=10)\nprint(results)\n"}' + arguments: '{"code":"results = await search(\"animals\", limit=5)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], + r[''score''])\nprint(results[:5])"}' name: execute_code - id: call_pvjujvr9 + id: call_4vtaz637 type: function - - 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'', + - content: '{"code":"results = await search(\"animals\", limit=5)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], + r[''score''])\nprint(results[:5])","stdout":"1\nAnimal Facts 0.016393441706895828\n[{''chunk_id'': ''69018e55-d141-4f97-89bb-7d7a19ffc273'', + ''content'': ''The quick brown fox jumps over the lazy dog.'', ''document_id'': ''7a3553dd-19ff-4660-8ff1-23c7e9b3aa89'', ''document_title'': ''Animal Facts'', ''document_uri'': None, ''score'': 0.016393441706895828, ''page_numbers'': [], ''headings'': None}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_pvjujvr9 + tool_call_id: call_4vtaz637 model: gpt-oss reasoning_effort: low stream: false @@ -516,7 +537,7 @@ interactions: response: headers: content-length: - - '888' + - '1314' content-type: - application/json parsed_body: @@ -524,21 +545,298 @@ interactions: - finish_reason: stop index: 0 message: - 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. + content: '{"answer":"The search result for the query “animals” came from the document titled **\"Animal Facts\"**. + The relevant chunk excerpted is: “The quick brown fox jumps over the lazy dog.”.","program":"import asyncio\n\nasync + def main():\n # Search for the term \"animals\" in the knowledge base\n results = await search(\"animals\", + limit=5)\n # Print the number of results found\n print(f\"Found {len(results)} result(s).\")\n # Output + the source document for each result\n for r in results:\n print(f\"Chunk ID: {r[''chunk_id'']}\")\n print(f\"Document + ID: {r[''document_id'']}\")\n print(f\"Document Title: {r[''document_title'']}\")\n print(f\"Score: + {r[''score'']:.6f}\")\n print(f\"Excerpt: {r[''content''][:200]}\\n\")\n\nif __name__ == \"__main__\":\n asyncio.run(main())"}' + reasoning: The only result is chunk about fox and dog from document Animal Facts. Provide answer and program that + runs search. role: assistant - created: 1771924525 - id: chatcmpl-109 + created: 1772549361 + id: chatcmpl-58 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 168 - prompt_tokens: 1822 - total_tokens: 1990 + completion_tokens: 265 + prompt_tokens: 2001 + total_tokens: 2266 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '9903' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Search for content about animals and tell me which document it came from. + role: user + - content: null + reasoning: Need to search for "animals". + role: assistant + tool_calls: + - function: + arguments: '{"code":"results = await search(\"animals\", limit=5)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], + r[''score''])\nprint(results[:5])"}' + name: execute_code + id: call_4vtaz637 + type: function + - content: '{"code":"results = await search(\"animals\", limit=5)\nprint(len(results))\nfor r in results:\n print(r[''document_title''], + r[''score''])\nprint(results[:5])","stdout":"1\nAnimal Facts 0.016393441706895828\n[{''chunk_id'': ''69018e55-d141-4f97-89bb-7d7a19ffc273'', + ''content'': ''The quick brown fox jumps over the lazy dog.'', ''document_id'': ''7a3553dd-19ff-4660-8ff1-23c7e9b3aa89'', + ''document_title'': ''Animal Facts'', ''document_uri'': None, ''score'': 0.016393441706895828, ''page_numbers'': + [], ''headings'': None}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_4vtaz637 + - content: '{"answer":"The search result for the query “animals” came from the document titled **\"Animal Facts\"**. + The relevant chunk excerpted is: “The quick brown fox jumps over the lazy dog.”.","program":"import asyncio\n\nasync + def main():\n # Search for the term \"animals\" in the knowledge base\n results = await search(\"animals\", + limit=5)\n # Print the number of results found\n print(f\"Found {len(results)} result(s).\")\n # Output + the source document for each result\n for r in results:\n print(f\"Chunk ID: {r[''chunk_id'']}\")\n print(f\"Document + ID: {r[''document_id'']}\")\n print(f\"Document Title: {r[''document_title'']}\")\n print(f\"Score: + {r[''score'']:.6f}\")\n print(f\"Excerpt: {r[''content''][:200]}\\n\")\n\nif __name__ == \"__main__\":\n asyncio.run(main())"}' + reasoning: The only result is chunk about fox and dog from document Animal Facts. Provide answer and program that + runs search. + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '1233' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to output JSON via tool call, not plain. Use function final_result. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"The search result for the query “animals” came from the document titled *\"Animal Facts\"*. + The relevant chunk excerpted is: “The quick brown fox jumps over the lazy dog.”.","program":"import asyncio\n\nasync + def main():\n results = await search(\"animals\", limit=5)\n print(f\"Found {len(results)} result(s).\")\n for + r in results:\n print(f\"Chunk ID: {r[''chunk_id'']}\")\n print(f\"Document ID: {r[''document_id'']}\")\n print(f\"Document + Title: {r[''document_title'']}\")\n print(f\"Score: {r[''score'']:.6f}\")\n print(f\"Excerpt: + {r[''content''][:200]}\\n\")\n\nif __name__ == \"__main__\":\n asyncio.run(main())"}' + name: final_result + id: call_y5fedbyn + index: 0 + type: function + created: 1772549367 + id: chatcmpl-895 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 231 + prompt_tokens: 2247 + total_tokens: 2478 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 375c1602..c329ed7d 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: - - '7387' + - '7766' content-type: - application/json host: @@ -142,15 +142,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -170,6 +168,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -183,7 +201,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -191,17 +209,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -217,44 +233,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -264,7 +273,7 @@ interactions: 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. + 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 @@ -318,7 +327,7 @@ interactions: response: headers: content-length: - - '677' + - '697' content-type: - application/json parsed_body: @@ -327,25 +336,25 @@ interactions: index: 0 message: content: '' - reasoning: Need to search for quarterly updates. Let's search "quarterly update". + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' name: execute_code - id: call_dwvve4zt + id: call_b39cif5o index: 0 type: function - created: 1771924530 - id: chatcmpl-596 + created: 1772549382 + id: chatcmpl-974 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 84 - prompt_tokens: 1634 - total_tokens: 1718 + completion_tokens: 91 + prompt_tokens: 1741 + total_tokens: 1832 status: code: 200 message: OK @@ -358,47 +367,7 @@ interactions: connection: - keep-alive content-length: - - '86' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - quarterly update - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: XjueuJ8K5jyHv9k7oRDPPEcGhrmFaEc9xwJTPT3cpTyFtmg891PQPIPKijzgHwQ7GjV0tzM7TrxJp6U8wytRvaTIBb2WkrU8UMhLPEmkqbvO3na7bDpKPPtJ6TxtmrC8oivBvM39M71OBKW8wCPAvZSnbLviUMw8fMJYvXab8rwduhg9YFeau3wYEDu3K7268Cuju/XC+Lsg8Wy8JgN0OmNmnTyesKm8gPgzO5DbcrvuZmw82cUNPUEfLzwV9E88fsaBvOdE0LtPiRA8o6QrPMQiMr3VNgS9QV4FPOweirzAwms9u0F/u7ylFL3izgG8nzevOqkc/zveuVi8+vSsO8ocrLsRU++86cOqvN0slr14dN07fCGUPDtACLyB51w87Io8vNrqDryZ7Ig8mbG4vOa6NrzZ3iA9IYvbOUyk7zuLNQI90RO1O6BQ57vuiJW8A45bPN1tlzo0Ahe6icUwOyCUQ72BXKG7YHsaPGWDu7s3ueE7kRCZPI5gCzybNy27x4/7u+0tobz1zjW704kPPHUN6bv/bxW6jdXzvFytnrwiZ7+832oFvSmhsLzZN/K7LoWLO/3l4jtZIXa86uv+u0y0lTzRMnS8NfURvKl+4TuxzDk9Wg+PPI5EjTtk8IW8Q2jku07SqjxfmgU8mpzeO65UMDz4fFa7qn94vJ6kfrsc4/u70AusPCsDAjyokIC8YvQnvMS7k7xATQu8xn8ovPsHCzqJFKI7L4dyvBjA4TxFiIO7WkLWu/GiKDxWLFe5t82KvIQNDLyQUvU6gF5OOjRUrjtJeAU8zNO0PA6LILz5DqS5tJZ2PAYxFzsd578802OaOxX5hDxwpdS76hPtPLfzkrv9rJI8yKgjvIg6BDxonHM8akRLPK4U6rywya87tRZ9vN8I77wjqfc5W9xdvPcLdrwmO6K8NzKivNfxVTywYve8OCsxPIYvHbxVQBs8S+6tu19C17svXqM7to72uhkTkDvze3Q8wHNEu6TiqjscMG86lJ/sO6hDVb3uSeo7PRHuO4OZyDsNTZY7SMGuvPf2+bwfzWs8sNT3uuVKJzy4c0o863AMO8bUcTt/KT28z3lUvAZTgTyGXei76YIwvCVehTwBUrq81CLaPL+AFLlm+xa8Z77NvOC6hjyFpVQ8/0GvvGrEmbu1TrE8TryvPWuqazxwGZi6xQdsvMuwbjx5vFq9Sd2JO2nwkjscOQk7pp5xPHomSjxH2ug8ABi9PItgmrx1fvG74eO3OzaJbzoQxXO6WqqFu9YsDryArX+7lrsGvEs1EryerZk6wkp1PCUsRLsl8w68SXeXui3ENLzjlI28qR8aOovrj7zpHp889IqFPC8M1Lunhv+7sdTXPEsSurx9Ml+8Asj4OUGECjvKuCo89GABPZi/0Lz/Uk27jGKxOeIXAr3MOQk8YFh2vAHGgDw9PQc74lYSO0hqUrwQUiy8Q6b4O1jOIjzMuLg7DGtgu9DERDw49L07bnkOPetZsLwvLTA6kGGuPHAadDuVVwA8zBW8PIk+u7zQYVU8s1uZvPJMHLyYyYY8cIJrvPH3nzzbej07yfidu4ZdIr2tGb07dxmJvDv327v9x8G7/QYYPBCHjDy7Uhi79ZuhOoITLDvXWJu8ebqQO1mHHTz45VY8cDkUvfwh0bwxDfu7k5l0u/zzk7t6+o26V+8GvYcKTLy6kXA8zcKpvCw9Cr27ECq8r/yBvQ2Nk7u50em6J6ktPQr5xzv/mNg7IMNyPDYp8jv6eyA8cqcAvAdPDz3lnvy8kGWkPPKtxruZ/p686MYgupg3Az0TPGI8cMjwO1M9jroJZQA864Juu+ij27yRBoK9byuzvJZoBrubcCU8fb+1vMXjPbxiS2g8op/2vBNbnzzlm5S8YR2svAo10zzNroi8BWegOx8u07pvP105t7dbvPGskzuBGHu8djKWvANsibqZrbG6A1qPvEGYBj1+3h69W3DBu2qhVzpmtY68tsocPA0567zoULi7+6CEu0SZ/jubrqs7c/6xuhKOj7sHW+k8wKYwPV3QOryDBU887yfivK31LTv9fzy8nyYvPIX8e7yO25A8PrF7vIjCk7tpDH08kJhOOW1qS7zX0RM9Gn9ZO5KNwjmxwD68kLGCvF1M57xkw868OOPvvK8F8rwXKkK8wnXkvLCXaLvGaYM9UUCDvEnRXLtD0My8fZwwPaVdI7z5GZu7edYRvdIkHjsRpZQ7MN7RvFGZ2bxDlvg8u/YnO19Kobuapok6URJZvL/kwjumszg8B7MYPNB3jjyRHRa96X+FOxKTyDsdBTM85R7wO4W+LzvfqJM73jr2PFqtTDyFaZe80ao+vPWb+7xlgLa8hTWpvDfiozwkf888f2g/vSLChbu4oCG7u9TKvIyQTj26eTS8ZPxSvDgS+7yVsr65RDfwupSOoryEyQo8yM8/PHvrsDuf7me8AvuaPPT+ob2sMVM8n9oGvGBFPLzsGoW8puhpvPu0w7xhGYa8UxFvPA+jZD27S268kG8zvLF9o7vnsyE8LrEcPBVq4TzJiLo763tlvOkOI7tUKvY8XGg9PE9mS7iOEb88hx82PW4gSTzbr807+2pnOq35JDygckk9UrcevOPOxjw+Cao8nlxZvFSyq7ySxDc8RaGSvKT4JzylWVq9GVSFu4+KBTy44Ss7q6vZPAsGTLyxAce7gYO0PKyGED1u8sY79O3vu+Vhq7zEGpE8GKajPPsGgTxPsB+8b8IXvX1+CTzYUxI8BHAVvfQuOjtimq48rDgyvb+pEr3C7888GVfQOwEob7xitke9TjoxPT7J5LzFEkg8sNdAO0N20TwcxCy7hD6evEI3wrwuWss6KTIivaaeo7yE0rM7d60HPStDn7zXsk6896BCuxPRo7rfQSg8KzNcvOFjTz1GUog8/ZUeO0qUIL2J3D29nbw2PUMNNzt0h6+7lO2VvDyXBb0lDoG8yQkJPWqJXLxsh8Y7NA3Puss8NTyTPzc9xTqkvLyPfDxMrek8DwDUO2cZ5LvJc7I8+nhlvH3KFzwl9mK8Q3KdvKqPmTync/e7dLqUPD3sijzMJsi8775FvDeWlbxPhoq8i2PHu1lHHDrXJ008Soy7PLHiBLzuuSw7LiUsvE4crrwBwRw8aiiuPJBVyDvRnKW8fj2rOhm6vDzvivu7obWlPFnoDL3HVIw8lEhWO9xJIb3/f6e7+BdKPfVtprwcC4+8x/0nO3TQRr3H8Yc8N87aOzPBAD3oMuq5F6iaPMMGijwbP7g7JFasu4aJCztj3fG75SVDvIIi0bt+oEK8E7OxvIUSnDxDj+G8bkGTvOJ0AzzVI1g83tIvurLK07wusV28dsCSvDAJHjkYiZS8OtLIO8gUvbum8C89DGidOyA/+jsWzXG7zuwlveGXjzz+qPk76WaFvPaVJDxnu5E7mQJUPA9zTzzT8Wu7v520vIWxk7wzhD48r2S+vCgS1Dzq/DW80ThwPLv2TzvP+xu8l7+hPMDmAb1SVoA8ZdUOvZRRDz1ZVig7RQkuvGenN7zfHww8E0i7PJ18ib00VgK8hBrSvHiOQr2XIWo9xti/uyeWXjdDkQS8Hlokvek+WLzcZAo96JWevFqtjTvT0287hCsrPFRyObz7wMA7KPFluhZNQLyvxA87ceDGPBMAhjz4+xk88mgIO5ChZjyScZg8+eXMOpJtobxl3xs9pfCIPIQJjTvREw67zTL5OsrwijzL2Le7AbsVva4kh7zXcvQ79FaKPMbiazvX34C7BVefO8bSmDy8DoE8vuH7PKm8ujw8UU28E0V+vTRNKry/uYS855HjO06Fm7wBj0y9fXhnPMbkibxWyKi7Sq4ePGWviLmHq0C8HzUAPCt14bsuDJI8PS9XPG8akrzjutM8xsgVPXYdvLu8Zhy72J1AOyb50ztUbrQ7PwihPPmIHrygNMI8/6eauiNH6ryo2ZA8Wu7yvFSQdDyXdOo73C6ZvMzh2DxlxN68tOKxvHv+DTqLlwA7SVWFvGiw7jvYM6G7owGUvBoA6LuozAw80jAKPE9QojtWWSI9q5PXPAoHD7xoRqO8YFeBvFeYQLyAb3i8SkyMPD3ldjzOEoS8YbWvvBCMJbxnPbO8O0IUPa1DCboKTJC7m4RsPCW2FrzOvg69+6QjvKLY0DxqonM7H1dcPFfgELsddDU78qcJvI7qATzAVPs6V2r2vD72Hr3wM4W8iyH1vMFuHDvoj5w8lwsfvN0q1LqG8Wo91oL6PLtAn7tD7qu644fDvG3fL7wrhlS8WxE/vIIXQ7yQJkQ96n+FO4UthDwgnAY8AdheO82auTsyjY+70tynPOM2Nb3Mq9W8cO6dPI0fj7t4Hui838piPP2EBrwqPoY8vM4pPLg4pTxICgI9pJJlvGDbvLyiWlE8BycJvSADzLpd/em8BSdbPH+ArLui0507fnOnPEIVnLxEQXI7ddDKvNXJFLzfaRU8pgVlvRCCrLwLTVi9L5QQPWrXU7yTWw49SjjwvKWa/bmDZUi7HAuKPI+0WzzLXMa5hKqJPNKVAT0ZkIM9+76LPAb1IT0JRzE76td6O+tN/Dz1Zqm7jLJJuSqKZLxU0CW8Q9m6u9hSvbyWgIC8QlwAvFYygzwKQck7kHl2vE4Js7oJQE+9XjUoPQu70Tt9oLm7hia+ux0OKLt2RRA8ZqTvu0KvGr2pxee75HEXPEIw0jwwlAq8LSscPV2aejyLWiq8goKyPKzBgjwFHz+7JEcHO+z4rrnF9Li8iim2PEgIHDwoBz88hVYqvBscmrtPdAe7YokDvecUgTxIS4S8wlKHuyyh+jx2cw28ahzOO0p3YD02ZFe7Zxd6u6aoi7xk5UK7eenhuQuDZbxtMAi9fYlhvDBczTyTU6S85+u+O8T91joTCre8AKm1vNIn3LuyHcg8Y7eevH/MK735Uvc8sVpTPQ4zCLz/P5g8HMiSPNPPcbybj4c8jWTfvNM1Ejvi7bu7uoWxvMSnsrwSILs7dPzRPHJeWTz4ffk7vIleu1SXDbv6hxi9fJeXvDEYOD1xHcs6jsw2OyCIDT2lTsw7kxEaOkWzlzsQYBk8fF7OvD8hATwqZlk6dG8uPOdZ/LzNcc887wtPvDI3mruwE9887ZIIvJBNezzOfhi8wqSvPHwNyrsE6uC89y+bPORWdLzBSyG8FqqIvPEJczphXr27jTHfOjdXa7wxNgA8R2Hpu802IzzRLxI9eNY3PcuMDT3qcXy8QN6SOAxQALzHJWC8xyTkujfj9bwpGPS6M6+ZvOFAOTy9/WW8Sx72O/Ak1jtoucE7uxwgPX9Ncrsa/H88QnVmPCW5dzxV35Q8PsmZulIca7wT6j48hI2+u9xSHb12NQm9g+7EvD6YyTwEY8089tfcPNuuFLvHGLG8lPl9u56rtLtGKtO7vJVJvCCvhjtuZ4u7V9a8PMTEKb2ileQ6TDPWug+BGTsjJnC86HC5PNNwezwmsx293DEpu5xHvTvZm7w8RgEUvMBwPjsU00U8YSksvAnJtjwGcwm8qDUwu+NMbDtnJAw80ahcvOWngrx7Cqw70Qk9vL7ODrx1Qf+7axZ0PIuDh7tem+Q6fEpVvFgFm7wpEu08xDmUvIq7Oj1jvp68Z/SBPL5tzrz0yJQ7EOJ/PFCeSTx3MBg9OaS8PB+CUzwuHxe8t63bvAxqWrqYVoo8livBvNgjzbsMK5A80foOvf6xozxWUau8jYXMPLbke7zb2KG5QcCwOx+/MTqFxve7B2M6vDenGL10muC8BVQ3vfgOFDxCcgS85dclvBd0Bjy9Iq687wSkPAo5JLwAzqI8VbQUPQrUDj1/4z68u0ryvM8n67tBcAQ9KNnMu1ntOr30D5Q8wf+KOzQCAD2sibG7/NSUPAovyzugMoM8bX/SvLIpiLywCXm8nO0GPS7As7xHweE8xN9ouf7UfDzgnRk8plDZPAV50zsW/ly8fvozvLnax7ztpOy8WKXyu88AVzw0yBc9hntQu4FiXTx4Qai8UkgTPC/gIT1bWfs8DMJCvGjp7ztQNYI7y18vPRNLajxRfFi80UxMPQJD5zsnIfq8Dbt4vIv0VzqsOoe8Bwvhu3hoYDsVs/U7/O7uO7W0cDwlNAU9dj7bvEZkczzz+548XErYvBClCb1nGRU9wbqzvL26QDsS7a87O/1BOouVJbpJggo9ncGbvFTehzyYaY+87UGRPMD5P7wm/Ts8pY+GvPWavDz+OX28Sc4uPLRbbLsfnfC8nDoFvHjcDryznCc8E9XUuw6H2btiQT084ivXO+tCOTsVpio8/RaMvJO/Yzu45u+7axG8PNf3Db3ECJs7sQpJuz6XND2QIR+7t/ILPLGHHLz0ZbA8wk62vICgKbvLyr+8zyUHPU+/ILywtpM8IZwmvKonHr1559q8RtiGO41xR70n5+w7un/qvIOP4bwa4IU839KUPF4BdjyodhS8HAGWPUrsUTxNhg89a9NyPNW4rTybcEK92uOEO6wNfDxMmwQ7T+3KPA2D07xrCuy8W+QSPURHYDzP6+U6ym8JvSpF47tTZp68SFnoO77TkrqesRG8yHesPFkbgTvgJYo80d79PM5nO7zvBA49I4y7vAsoKbv2waE8ktAGPQ/jyzxRtbE8ivS+vGVQlDxjJy090z4zPMoSV7xc03w8G7jquTAejLyYQPm7hWXbO5F+q7yTioM8TZWIvHS3F7wD7NE8pLxSO9BA9DtQ+8Y8rZH2vLmdHL1xV/O8Dw1LvKQPkDyk7227VoqGPCT9ZryWcoM8CLdgPAvZijwXYks9lBZSvTIGIz3Wyr66qEgYuxec57zrcWi8w8C/OpzJ5DtCWOk7AaJrvIIiljqiKqC8z5+kuy9rDL2CigW9yJ4DPZbOCbyqY8u8QR3oPB7HdTx4JMO8yi3lvOkWqjsDFEy7r20wPAQKvrzW71A9rAO6O6spgLyaw7G7J/07PDdfB73t5IA878sNvWbT7Tx7KJ88Xo4KvDsT2zsyoYo8bMsXvYDLybxad0w8SpoSvHjsxryOBpG8igMyvZNn3DxCqwM8O/wSvLCRwby+B148f0QJPOVgFzy6hBw82W6cOfmS47xaYqu6hI+kO2lJFTtvcgQ9gJzdvLZjsDyp7gG7TtoqPJ6Jgzw9SXM8XgiiPPDD0Ly0l9471TUPvDQmlLyCobi86hXiPOExObkHgem8OiI8PMR83bsTjiK9xIy7uqbH+bzG2Ss8uxkNPRewq7szTeM8N161O1B9wzzJNsc7DnMwO+c5CjtPa928OEuKPKRdtTtTaaS8KVhMPNxxmTytuFi8aPM7vPcb9DuwXfI8zNITPUq/17vLzTm7VJjiPJuEaruu7EY6OlcIuRlCHzstx4S7FK2rPG5Mt7u6Uea6a4gpvD2VTz1gk/I7ns5CPGo6wTzqFx48Rr80PXzO6ryj9re6EwTku1Oi5Dzz0eo8tgxkOqY36Donb+y8yy8QvOgYDTu1mak7jdv5uyKgtLz125k5Pq9LOy2LB7qM5gS7PUUhPKhLTbwWT/W8SrimvLmVWT36fHk7KSievIvft7saPRW9dyAvPMbWILwh+4g8Z0wwu5d7AT2yw1C7eE6pu+cOj7wxNEW7myr9ux+HDD3fYGq8pS1yvAHqGzzAbXe6qZwXvZO1U7zKrnW7+wjZPG/58DvqL2g8WOfUu2MIsTyHVpM7o3CmO/6mTjlBzi084XsGvKtXl7qlnzM8J+0RO3vZkbwpnp88RdrQOwcHjrzSQE48LxW8PHj0hbtaLDW83gK4vEQMmjyDZSc8/zQePFlkXjwI+XG8HlVoPPbMrby5vWs87OYvPSkGVTy1dLm85wsEOxqv8DppoPa81rQSvT/dqjsdpsW8OEJcPGA0PLsMwRU9+g9wvNbyVr3Tpqg8Mh+IvM/d0rzbGI88JS8WPM7acruW75O8uhNAPHfcLbxMiHi8ajcmOp8C3DwXHJM8MAsivBFf4jwVFWQ8a5IePeVwlTyFAAC9d0mzvP5DEbzNgce8anS5O3KdLbzJemK75Ny6PPVQIjzUvua7vwxnPO+8VbundcW4N03xvCbkaDxeHZG8KrCnO5Y3ojwjFo48DRfXPE9VFTuMtec7yRmau31oujsGNrU8x5tUvMj6QTyQvAa9nmqyO92KsLxzPE07uwKJuOYPMz0DeV88iNr2vPr53zv7MZk8PSurO8vruryz+i+7SbwgO9cSGrs+jGG80SRKPKcZjjxbJ5W8QxIJPB90qTyjKwy8ccDmvPvnLTxe3JQ7rkSyO7p15jv2m487EtgXvG/xyTzA58m7BFmwvK7F1TvPcss89xRPvNFiB7y26mm8QOGrPAjsRT1F7Is6qUWfO5qSB7zO0we9LRkcPAe56jzNnEC7NfMGO8BE+7y9f0U8XMk3PGkZSrsYJDQ6uD0CO/aFMzzyA5Q8FV0LPRMciTyiKHu6SzkrvW3aoLyYmqe8H4/HPM8tprxnYYQ8vmjTPPNE0Lz+FQy9qfyaOsWv6DznBBg97nhoOokKqTux/nu8Km08PC7hHLwioAY9EgjqOxf+/TsqjDm8suqLvDfQzjxOMFO9Cb0TPERRPrsW5QE825ySuqP+HTypsIw83BmcuxWTjLmzyEI8BkA+u8XcfbyVmoq80nwBPRdoBLrC5E25136SO+nD1ruwvQ87enrHvLV3QTwv/pW8dkwzPB5YsLwJIhQ8tW1ZvPHfAD3re4K8aJFsPAGbkbz0dtK8KJtevC+KRbwoA9s89Ao5O83F7jpxBcS8NvydvHRC0jxvCRu9eiLhvC87tLp3w/A8j1VduyARjDyC3b+8dB0EPF4/BjzPI8e8F6UsulrARjyNaCY87Qc3Oqgg0rukksQ6wMYPvXVnZzvhx5M7RsGEPENd3joSpQW8tS0nPDGHm7xk9+27Rt7zOzjX0byPQq28dIELvST7sDzewi89yS6kuyYVybtPTq88HPTTPNljUrxP7Y+7Jx9yO9qhAr2VDDu8f+LJu15gRjt3kz+8f74OO2LlFDoHKVC89a3Guwug5jpWq6E8lQo7O0uOjjzB+Wm8KziyuwibALrLzeS5m4lgvILTLrzAJTG9oyxOPOJ2ajzD70S9VqSoO+pkqDoYCxu9RMMgvayQ+rxRp6u8ij2nO8wQjDx168y8SlWXvPq/dDyIaDg7/5eGO72z2zzc+VA7bF8VPbdnOzuMrWw83JtMvCF1m7zG7I87ggl5PHWchbqJJYc8JcC9u4uiCD1375M8fwgyPOeGUT1eoVm9UQLPO+itA70ZMTe8Y7u1O4rz77uSmJA8fweJPPqICDzabJ28O5XOPC8zCbxMVbC7TpMyvA8i2Txz5308mr6AvKR8rjt0UZo8HoA6vAlCkbw0t2294mk1PVEM7Tx1RQw9diqDvExAlLoE8ye8pr2rvAAp7LwY0Re7SrG2vEsTyTuetia8XrQSPX4k1rxPXJO8C/GdvOMI2TyAQBY8k+AEPM9pvTy65hy8vFHOPMbBGT16p9O842CVvNuRMD1keL48FvgbvZISADzBl6w6GNrOPIiQ/7v4b2K8Q6NFPNP4FjleFcK8RtT4PPZBxjve+Wa86e31O6dpAT3d3FQ7wucpPA0xhbum2wq72ulduxFuJL36rUQ8T/6kPA9eDDx7gay7kdtFu75TErz9bPE8knGnOxgmQD1XIsA8vAYDvE+617tVVkw7jVzAOyJWJrxdZVy75ajGuxb1grzr/ty8f6ZEPKrORTv2v6C8ZqvvvL2Zkrymfhc9C9lUuyrQCr3dPa28nnj1u6ch0rzguaC8oGj3vFMODT34eUq8hdQlPXqhDL2oXPo8PYTEPJTemTjejVo81dL7u6r5vTxaAeO8CM/HO/HC1LzmLjU8cUkavaE9tDznfq06/+jrus2nI7wQVpA7rT8KvZ9BSruSSnO8qg2iu7XCijzxHIs7lDcPvTkG2Lxg1yC89rAgPa4ojTyD6CS6MGjZO25vAb0F9GG84XczPK82ezx/dKK8GLr7uyL797rcQnQ7UgK4PNiDAz16t7i7IA/yuxZqULz1OMA6ODpxPOgKOrvMNt48SpGZPKpiVzyKlJI7eAIvPcYrrrx+4J88yeahu/HbkDyhXoM803ylPCZiO7v/DZW8rFYbPNlqRD3JPIy7Ed7RPF4S4rsZRAi7TmKPuyXDkzw9jCs8FVJwuvAIqrzPqGa8/JdivJHrhTzZcb28WzeIO/GtxTx7VLA7Pft5PIz6qjzSOi08Jl+LOuZKnLxFSvy7TF/UPMd1yztwrnI8AK9Zu9K8XDztoPs7MoRcPIAjF7zurQ88Q2b4u4KIrjxIUH68FmW1PPkuqLuQuZq8LX0eOsWsqzy1yva6Szzyuvi7qzqxaT+8NLMkPfhlkLzTPac8goYTPNl4ubyYiYm87WtFPLIEhruONRs8ituSuxaiDLxTCi29eCM0PE6PqLyEG1w8w1AZvCgcFr3bQv47Wb7APCW/HLyuUn+85h9MvSaK+LzLgt287C3nPHp98jwBlSe8DAr7PBqHM7xdFpc828UavBLO4rueeqq8cLyWOp/DzzxyS2K8TzMgOwSxnrzBTCi9DtLLPHL+kTu69AK69GqFPDosLTxPARU9BIQMuyxLELwoC7I7Cuf9PNKwlruAKcu8+DQnvD9GizwFtcU8GInoO3GsFT0AfAE9zzC6vAWkRjwF89U70+GNNoyiRDxUmrw8oP+JvL99y7xnXsy7gNs2PHvxorxuEIk6qoxqvEeFGr2GBxY7dyU7vCITpLrljDk8eWyHPKaTeDzZgs+7hIzlvO4q1ju6+TO7OGfsOl4lmrynpX6856eQvHOjPTxiReS8yzCFO7fBcbwjnrK71ByJuxi6SLwei0M79zQcPA4K0bu7sEm90yDtvH5nEDxzhn27IDMyvCxshzzW5wM9Wb3JO9QHiLwdzE68MvJ1PPxogjyDjFi7xiMdPZwImLwfmFU8QnxIPLP4srqCxQG9K2k4vfrsvjwLKti8qt2suV661bkBmYG8qyc/PaXlujwlvui85wuMPDEqDr34xm072NGxPJfn9DwwnYQ8+qS8vJrBYjyAIQq98qIIO3wLu7xUYba6MiqZvJ6XeTzzHbS8lXgSOydBWLtLhNq7WkiGPIEomDxfsvQ7Fhz3vB5JpLwa5S87bIOWu1acmrwSkpW8C/MmuyQgA7zAeum7FmEAu/kkhTxOXqe8cE4ouoLvizzQMB48K77ZO0YfmDxOHWM8l6hMPOMxk7zdNQ68EyHyPO6X3DyvlYw7cI/tuwGD4bzKBzG96wl3PC4FBDw+Xna7Du8LPCdnczuEvaa8IavYuyP/TzzCF628Rk4lPH+VgbxRJFY8cQIMOzzPjjrfe1o84O3mOyw5n7ssvQ29LpSUvO7YObogbuI8le2XuwWnqDyL1Nq7WX6Eu8ZwMry645g75XOTPA2it7vgkGA8SZfQPN6Vp7s61ZO8UrscvEUObDovtbS7ibnPvIW7SjuPGN68mELMOyIhqzvoKWq8/Uz+O3g8Jb03Rpc8RqDSO36zabzq5By9czkdvIpT5LsiuAE7G49ZvKrFfTzGU5S8J9okvQfqBbx2EYg8NG/HvFO+DbxzvMK86W2yu3eT/Dy5Vmg8w9qwvGDqQLw7vbI78stBPI8EFTwQXS28yV1SukZKVL3npIU8LuUJvWs2bbkyFbm7erIlvNC/SjwJgPA8AUm9PK8jprxC4Fk9e9idvI3LO7zz7i887JAAPbVEyrv8sf68RotKPBxoc7wV7jo8NuTlOhHdJDzXMYW8RvgMvSmGDTwD6Ia80cwmvJYE4jqOjAi8/udYvEudYb34Joo8RJkIvIaMI7yMzxO8U0iQvE3LE73ffUE8FNgKPSRLvToFdtq62V4YPDfRujt7heo6hCNtuqYjlTwD0mo8lpoQPCviDjsG9zU8raAUPHnvCr1HmmU8On8QPaOea7z5J6a8iY9GvNZ7iDwBc6u7zIzEvCFeJLyy5si74fZFuxrwoboGnbw7lcSGvEY6Nrz5DoW8aT1lvcpOCL2PVyu82QmZOxtCpDtH4SC8m6irvM84jDwvQcc8EEzouhnT5DyucaU7z4YNO9djQj2a6Ik7FeLFOwfERbvEKzm9yIakOxfmwbzvXLK8xmyNPKR3bruUL+08LYALPDpBqruIgn+8CDOFOwhWCDyVZa28xNmJPH43RzsG+Xw8DtgkujWxJrzeDVK9PzZevNkXpTsSdI07TX/YPP1DC7x9PiA86oPsPA9VfbvNh7E7iyYYPGdrYLvTvbQ6LQUXvYxmVjyOFJ8654eCPNwqELzbdQe8gz2iPOfcszwiSmo60gA8PNrxJr2VO1c8E+3QvAzNq7n6WKK8/+jluwuHlLy4h7E78YfMPK0vazyid/M6lP8HvVulGb3lu6k7lfO/O+c5MDvROZG8hw68u3WnoDxGkD48VEZhvKukITtII0A8Zl9wvMB06LxcmIS7J3r0vLN5JLu/oTk8rm7MOxxIDT3k4qo6q9LivAl5YTvA/h68ZBrMPKXdlrweYYa7j2suvFwidDw42AY78LDVu8dR0LsKUyG8N3AcPB+Mobyi7FC8/9C3vEt8M7sx7yi8ueBSO93TyryRMIW8v5pePIsC7zzAdF087lMAPa9ebrtGBwK8itd6O9bOlzt2Ovq7QvQUvCPdVry1raq8AEnHPDs05btWehO8J0qXPAgzdbwKsCW65hjYO2GCwDzbKu08ibsePBm6ljzlGhY8f0uHvE5Zezwf3CE8/z/2u/DWszmE2988anUWvWxYpDrOGRY9Oy7rPDDWBLw8++i79FI5u0/NZLslg6U8n03XPDK8QLy+9EI8gVJKvMZpxbsZw7Y7dYlTvFJviLwpeYE8hNWTu4n0UTumfeC78i/dvLeT1bp0rxw8AM4WvLnMjDxz4uE8+p+FvPOBG73ybmy8jHFoPGRFGTwhX808U00Evcg6C71xLNG7v/G7vB58uzx/DB08pH2sPCEwDbzX8tg8jGzRvC+KZbs2Rpa8sDHXPLSKKL3WdUa8KWCaPA8QazzTGYC8xRKkPK7PArz4etI8OoIbPVxtEjydcBU9R1cSPCI/QTxGmn08+mWJvAs4lryzkyg8euKFOwx9FLxy45s8jcBUvIEzLjzQ4X67fFYsvMSy07rjKME877yoPKblRjxOgQq8rbTTPJp6sryKU4w7cE0QPW6tMjydB8A7h65PvLHq37oV0905gPw7PPLkvzxqXw+8fP6AvNM1ETtf6ba8gLolPVj3EbplHaA8Jq2zPJ9MerxmUBo8ZT0guhxHcrwOQ0m8FpIXPDD8N7yALsE8F7mkOtmD1jqnE408NSMevD5n47sNrju7GOr4PIU3ort58qI8nNobvK3SCzxtOXW8MciUvBdRrryD+PG7pfegu/N+AL0HX4+82Un3O4I2kLxxQQw8IRaWvBEixzol6EK7MGtzPArOmrx2Oda8X+5MuY19ObwOtcQ71AdFvOpaYTt2LM+7qDkgvYn1Irvx6Pg5Qc2YPEytl7yoolQ7/+5quuUP8judEys8orLPOlW7YrwzsPc6Fa0tPOMWtTuGsrM6CFikvPZPvjtH3Yy7Qwh6PBRnBDz4iz68n0PduzQmlbw9tZC7RYVUPHHumbqxsB273r2HPOaTnDwJ9fo8qSgEPDDAzbw5umM8W88Xug== - 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: - - '8302' + - '8487' content-type: - application/json host: @@ -409,15 +378,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -437,6 +404,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -450,7 +437,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -458,17 +445,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -484,44 +469,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -531,26 +509,24 @@ interactions: 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. + 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". + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' name: execute_code - id: call_dwvve4zt + id: call_b39cif5o 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_dwvve4zt + tool_call_id: call_b39cif5o model: gpt-oss reasoning_effort: low stream: false @@ -601,7 +577,7 @@ interactions: response: headers: content-length: - - '707' + - '668' content-type: - application/json parsed_body: @@ -610,25 +586,25 @@ interactions: index: 0 message: content: '' - reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in + (d.get(''title'') or '''').lower():\n print(''Found'', d[''id''])"}' name: execute_code - id: call_juxb6n6o + id: call_y0fbrvbg index: 0 type: function - created: 1771924532 - id: chatcmpl-716 + created: 1772549384 + id: chatcmpl-612 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 97 - prompt_tokens: 1901 - total_tokens: 1998 + completion_tokens: 86 + prompt_tokens: 1907 + total_tokens: 1993 status: code: 200 message: OK @@ -641,7 +617,7 @@ interactions: connection: - keep-alive content-length: - - '9074' + - '9165' content-type: - application/json host: @@ -652,15 +628,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -680,6 +654,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -693,7 +687,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -701,17 +695,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -727,44 +719,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -774,40 +759,38 @@ interactions: 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. + 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". + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' name: execute_code - id: call_dwvve4zt + id: call_b39cif5o 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_dwvve4zt + tool_call_id: call_b39cif5o - content: null - reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' name: execute_code - id: call_juxb6n6o + id: call_y0fbrvbg 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_juxb6n6o + tool_call_id: call_y0fbrvbg model: gpt-oss reasoning_effort: low stream: false @@ -858,7 +841,7 @@ interactions: response: headers: content-length: - - '762' + - '564' content-type: - application/json parsed_body: @@ -867,25 +850,24 @@ interactions: index: 0 message: content: '' - reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. + reasoning: Maybe titles None. Let's list all docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' name: execute_code - id: call_7m5eb6ho + id: call_ceq2v75l index: 0 type: function - created: 1771924534 - id: chatcmpl-681 + created: 1772549385 + id: chatcmpl-892 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 102 - prompt_tokens: 2076 - total_tokens: 2178 + completion_tokens: 58 + prompt_tokens: 2061 + total_tokens: 2119 status: code: 200 message: OK @@ -898,47 +880,7 @@ interactions: connection: - keep-alive content-length: - - '86' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - quarterly update - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: XjueuJ8K5jyHv9k7oRDPPEcGhrmFaEc9xwJTPT3cpTyFtmg891PQPIPKijzgHwQ7GjV0tzM7TrxJp6U8wytRvaTIBb2WkrU8UMhLPEmkqbvO3na7bDpKPPtJ6TxtmrC8oivBvM39M71OBKW8wCPAvZSnbLviUMw8fMJYvXab8rwduhg9YFeau3wYEDu3K7268Cuju/XC+Lsg8Wy8JgN0OmNmnTyesKm8gPgzO5DbcrvuZmw82cUNPUEfLzwV9E88fsaBvOdE0LtPiRA8o6QrPMQiMr3VNgS9QV4FPOweirzAwms9u0F/u7ylFL3izgG8nzevOqkc/zveuVi8+vSsO8ocrLsRU++86cOqvN0slr14dN07fCGUPDtACLyB51w87Io8vNrqDryZ7Ig8mbG4vOa6NrzZ3iA9IYvbOUyk7zuLNQI90RO1O6BQ57vuiJW8A45bPN1tlzo0Ahe6icUwOyCUQ72BXKG7YHsaPGWDu7s3ueE7kRCZPI5gCzybNy27x4/7u+0tobz1zjW704kPPHUN6bv/bxW6jdXzvFytnrwiZ7+832oFvSmhsLzZN/K7LoWLO/3l4jtZIXa86uv+u0y0lTzRMnS8NfURvKl+4TuxzDk9Wg+PPI5EjTtk8IW8Q2jku07SqjxfmgU8mpzeO65UMDz4fFa7qn94vJ6kfrsc4/u70AusPCsDAjyokIC8YvQnvMS7k7xATQu8xn8ovPsHCzqJFKI7L4dyvBjA4TxFiIO7WkLWu/GiKDxWLFe5t82KvIQNDLyQUvU6gF5OOjRUrjtJeAU8zNO0PA6LILz5DqS5tJZ2PAYxFzsd578802OaOxX5hDxwpdS76hPtPLfzkrv9rJI8yKgjvIg6BDxonHM8akRLPK4U6rywya87tRZ9vN8I77wjqfc5W9xdvPcLdrwmO6K8NzKivNfxVTywYve8OCsxPIYvHbxVQBs8S+6tu19C17svXqM7to72uhkTkDvze3Q8wHNEu6TiqjscMG86lJ/sO6hDVb3uSeo7PRHuO4OZyDsNTZY7SMGuvPf2+bwfzWs8sNT3uuVKJzy4c0o863AMO8bUcTt/KT28z3lUvAZTgTyGXei76YIwvCVehTwBUrq81CLaPL+AFLlm+xa8Z77NvOC6hjyFpVQ8/0GvvGrEmbu1TrE8TryvPWuqazxwGZi6xQdsvMuwbjx5vFq9Sd2JO2nwkjscOQk7pp5xPHomSjxH2ug8ABi9PItgmrx1fvG74eO3OzaJbzoQxXO6WqqFu9YsDryArX+7lrsGvEs1EryerZk6wkp1PCUsRLsl8w68SXeXui3ENLzjlI28qR8aOovrj7zpHp889IqFPC8M1Lunhv+7sdTXPEsSurx9Ml+8Asj4OUGECjvKuCo89GABPZi/0Lz/Uk27jGKxOeIXAr3MOQk8YFh2vAHGgDw9PQc74lYSO0hqUrwQUiy8Q6b4O1jOIjzMuLg7DGtgu9DERDw49L07bnkOPetZsLwvLTA6kGGuPHAadDuVVwA8zBW8PIk+u7zQYVU8s1uZvPJMHLyYyYY8cIJrvPH3nzzbej07yfidu4ZdIr2tGb07dxmJvDv327v9x8G7/QYYPBCHjDy7Uhi79ZuhOoITLDvXWJu8ebqQO1mHHTz45VY8cDkUvfwh0bwxDfu7k5l0u/zzk7t6+o26V+8GvYcKTLy6kXA8zcKpvCw9Cr27ECq8r/yBvQ2Nk7u50em6J6ktPQr5xzv/mNg7IMNyPDYp8jv6eyA8cqcAvAdPDz3lnvy8kGWkPPKtxruZ/p686MYgupg3Az0TPGI8cMjwO1M9jroJZQA864Juu+ij27yRBoK9byuzvJZoBrubcCU8fb+1vMXjPbxiS2g8op/2vBNbnzzlm5S8YR2svAo10zzNroi8BWegOx8u07pvP105t7dbvPGskzuBGHu8djKWvANsibqZrbG6A1qPvEGYBj1+3h69W3DBu2qhVzpmtY68tsocPA0567zoULi7+6CEu0SZ/jubrqs7c/6xuhKOj7sHW+k8wKYwPV3QOryDBU887yfivK31LTv9fzy8nyYvPIX8e7yO25A8PrF7vIjCk7tpDH08kJhOOW1qS7zX0RM9Gn9ZO5KNwjmxwD68kLGCvF1M57xkw868OOPvvK8F8rwXKkK8wnXkvLCXaLvGaYM9UUCDvEnRXLtD0My8fZwwPaVdI7z5GZu7edYRvdIkHjsRpZQ7MN7RvFGZ2bxDlvg8u/YnO19Kobuapok6URJZvL/kwjumszg8B7MYPNB3jjyRHRa96X+FOxKTyDsdBTM85R7wO4W+LzvfqJM73jr2PFqtTDyFaZe80ao+vPWb+7xlgLa8hTWpvDfiozwkf888f2g/vSLChbu4oCG7u9TKvIyQTj26eTS8ZPxSvDgS+7yVsr65RDfwupSOoryEyQo8yM8/PHvrsDuf7me8AvuaPPT+ob2sMVM8n9oGvGBFPLzsGoW8puhpvPu0w7xhGYa8UxFvPA+jZD27S268kG8zvLF9o7vnsyE8LrEcPBVq4TzJiLo763tlvOkOI7tUKvY8XGg9PE9mS7iOEb88hx82PW4gSTzbr807+2pnOq35JDygckk9UrcevOPOxjw+Cao8nlxZvFSyq7ySxDc8RaGSvKT4JzylWVq9GVSFu4+KBTy44Ss7q6vZPAsGTLyxAce7gYO0PKyGED1u8sY79O3vu+Vhq7zEGpE8GKajPPsGgTxPsB+8b8IXvX1+CTzYUxI8BHAVvfQuOjtimq48rDgyvb+pEr3C7888GVfQOwEob7xitke9TjoxPT7J5LzFEkg8sNdAO0N20TwcxCy7hD6evEI3wrwuWss6KTIivaaeo7yE0rM7d60HPStDn7zXsk6896BCuxPRo7rfQSg8KzNcvOFjTz1GUog8/ZUeO0qUIL2J3D29nbw2PUMNNzt0h6+7lO2VvDyXBb0lDoG8yQkJPWqJXLxsh8Y7NA3Puss8NTyTPzc9xTqkvLyPfDxMrek8DwDUO2cZ5LvJc7I8+nhlvH3KFzwl9mK8Q3KdvKqPmTync/e7dLqUPD3sijzMJsi8775FvDeWlbxPhoq8i2PHu1lHHDrXJ008Soy7PLHiBLzuuSw7LiUsvE4crrwBwRw8aiiuPJBVyDvRnKW8fj2rOhm6vDzvivu7obWlPFnoDL3HVIw8lEhWO9xJIb3/f6e7+BdKPfVtprwcC4+8x/0nO3TQRr3H8Yc8N87aOzPBAD3oMuq5F6iaPMMGijwbP7g7JFasu4aJCztj3fG75SVDvIIi0bt+oEK8E7OxvIUSnDxDj+G8bkGTvOJ0AzzVI1g83tIvurLK07wusV28dsCSvDAJHjkYiZS8OtLIO8gUvbum8C89DGidOyA/+jsWzXG7zuwlveGXjzz+qPk76WaFvPaVJDxnu5E7mQJUPA9zTzzT8Wu7v520vIWxk7wzhD48r2S+vCgS1Dzq/DW80ThwPLv2TzvP+xu8l7+hPMDmAb1SVoA8ZdUOvZRRDz1ZVig7RQkuvGenN7zfHww8E0i7PJ18ib00VgK8hBrSvHiOQr2XIWo9xti/uyeWXjdDkQS8Hlokvek+WLzcZAo96JWevFqtjTvT0287hCsrPFRyObz7wMA7KPFluhZNQLyvxA87ceDGPBMAhjz4+xk88mgIO5ChZjyScZg8+eXMOpJtobxl3xs9pfCIPIQJjTvREw67zTL5OsrwijzL2Le7AbsVva4kh7zXcvQ79FaKPMbiazvX34C7BVefO8bSmDy8DoE8vuH7PKm8ujw8UU28E0V+vTRNKry/uYS855HjO06Fm7wBj0y9fXhnPMbkibxWyKi7Sq4ePGWviLmHq0C8HzUAPCt14bsuDJI8PS9XPG8akrzjutM8xsgVPXYdvLu8Zhy72J1AOyb50ztUbrQ7PwihPPmIHrygNMI8/6eauiNH6ryo2ZA8Wu7yvFSQdDyXdOo73C6ZvMzh2DxlxN68tOKxvHv+DTqLlwA7SVWFvGiw7jvYM6G7owGUvBoA6LuozAw80jAKPE9QojtWWSI9q5PXPAoHD7xoRqO8YFeBvFeYQLyAb3i8SkyMPD3ldjzOEoS8YbWvvBCMJbxnPbO8O0IUPa1DCboKTJC7m4RsPCW2FrzOvg69+6QjvKLY0DxqonM7H1dcPFfgELsddDU78qcJvI7qATzAVPs6V2r2vD72Hr3wM4W8iyH1vMFuHDvoj5w8lwsfvN0q1LqG8Wo91oL6PLtAn7tD7qu644fDvG3fL7wrhlS8WxE/vIIXQ7yQJkQ96n+FO4UthDwgnAY8AdheO82auTsyjY+70tynPOM2Nb3Mq9W8cO6dPI0fj7t4Hui838piPP2EBrwqPoY8vM4pPLg4pTxICgI9pJJlvGDbvLyiWlE8BycJvSADzLpd/em8BSdbPH+ArLui0507fnOnPEIVnLxEQXI7ddDKvNXJFLzfaRU8pgVlvRCCrLwLTVi9L5QQPWrXU7yTWw49SjjwvKWa/bmDZUi7HAuKPI+0WzzLXMa5hKqJPNKVAT0ZkIM9+76LPAb1IT0JRzE76td6O+tN/Dz1Zqm7jLJJuSqKZLxU0CW8Q9m6u9hSvbyWgIC8QlwAvFYygzwKQck7kHl2vE4Js7oJQE+9XjUoPQu70Tt9oLm7hia+ux0OKLt2RRA8ZqTvu0KvGr2pxee75HEXPEIw0jwwlAq8LSscPV2aejyLWiq8goKyPKzBgjwFHz+7JEcHO+z4rrnF9Li8iim2PEgIHDwoBz88hVYqvBscmrtPdAe7YokDvecUgTxIS4S8wlKHuyyh+jx2cw28ahzOO0p3YD02ZFe7Zxd6u6aoi7xk5UK7eenhuQuDZbxtMAi9fYlhvDBczTyTU6S85+u+O8T91joTCre8AKm1vNIn3LuyHcg8Y7eevH/MK735Uvc8sVpTPQ4zCLz/P5g8HMiSPNPPcbybj4c8jWTfvNM1Ejvi7bu7uoWxvMSnsrwSILs7dPzRPHJeWTz4ffk7vIleu1SXDbv6hxi9fJeXvDEYOD1xHcs6jsw2OyCIDT2lTsw7kxEaOkWzlzsQYBk8fF7OvD8hATwqZlk6dG8uPOdZ/LzNcc887wtPvDI3mruwE9887ZIIvJBNezzOfhi8wqSvPHwNyrsE6uC89y+bPORWdLzBSyG8FqqIvPEJczphXr27jTHfOjdXa7wxNgA8R2Hpu802IzzRLxI9eNY3PcuMDT3qcXy8QN6SOAxQALzHJWC8xyTkujfj9bwpGPS6M6+ZvOFAOTy9/WW8Sx72O/Ak1jtoucE7uxwgPX9Ncrsa/H88QnVmPCW5dzxV35Q8PsmZulIca7wT6j48hI2+u9xSHb12NQm9g+7EvD6YyTwEY8089tfcPNuuFLvHGLG8lPl9u56rtLtGKtO7vJVJvCCvhjtuZ4u7V9a8PMTEKb2ileQ6TDPWug+BGTsjJnC86HC5PNNwezwmsx293DEpu5xHvTvZm7w8RgEUvMBwPjsU00U8YSksvAnJtjwGcwm8qDUwu+NMbDtnJAw80ahcvOWngrx7Cqw70Qk9vL7ODrx1Qf+7axZ0PIuDh7tem+Q6fEpVvFgFm7wpEu08xDmUvIq7Oj1jvp68Z/SBPL5tzrz0yJQ7EOJ/PFCeSTx3MBg9OaS8PB+CUzwuHxe8t63bvAxqWrqYVoo8livBvNgjzbsMK5A80foOvf6xozxWUau8jYXMPLbke7zb2KG5QcCwOx+/MTqFxve7B2M6vDenGL10muC8BVQ3vfgOFDxCcgS85dclvBd0Bjy9Iq687wSkPAo5JLwAzqI8VbQUPQrUDj1/4z68u0ryvM8n67tBcAQ9KNnMu1ntOr30D5Q8wf+KOzQCAD2sibG7/NSUPAovyzugMoM8bX/SvLIpiLywCXm8nO0GPS7As7xHweE8xN9ouf7UfDzgnRk8plDZPAV50zsW/ly8fvozvLnax7ztpOy8WKXyu88AVzw0yBc9hntQu4FiXTx4Qai8UkgTPC/gIT1bWfs8DMJCvGjp7ztQNYI7y18vPRNLajxRfFi80UxMPQJD5zsnIfq8Dbt4vIv0VzqsOoe8Bwvhu3hoYDsVs/U7/O7uO7W0cDwlNAU9dj7bvEZkczzz+548XErYvBClCb1nGRU9wbqzvL26QDsS7a87O/1BOouVJbpJggo9ncGbvFTehzyYaY+87UGRPMD5P7wm/Ts8pY+GvPWavDz+OX28Sc4uPLRbbLsfnfC8nDoFvHjcDryznCc8E9XUuw6H2btiQT084ivXO+tCOTsVpio8/RaMvJO/Yzu45u+7axG8PNf3Db3ECJs7sQpJuz6XND2QIR+7t/ILPLGHHLz0ZbA8wk62vICgKbvLyr+8zyUHPU+/ILywtpM8IZwmvKonHr1559q8RtiGO41xR70n5+w7un/qvIOP4bwa4IU839KUPF4BdjyodhS8HAGWPUrsUTxNhg89a9NyPNW4rTybcEK92uOEO6wNfDxMmwQ7T+3KPA2D07xrCuy8W+QSPURHYDzP6+U6ym8JvSpF47tTZp68SFnoO77TkrqesRG8yHesPFkbgTvgJYo80d79PM5nO7zvBA49I4y7vAsoKbv2waE8ktAGPQ/jyzxRtbE8ivS+vGVQlDxjJy090z4zPMoSV7xc03w8G7jquTAejLyYQPm7hWXbO5F+q7yTioM8TZWIvHS3F7wD7NE8pLxSO9BA9DtQ+8Y8rZH2vLmdHL1xV/O8Dw1LvKQPkDyk7227VoqGPCT9ZryWcoM8CLdgPAvZijwXYks9lBZSvTIGIz3Wyr66qEgYuxec57zrcWi8w8C/OpzJ5DtCWOk7AaJrvIIiljqiKqC8z5+kuy9rDL2CigW9yJ4DPZbOCbyqY8u8QR3oPB7HdTx4JMO8yi3lvOkWqjsDFEy7r20wPAQKvrzW71A9rAO6O6spgLyaw7G7J/07PDdfB73t5IA878sNvWbT7Tx7KJ88Xo4KvDsT2zsyoYo8bMsXvYDLybxad0w8SpoSvHjsxryOBpG8igMyvZNn3DxCqwM8O/wSvLCRwby+B148f0QJPOVgFzy6hBw82W6cOfmS47xaYqu6hI+kO2lJFTtvcgQ9gJzdvLZjsDyp7gG7TtoqPJ6Jgzw9SXM8XgiiPPDD0Ly0l9471TUPvDQmlLyCobi86hXiPOExObkHgem8OiI8PMR83bsTjiK9xIy7uqbH+bzG2Ss8uxkNPRewq7szTeM8N161O1B9wzzJNsc7DnMwO+c5CjtPa928OEuKPKRdtTtTaaS8KVhMPNxxmTytuFi8aPM7vPcb9DuwXfI8zNITPUq/17vLzTm7VJjiPJuEaruu7EY6OlcIuRlCHzstx4S7FK2rPG5Mt7u6Uea6a4gpvD2VTz1gk/I7ns5CPGo6wTzqFx48Rr80PXzO6ryj9re6EwTku1Oi5Dzz0eo8tgxkOqY36Donb+y8yy8QvOgYDTu1mak7jdv5uyKgtLz125k5Pq9LOy2LB7qM5gS7PUUhPKhLTbwWT/W8SrimvLmVWT36fHk7KSievIvft7saPRW9dyAvPMbWILwh+4g8Z0wwu5d7AT2yw1C7eE6pu+cOj7wxNEW7myr9ux+HDD3fYGq8pS1yvAHqGzzAbXe6qZwXvZO1U7zKrnW7+wjZPG/58DvqL2g8WOfUu2MIsTyHVpM7o3CmO/6mTjlBzi084XsGvKtXl7qlnzM8J+0RO3vZkbwpnp88RdrQOwcHjrzSQE48LxW8PHj0hbtaLDW83gK4vEQMmjyDZSc8/zQePFlkXjwI+XG8HlVoPPbMrby5vWs87OYvPSkGVTy1dLm85wsEOxqv8DppoPa81rQSvT/dqjsdpsW8OEJcPGA0PLsMwRU9+g9wvNbyVr3Tpqg8Mh+IvM/d0rzbGI88JS8WPM7acruW75O8uhNAPHfcLbxMiHi8ajcmOp8C3DwXHJM8MAsivBFf4jwVFWQ8a5IePeVwlTyFAAC9d0mzvP5DEbzNgce8anS5O3KdLbzJemK75Ny6PPVQIjzUvua7vwxnPO+8VbundcW4N03xvCbkaDxeHZG8KrCnO5Y3ojwjFo48DRfXPE9VFTuMtec7yRmau31oujsGNrU8x5tUvMj6QTyQvAa9nmqyO92KsLxzPE07uwKJuOYPMz0DeV88iNr2vPr53zv7MZk8PSurO8vruryz+i+7SbwgO9cSGrs+jGG80SRKPKcZjjxbJ5W8QxIJPB90qTyjKwy8ccDmvPvnLTxe3JQ7rkSyO7p15jv2m487EtgXvG/xyTzA58m7BFmwvK7F1TvPcss89xRPvNFiB7y26mm8QOGrPAjsRT1F7Is6qUWfO5qSB7zO0we9LRkcPAe56jzNnEC7NfMGO8BE+7y9f0U8XMk3PGkZSrsYJDQ6uD0CO/aFMzzyA5Q8FV0LPRMciTyiKHu6SzkrvW3aoLyYmqe8H4/HPM8tprxnYYQ8vmjTPPNE0Lz+FQy9qfyaOsWv6DznBBg97nhoOokKqTux/nu8Km08PC7hHLwioAY9EgjqOxf+/TsqjDm8suqLvDfQzjxOMFO9Cb0TPERRPrsW5QE825ySuqP+HTypsIw83BmcuxWTjLmzyEI8BkA+u8XcfbyVmoq80nwBPRdoBLrC5E25136SO+nD1ruwvQ87enrHvLV3QTwv/pW8dkwzPB5YsLwJIhQ8tW1ZvPHfAD3re4K8aJFsPAGbkbz0dtK8KJtevC+KRbwoA9s89Ao5O83F7jpxBcS8NvydvHRC0jxvCRu9eiLhvC87tLp3w/A8j1VduyARjDyC3b+8dB0EPF4/BjzPI8e8F6UsulrARjyNaCY87Qc3Oqgg0rukksQ6wMYPvXVnZzvhx5M7RsGEPENd3joSpQW8tS0nPDGHm7xk9+27Rt7zOzjX0byPQq28dIELvST7sDzewi89yS6kuyYVybtPTq88HPTTPNljUrxP7Y+7Jx9yO9qhAr2VDDu8f+LJu15gRjt3kz+8f74OO2LlFDoHKVC89a3Guwug5jpWq6E8lQo7O0uOjjzB+Wm8KziyuwibALrLzeS5m4lgvILTLrzAJTG9oyxOPOJ2ajzD70S9VqSoO+pkqDoYCxu9RMMgvayQ+rxRp6u8ij2nO8wQjDx168y8SlWXvPq/dDyIaDg7/5eGO72z2zzc+VA7bF8VPbdnOzuMrWw83JtMvCF1m7zG7I87ggl5PHWchbqJJYc8JcC9u4uiCD1375M8fwgyPOeGUT1eoVm9UQLPO+itA70ZMTe8Y7u1O4rz77uSmJA8fweJPPqICDzabJ28O5XOPC8zCbxMVbC7TpMyvA8i2Txz5308mr6AvKR8rjt0UZo8HoA6vAlCkbw0t2294mk1PVEM7Tx1RQw9diqDvExAlLoE8ye8pr2rvAAp7LwY0Re7SrG2vEsTyTuetia8XrQSPX4k1rxPXJO8C/GdvOMI2TyAQBY8k+AEPM9pvTy65hy8vFHOPMbBGT16p9O842CVvNuRMD1keL48FvgbvZISADzBl6w6GNrOPIiQ/7v4b2K8Q6NFPNP4FjleFcK8RtT4PPZBxjve+Wa86e31O6dpAT3d3FQ7wucpPA0xhbum2wq72ulduxFuJL36rUQ8T/6kPA9eDDx7gay7kdtFu75TErz9bPE8knGnOxgmQD1XIsA8vAYDvE+617tVVkw7jVzAOyJWJrxdZVy75ajGuxb1grzr/ty8f6ZEPKrORTv2v6C8ZqvvvL2Zkrymfhc9C9lUuyrQCr3dPa28nnj1u6ch0rzguaC8oGj3vFMODT34eUq8hdQlPXqhDL2oXPo8PYTEPJTemTjejVo81dL7u6r5vTxaAeO8CM/HO/HC1LzmLjU8cUkavaE9tDznfq06/+jrus2nI7wQVpA7rT8KvZ9BSruSSnO8qg2iu7XCijzxHIs7lDcPvTkG2Lxg1yC89rAgPa4ojTyD6CS6MGjZO25vAb0F9GG84XczPK82ezx/dKK8GLr7uyL797rcQnQ7UgK4PNiDAz16t7i7IA/yuxZqULz1OMA6ODpxPOgKOrvMNt48SpGZPKpiVzyKlJI7eAIvPcYrrrx+4J88yeahu/HbkDyhXoM803ylPCZiO7v/DZW8rFYbPNlqRD3JPIy7Ed7RPF4S4rsZRAi7TmKPuyXDkzw9jCs8FVJwuvAIqrzPqGa8/JdivJHrhTzZcb28WzeIO/GtxTx7VLA7Pft5PIz6qjzSOi08Jl+LOuZKnLxFSvy7TF/UPMd1yztwrnI8AK9Zu9K8XDztoPs7MoRcPIAjF7zurQ88Q2b4u4KIrjxIUH68FmW1PPkuqLuQuZq8LX0eOsWsqzy1yva6Szzyuvi7qzqxaT+8NLMkPfhlkLzTPac8goYTPNl4ubyYiYm87WtFPLIEhruONRs8ituSuxaiDLxTCi29eCM0PE6PqLyEG1w8w1AZvCgcFr3bQv47Wb7APCW/HLyuUn+85h9MvSaK+LzLgt287C3nPHp98jwBlSe8DAr7PBqHM7xdFpc828UavBLO4rueeqq8cLyWOp/DzzxyS2K8TzMgOwSxnrzBTCi9DtLLPHL+kTu69AK69GqFPDosLTxPARU9BIQMuyxLELwoC7I7Cuf9PNKwlruAKcu8+DQnvD9GizwFtcU8GInoO3GsFT0AfAE9zzC6vAWkRjwF89U70+GNNoyiRDxUmrw8oP+JvL99y7xnXsy7gNs2PHvxorxuEIk6qoxqvEeFGr2GBxY7dyU7vCITpLrljDk8eWyHPKaTeDzZgs+7hIzlvO4q1ju6+TO7OGfsOl4lmrynpX6856eQvHOjPTxiReS8yzCFO7fBcbwjnrK71ByJuxi6SLwei0M79zQcPA4K0bu7sEm90yDtvH5nEDxzhn27IDMyvCxshzzW5wM9Wb3JO9QHiLwdzE68MvJ1PPxogjyDjFi7xiMdPZwImLwfmFU8QnxIPLP4srqCxQG9K2k4vfrsvjwLKti8qt2suV661bkBmYG8qyc/PaXlujwlvui85wuMPDEqDr34xm072NGxPJfn9DwwnYQ8+qS8vJrBYjyAIQq98qIIO3wLu7xUYba6MiqZvJ6XeTzzHbS8lXgSOydBWLtLhNq7WkiGPIEomDxfsvQ7Fhz3vB5JpLwa5S87bIOWu1acmrwSkpW8C/MmuyQgA7zAeum7FmEAu/kkhTxOXqe8cE4ouoLvizzQMB48K77ZO0YfmDxOHWM8l6hMPOMxk7zdNQ68EyHyPO6X3DyvlYw7cI/tuwGD4bzKBzG96wl3PC4FBDw+Xna7Du8LPCdnczuEvaa8IavYuyP/TzzCF628Rk4lPH+VgbxRJFY8cQIMOzzPjjrfe1o84O3mOyw5n7ssvQ29LpSUvO7YObogbuI8le2XuwWnqDyL1Nq7WX6Eu8ZwMry645g75XOTPA2it7vgkGA8SZfQPN6Vp7s61ZO8UrscvEUObDovtbS7ibnPvIW7SjuPGN68mELMOyIhqzvoKWq8/Uz+O3g8Jb03Rpc8RqDSO36zabzq5By9czkdvIpT5LsiuAE7G49ZvKrFfTzGU5S8J9okvQfqBbx2EYg8NG/HvFO+DbxzvMK86W2yu3eT/Dy5Vmg8w9qwvGDqQLw7vbI78stBPI8EFTwQXS28yV1SukZKVL3npIU8LuUJvWs2bbkyFbm7erIlvNC/SjwJgPA8AUm9PK8jprxC4Fk9e9idvI3LO7zz7i887JAAPbVEyrv8sf68RotKPBxoc7wV7jo8NuTlOhHdJDzXMYW8RvgMvSmGDTwD6Ia80cwmvJYE4jqOjAi8/udYvEudYb34Joo8RJkIvIaMI7yMzxO8U0iQvE3LE73ffUE8FNgKPSRLvToFdtq62V4YPDfRujt7heo6hCNtuqYjlTwD0mo8lpoQPCviDjsG9zU8raAUPHnvCr1HmmU8On8QPaOea7z5J6a8iY9GvNZ7iDwBc6u7zIzEvCFeJLyy5si74fZFuxrwoboGnbw7lcSGvEY6Nrz5DoW8aT1lvcpOCL2PVyu82QmZOxtCpDtH4SC8m6irvM84jDwvQcc8EEzouhnT5DyucaU7z4YNO9djQj2a6Ik7FeLFOwfERbvEKzm9yIakOxfmwbzvXLK8xmyNPKR3bruUL+08LYALPDpBqruIgn+8CDOFOwhWCDyVZa28xNmJPH43RzsG+Xw8DtgkujWxJrzeDVK9PzZevNkXpTsSdI07TX/YPP1DC7x9PiA86oPsPA9VfbvNh7E7iyYYPGdrYLvTvbQ6LQUXvYxmVjyOFJ8654eCPNwqELzbdQe8gz2iPOfcszwiSmo60gA8PNrxJr2VO1c8E+3QvAzNq7n6WKK8/+jluwuHlLy4h7E78YfMPK0vazyid/M6lP8HvVulGb3lu6k7lfO/O+c5MDvROZG8hw68u3WnoDxGkD48VEZhvKukITtII0A8Zl9wvMB06LxcmIS7J3r0vLN5JLu/oTk8rm7MOxxIDT3k4qo6q9LivAl5YTvA/h68ZBrMPKXdlrweYYa7j2suvFwidDw42AY78LDVu8dR0LsKUyG8N3AcPB+Mobyi7FC8/9C3vEt8M7sx7yi8ueBSO93TyryRMIW8v5pePIsC7zzAdF087lMAPa9ebrtGBwK8itd6O9bOlzt2Ovq7QvQUvCPdVry1raq8AEnHPDs05btWehO8J0qXPAgzdbwKsCW65hjYO2GCwDzbKu08ibsePBm6ljzlGhY8f0uHvE5Zezwf3CE8/z/2u/DWszmE2988anUWvWxYpDrOGRY9Oy7rPDDWBLw8++i79FI5u0/NZLslg6U8n03XPDK8QLy+9EI8gVJKvMZpxbsZw7Y7dYlTvFJviLwpeYE8hNWTu4n0UTumfeC78i/dvLeT1bp0rxw8AM4WvLnMjDxz4uE8+p+FvPOBG73ybmy8jHFoPGRFGTwhX808U00Evcg6C71xLNG7v/G7vB58uzx/DB08pH2sPCEwDbzX8tg8jGzRvC+KZbs2Rpa8sDHXPLSKKL3WdUa8KWCaPA8QazzTGYC8xRKkPK7PArz4etI8OoIbPVxtEjydcBU9R1cSPCI/QTxGmn08+mWJvAs4lryzkyg8euKFOwx9FLxy45s8jcBUvIEzLjzQ4X67fFYsvMSy07rjKME877yoPKblRjxOgQq8rbTTPJp6sryKU4w7cE0QPW6tMjydB8A7h65PvLHq37oV0905gPw7PPLkvzxqXw+8fP6AvNM1ETtf6ba8gLolPVj3EbplHaA8Jq2zPJ9MerxmUBo8ZT0guhxHcrwOQ0m8FpIXPDD8N7yALsE8F7mkOtmD1jqnE408NSMevD5n47sNrju7GOr4PIU3ort58qI8nNobvK3SCzxtOXW8MciUvBdRrryD+PG7pfegu/N+AL0HX4+82Un3O4I2kLxxQQw8IRaWvBEixzol6EK7MGtzPArOmrx2Oda8X+5MuY19ObwOtcQ71AdFvOpaYTt2LM+7qDkgvYn1Irvx6Pg5Qc2YPEytl7yoolQ7/+5quuUP8judEys8orLPOlW7YrwzsPc6Fa0tPOMWtTuGsrM6CFikvPZPvjtH3Yy7Qwh6PBRnBDz4iz68n0PduzQmlbw9tZC7RYVUPHHumbqxsB273r2HPOaTnDwJ9fo8qSgEPDDAzbw5umM8W88Xug== - 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' + - '9821' content-type: - application/json host: @@ -949,15 +891,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -977,6 +917,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -990,7 +950,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -998,17 +958,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -1024,44 +982,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -1071,55 +1022,1309 @@ interactions: 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. + 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". + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' name: execute_code - id: call_dwvve4zt + id: call_b39cif5o 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_dwvve4zt + tool_call_id: call_b39cif5o - content: null - reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' name: execute_code - id: call_juxb6n6o + id: call_y0fbrvbg 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_juxb6n6o + tool_call_id: call_y0fbrvbg - content: null - reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. + reasoning: Maybe titles None. Let's list all docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' name: execute_code - id: call_7m5eb6ho + id: call_ceq2v75l 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}' + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' role: tool - tool_call_id: call_7m5eb6ho + tool_call_id: call_ceq2v75l + 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: + - '799' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + index: 0 + type: function + created: 1772549389 + id: chatcmpl-838 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 163 + prompt_tokens: 2247 + total_tokens: 2410 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '10814' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + 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: + - '764' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + index: 0 + type: function + created: 1772549393 + id: chatcmpl-688 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 150 + prompt_tokens: 2565 + total_tokens: 2715 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '11847' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + 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: + - '567' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + index: 0 + type: function + created: 1772549394 + id: chatcmpl-336 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 58 + prompt_tokens: 2934 + total_tokens: 2992 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '12400' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + 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: + - '802' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + index: 0 + type: function + created: 1772549398 + id: chatcmpl-466 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 164 + prompt_tokens: 3054 + total_tokens: 3218 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '14012' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 model: gpt-oss reasoning_effort: low stream: false @@ -1179,26 +2384,26 @@ interactions: index: 0 message: content: '' - reasoning: Now classify sentiment. + reasoning: Now sentiment. Use llm to classify each content. 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"}' + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' name: execute_code - id: call_g7dz17hi + id: call_4c3q413c index: 0 type: function - created: 1771924537 - id: chatcmpl-853 + created: 1772549402 + id: chatcmpl-524 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 102 - prompt_tokens: 2278 - total_tokens: 2380 + completion_tokens: 110 + prompt_tokens: 3544 + total_tokens: 3654 status: code: 200 message: OK @@ -1211,7 +2416,7 @@ interactions: connection: - keep-alive content-length: - - '10969' + - '14978' content-type: - application/json host: @@ -1222,15 +2427,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -1250,6 +2453,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -1263,7 +2486,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -1271,17 +2494,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -1297,44 +2518,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -1344,72 +2558,130 @@ interactions: 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. + 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". + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' name: execute_code - id: call_dwvve4zt + id: call_b39cif5o 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_dwvve4zt + tool_call_id: call_b39cif5o - content: null - reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' name: execute_code - id: call_juxb6n6o + id: call_y0fbrvbg 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_juxb6n6o + tool_call_id: call_y0fbrvbg - content: null - reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. + reasoning: Maybe titles None. Let's list all docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' name: execute_code - id: call_7m5eb6ho + id: call_ceq2v75l 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}' + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' role: tool - tool_call_id: call_7m5eb6ho + tool_call_id: call_ceq2v75l - content: null - reasoning: Now classify sentiment. + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. 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"}' + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' name: execute_code - id: call_g7dz17hi + id: call_liuy6nph 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}' + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_g7dz17hi + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 + - content: null + reasoning: Now sentiment. Use llm to classify each content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_4c3q413c + type: function + - content: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall sentiment + of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"","stderr":"NameError: name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_4c3q413c model: gpt-oss reasoning_effort: low stream: false @@ -1460,7 +2732,7 @@ interactions: response: headers: content-length: - - '1045' + - '947' content-type: - application/json parsed_body: @@ -1469,28 +2741,27 @@ interactions: index: 0 message: content: '' - reasoning: We need to re-define chunks. Let's re-run entire block. + reasoning: Recreate qs. 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"}' + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify + the overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' name: execute_code - id: call_knskt3jd + id: call_qwo653xg index: 0 type: function - created: 1771924541 - id: chatcmpl-263 + created: 1772549407 + id: chatcmpl-490 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 171 - prompt_tokens: 2489 - total_tokens: 2660 + completion_tokens: 199 + prompt_tokens: 3767 + total_tokens: 3966 status: code: 200 message: OK @@ -1503,47 +2774,7 @@ interactions: connection: - keep-alive content-length: - - '86' - content-type: - - application/json - host: - - localhost:11434 - method: POST - parsed_body: - encoding_format: base64 - input: - - quarterly update - model: qwen3-embedding:4b - uri: http://localhost:11434/v1/embeddings - response: - headers: - content-type: - - application/json - transfer-encoding: - - chunked - parsed_body: - data: - - embedding: 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 - index: 0 - object: embedding - model: qwen3-embedding:4b - object: list - usage: - prompt_tokens: 4 - total_tokens: 4 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '367' + - '370' content-type: - application/json host: @@ -1551,9 +2782,9 @@ interactions: method: POST parsed_body: messages: - - 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.' + - content: "Classify the overall sentiment of the following business update as positive, negative, or mixed.\nContent: + The new product launch exceeded expectations. Sales grew 40% and customer feedback has been overwhelmingly positive. + Team morale is at an all-time high.\nAnswer: " role: user model: gpt-oss reasoning_effort: low @@ -1562,109 +2793,7 @@ interactions: response: headers: content-length: - - '393' - content-type: - - application/json - parsed_body: - choices: - - finish_reason: stop - index: 0 - message: - content: '**Overall sentiment:** Mixed.' - reasoning: 'We need to classify overall sentiment: mixed. Probably "mixed".' - role: assistant - created: 1771924541 - id: chatcmpl-966 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - 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' + - '315' content-type: - application/json parsed_body: @@ -1673,17 +2802,17 @@ interactions: index: 0 message: content: Positive - reasoning: All positive. + reasoning: Positive. role: assistant - created: 1771924543 - id: chatcmpl-177 + created: 1772549407 + id: chatcmpl-85 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 14 - prompt_tokens: 114 - total_tokens: 128 + completion_tokens: 13 + prompt_tokens: 119 + total_tokens: 132 status: code: 200 message: OK @@ -1696,7 +2825,110 @@ interactions: connection: - keep-alive content-length: - - '12558' + - '379' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: "Classify the overall sentiment of the following business update as positive, negative, or mixed.\nContent: + 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.\nAnswer: " + role: user + model: gpt-oss + reasoning_effort: low + stream: false + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '405' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: stop + index: 0 + message: + content: Negative + reasoning: 'We need to classify overall sentiment: challenges, missed target, employees left. That''s negative.' + role: assistant + created: 1772549408 + id: chatcmpl-764 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 29 + prompt_tokens: 120 + total_tokens: 149 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '386' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: "Classify the overall sentiment of the following business update as positive, negative, or mixed.\nContent: + Mixed results this quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat + compared to last year but customer retention increased.\nAnswer: " + role: user + model: gpt-oss + reasoning_effort: low + stream: false + uri: http://localhost:11434/v1/chat/completions + response: + headers: + content-length: + - '516' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: stop + index: 0 + message: + content: Mixed + reasoning: 'We need to give overall sentiment classification: positive, negative, or mixed. The content has mixed + results, some positive, some negative. Overall seems balanced but might lean slightly neutral. Probably mixed.' + role: assistant + created: 1772549409 + id: chatcmpl-491 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 50 + prompt_tokens: 117 + total_tokens: 167 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '16422' content-type: - application/json host: @@ -1707,15 +2939,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -1735,6 +2965,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -1748,7 +2998,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -1756,17 +3006,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -1782,44 +3030,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -1829,93 +3070,149 @@ interactions: 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. + 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". + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' name: execute_code - id: call_dwvve4zt + id: call_b39cif5o 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' role: tool - tool_call_id: call_dwvve4zt + tool_call_id: call_b39cif5o - content: null - reasoning: We have Q3, Q2, Q1. Need sentiment classify each. Use llm on content. get_chunk each. Let's get chunks. + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. 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"}' + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' name: execute_code - id: call_juxb6n6o + id: call_y0fbrvbg 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}' + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_juxb6n6o + tool_call_id: call_y0fbrvbg - content: null - reasoning: Need to keep results across calls? Variables persist? Might need to re-search. Let's re-execute. + reasoning: Maybe titles None. Let's list all docs. 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"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' name: execute_code - id: call_7m5eb6ho + id: call_ceq2v75l 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}' + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' role: tool - tool_call_id: call_7m5eb6ho + tool_call_id: call_ceq2v75l - content: null - reasoning: Now classify sentiment. + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. 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"}' + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' name: execute_code - id: call_g7dz17hi + id: call_liuy6nph 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}' + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_g7dz17hi + tool_call_id: call_liuy6nph - content: null - reasoning: We need to re-define chunks. Let's re-run entire block. + reasoning: no import json needed. Let's just get content. 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"}' + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' name: execute_code - id: call_knskt3jd + id: call_80syxghd 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}' + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' role: tool - tool_call_id: call_knskt3jd + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 + - content: null + reasoning: Now sentiment. Use llm to classify each content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_4c3q413c + type: function + - content: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall sentiment + of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"","stderr":"NameError: name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_4c3q413c + - content: null + reasoning: Recreate qs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify + the overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_qwo653xg + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the + overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + => Positive\n63b7a740-5678-4347-8d7b-37f869c0f300 => Negative\n76498bad-c7f7-4c90-a4aa-4caf826c05df => Mixed\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qwo653xg model: gpt-oss reasoning_effort: low stream: false @@ -1966,7 +3263,1227 @@ interactions: response: headers: content-length: - - '1912' + - '557' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Count. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)"}' + name: execute_code + id: call_s8351th6 + index: 0 + type: function + created: 1772549416 + id: chatcmpl-550 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 59 + prompt_tokens: 4240 + total_tokens: 4299 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '17262' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 + - content: null + reasoning: Now sentiment. Use llm to classify each content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_4c3q413c + type: function + - content: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall sentiment + of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"","stderr":"NameError: name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_4c3q413c + - content: null + reasoning: Recreate qs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify + the overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_qwo653xg + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the + overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + => Positive\n63b7a740-5678-4347-8d7b-37f869c0f300 => Negative\n76498bad-c7f7-4c90-a4aa-4caf826c05df => Mixed\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qwo653xg + - content: null + reasoning: Count. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)"}' + name: execute_code + id: call_s8351th6 + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)","stdout":"","stderr":"Expected + name, got Subscript(ExprSubscript { node_index: NodeIndex(None), range: 81..90, value: Name(ExprName { node_index: + NodeIndex(None), range: 81..87, id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), + range: 88..89, id: Name(\"v\"), ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_s8351th6 + 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: + - '711' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Syntax error due to using f-strings? Actually using plain code fine. The error arises from code syntax? + Let's just print using loop. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if + v in counts:\n counts[v] += 1\nprint(counts)"}' + name: execute_code + id: call_jdvk05nf + index: 0 + type: function + created: 1772549419 + id: chatcmpl-386 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 94 + prompt_tokens: 4450 + total_tokens: 4544 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '18290' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 + - content: null + reasoning: Now sentiment. Use llm to classify each content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_4c3q413c + type: function + - content: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall sentiment + of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"","stderr":"NameError: name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_4c3q413c + - content: null + reasoning: Recreate qs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify + the overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_qwo653xg + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the + overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + => Positive\n63b7a740-5678-4347-8d7b-37f869c0f300 => Negative\n76498bad-c7f7-4c90-a4aa-4caf826c05df => Mixed\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qwo653xg + - content: null + reasoning: Count. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)"}' + name: execute_code + id: call_s8351th6 + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)","stdout":"","stderr":"Expected + name, got Subscript(ExprSubscript { node_index: NodeIndex(None), range: 81..90, value: Name(ExprName { node_index: + NodeIndex(None), range: 81..87, id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), + range: 88..89, id: Name(\"v\"), ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_s8351th6 + - content: null + reasoning: Syntax error due to using f-strings? Actually using plain code fine. The error arises from code syntax? + Let's just print using loop. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if + v in counts:\n counts[v] += 1\nprint(counts)"}' + name: execute_code + id: call_jdvk05nf + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if v in + counts:\n counts[v] += 1\nprint(counts)","stdout":"","stderr":"Expected name, got Subscript(ExprSubscript + { node_index: NodeIndex(None), range: 105..114, value: Name(ExprName { node_index: NodeIndex(None), range: 105..111, + id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), range: 112..113, id: Name(\"v\"), + ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_jdvk05nf + 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: + - '597' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: It seems syntax not allowed? Maybe the sandbox disallows dictionary indexing? Strange. Let's instead + collect list. + role: assistant + tool_calls: + - function: + arguments: '{"code":"vals = list(sentiments.values())\nprint(vals)"}' + name: execute_code + id: call_6emhxixc + index: 0 + type: function + created: 1772549421 + id: chatcmpl-67 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 56 + prompt_tokens: 4704 + total_tokens: 4760 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '18836' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 + - content: null + reasoning: Now sentiment. Use llm to classify each content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_4c3q413c + type: function + - content: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall sentiment + of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"","stderr":"NameError: name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_4c3q413c + - content: null + reasoning: Recreate qs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify + the overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_qwo653xg + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the + overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + => Positive\n63b7a740-5678-4347-8d7b-37f869c0f300 => Negative\n76498bad-c7f7-4c90-a4aa-4caf826c05df => Mixed\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qwo653xg + - content: null + reasoning: Count. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)"}' + name: execute_code + id: call_s8351th6 + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)","stdout":"","stderr":"Expected + name, got Subscript(ExprSubscript { node_index: NodeIndex(None), range: 81..90, value: Name(ExprName { node_index: + NodeIndex(None), range: 81..87, id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), + range: 88..89, id: Name(\"v\"), ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_s8351th6 + - content: null + reasoning: Syntax error due to using f-strings? Actually using plain code fine. The error arises from code syntax? + Let's just print using loop. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if + v in counts:\n counts[v] += 1\nprint(counts)"}' + name: execute_code + id: call_jdvk05nf + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if v in + counts:\n counts[v] += 1\nprint(counts)","stdout":"","stderr":"Expected name, got Subscript(ExprSubscript + { node_index: NodeIndex(None), range: 105..114, value: Name(ExprName { node_index: NodeIndex(None), range: 105..111, + id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), range: 112..113, id: Name(\"v\"), + ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_jdvk05nf + - content: null + reasoning: It seems syntax not allowed? Maybe the sandbox disallows dictionary indexing? Strange. Let's instead collect + list. + role: assistant + tool_calls: + - function: + arguments: '{"code":"vals = list(sentiments.values())\nprint(vals)"}' + name: execute_code + id: call_6emhxixc + type: function + - content: '{"code":"vals = list(sentiments.values())\nprint(vals)","stdout":"","stderr":"NameError: name ''sentiments'' + is not defined","success":false}' + role: tool + tool_call_id: call_6emhxixc + 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: + - '1767' content-type: - application/json parsed_body: @@ -1974,30 +4491,462 @@ interactions: - finish_reason: stop index: 0 message: - 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.' + content: |- + {"answer":"There were 3 quarterly updates. The sentiment analysis shows:\n- **1 positive** quarter (the Q1 update).\n- **1 negative** quarter (the Q2 update).\n- **1 mixed** quarter (the Q3 update).", + "program":"# Consolidated script to analyze quarterly update sentiments\n\n# 1. Define document IDs for the quarterly updates\nq_ids = [\n 'e3940914-d6f7-4455-889a-79c595be1760', # Q1\n '63b7a740-5678-4347-8d7b-37f869c0f300', # Q2\n '76498bad-c7f7-4c90-a4aa-4caf826c05df' # Q3\n]\n\n# 2. Retrieve contents of each document\nqs = []\nfor doc_id in q_ids:\n content = await get_document(doc_id)\n qs.append((doc_id, content))\n\n# 3. Classify sentiment of each update using the LLM\nsentiments = {}\nfor doc_id, content in qs:\n prompt = (\n \"Classify the overall sentiment of the following business update as \"\n \"positive, negative, or mixed.\\nContent: %s\\nAnswer:\" % content\n )\n sentiment = await llm(prompt)\n sentiments[doc_id] = sentiment.strip()\n\n# 4. Count the results\ncount = {\"Positive\":0, \"Negative\":0, \"Mixed\":0}\nfor s in sentiments.values():\n if s in count:\n count[s] += 1\n\n# 5. Return the counts (this example prints them; in practice you could return a dict)\nprint(\"Sentiment counts:\", count)\n"} + reasoning: Seems the previous code didn't persist variables across calls. So need one single script that does all. role: assistant - created: 1771924554 - id: chatcmpl-522 + created: 1772549432 + id: chatcmpl-62 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 386 - prompt_tokens: 2859 - total_tokens: 3245 + completion_tokens: 435 + prompt_tokens: 4807 + total_tokens: 5242 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '20479' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: Analyze the sentiment of each quarterly update. How many quarters were positive, negative, and mixed? + role: user + - content: null + reasoning: Need search for quarterly updates. Likely documents titled "Quarterly Update". We'll list docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly + update'' in d[''title''].lower():\n print(d[''title''], d[''id''])"}' + name: execute_code + id: call_b39cif5o + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nprint(len(docs))\nfor d in docs:\n if ''quarterly update'' + in d[''title''].lower():\n print(d[''title''], d[''id''])","stdout":"3\n","stderr":"","success":true}' + role: tool + tool_call_id: call_b39cif5o + - content: null + reasoning: We got 3 documents but titles not printed? maybe title None. let's fetch each. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])"}' + name: execute_code + id: call_y0fbrvbg + type: function + - content: '{"code":"docs = await list_documents(limit=100)\nfor d in docs:\n if ''quarterly update'' in (d.get(''title'') + or '''').lower():\n print(''Found'', d[''id''])","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_y0fbrvbg + - content: null + reasoning: Maybe titles None. Let's list all docs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])"}' + name: execute_code + id: call_ceq2v75l + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor d in docs:\n print(d[''id''], d[''title''])","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + Q1 Update\n63b7a740-5678-4347-8d7b-37f869c0f300 Q2 Update\n76498bad-c7f7-4c90-a4aa-4caf826c05df Q3 Update\n","stderr":"","success":true}' + role: tool + tool_call_id: call_ceq2v75l + - content: null + reasoning: We have Q1, Q2, Q3 updates. Need sentiment per update. Get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_liuy6nph + type: function + - content: '{"code":"import json\nqs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_liuy6nph + - content: null + reasoning: no import json needed. Let's just get content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')"}' + name: execute_code + id: call_80syxghd + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\n print(doc_id, len(content.split()))\nprint(''done'')","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + 23\n63b7a740-5678-4347-8d7b-37f869c0f300 25\n76498bad-c7f7-4c90-a4aa-4caf826c05df 22\ndone\n","stderr":"","success":true}' + role: tool + tool_call_id: call_80syxghd + - content: null + reasoning: Short documents. Let's print content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_khz2vm8e + type: function + - content: '{"code":"for doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"","stderr":"NameError: + name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_khz2vm8e + - content: null + reasoning: Need to keep qs variable. Let's rerun combined. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')"}' + name: execute_code + id: call_v5t3zs08 + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n content + = await get_document(doc_id)\n qs.append((doc_id, content))\nfor doc_id, content in qs:\n print(''Doc'',doc_id)\n print(content)\n print(''---'')","stdout":"Doc + e3940914-d6f7-4455-889a-79c595be1760\nThe new product launch exceeded expectations. Sales grew 40% and customer + feedback has been overwhelmingly positive. Team morale is at an all-time high.\n---\nDoc 63b7a740-5678-4347-8d7b-37f869c0f300\nWe + faced significant challenges this quarter. Supply chain issues caused delays, and we missed our revenue target by + 15%. Several key employees left the company.\n---\nDoc 76498bad-c7f7-4c90-a4aa-4caf826c05df\nMixed results this + quarter. While product quality improved, marketing campaigns underperformed. Revenue was flat compared to last year + but customer retention increased.\n---\n","stderr":"","success":true}' + role: tool + tool_call_id: call_v5t3zs08 + - content: null + reasoning: Now sentiment. Use llm to classify each content. + role: assistant + tool_calls: + - function: + arguments: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall + sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_4c3q413c + type: function + - content: '{"code":"sentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the overall sentiment + of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: \")\n sentiments[doc_id] + = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"","stderr":"NameError: name ''qs'' is not defined","success":false}' + role: tool + tool_call_id: call_4c3q413c + - content: null + reasoning: Recreate qs. + role: assistant + tool_calls: + - function: + arguments: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify + the overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=\u003e'', resp.strip())"}' + name: execute_code + id: call_qwo653xg + type: function + - content: '{"code":"qs = []\nfor doc_id in [''e3940914-d6f7-4455-889a-79c595be1760'',''63b7a740-5678-4347-8d7b-37f869c0f300'',''76498bad-c7f7-4c90-a4aa-4caf826c05df'']:\n qs.append((doc_id, + await get_document(doc_id)))\nsentiments = {}\nfor doc_id, content in qs:\n resp = await llm(f\"Classify the + overall sentiment of the following business update as positive, negative, or mixed.\\nContent: {content}\\nAnswer: + \")\n sentiments[doc_id] = resp.strip()\n print(doc_id, ''=>'', resp.strip())","stdout":"e3940914-d6f7-4455-889a-79c595be1760 + => Positive\n63b7a740-5678-4347-8d7b-37f869c0f300 => Negative\n76498bad-c7f7-4c90-a4aa-4caf826c05df => Mixed\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qwo653xg + - content: null + reasoning: Count. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)"}' + name: execute_code + id: call_s8351th6 + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n counts[v]+=1\nprint(counts)","stdout":"","stderr":"Expected + name, got Subscript(ExprSubscript { node_index: NodeIndex(None), range: 81..90, value: Name(ExprName { node_index: + NodeIndex(None), range: 81..87, id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), + range: 88..89, id: Name(\"v\"), ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_s8351th6 + - content: null + reasoning: Syntax error due to using f-strings? Actually using plain code fine. The error arises from code syntax? + Let's just print using loop. + role: assistant + tool_calls: + - function: + arguments: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if + v in counts:\n counts[v] += 1\nprint(counts)"}' + name: execute_code + id: call_jdvk05nf + type: function + - content: '{"code":"counts = {''Positive'':0,''Negative'':0,''Mixed'':0}\nfor v in sentiments.values():\n if v in + counts:\n counts[v] += 1\nprint(counts)","stdout":"","stderr":"Expected name, got Subscript(ExprSubscript + { node_index: NodeIndex(None), range: 105..114, value: Name(ExprName { node_index: NodeIndex(None), range: 105..111, + id: Name(\"counts\"), ctx: Load }), slice: Name(ExprName { node_index: NodeIndex(None), range: 112..113, id: Name(\"v\"), + ctx: Load }), ctx: Store })","success":false}' + role: tool + tool_call_id: call_jdvk05nf + - content: null + reasoning: It seems syntax not allowed? Maybe the sandbox disallows dictionary indexing? Strange. Let's instead collect + list. + role: assistant + tool_calls: + - function: + arguments: '{"code":"vals = list(sentiments.values())\nprint(vals)"}' + name: execute_code + id: call_6emhxixc + type: function + - content: '{"code":"vals = list(sentiments.values())\nprint(vals)","stdout":"","stderr":"NameError: name ''sentiments'' + is not defined","success":false}' + role: tool + tool_call_id: call_6emhxixc + - content: |- + {"answer":"There were 3 quarterly updates. The sentiment analysis shows:\n- **1 positive** quarter (the Q1 update).\n- **1 negative** quarter (the Q2 update).\n- **1 mixed** quarter (the Q3 update).", + "program":"# Consolidated script to analyze quarterly update sentiments\n\n# 1. Define document IDs for the quarterly updates\nq_ids = [\n 'e3940914-d6f7-4455-889a-79c595be1760', # Q1\n '63b7a740-5678-4347-8d7b-37f869c0f300', # Q2\n '76498bad-c7f7-4c90-a4aa-4caf826c05df' # Q3\n]\n\n# 2. Retrieve contents of each document\nqs = []\nfor doc_id in q_ids:\n content = await get_document(doc_id)\n qs.append((doc_id, content))\n\n# 3. Classify sentiment of each update using the LLM\nsentiments = {}\nfor doc_id, content in qs:\n prompt = (\n \"Classify the overall sentiment of the following business update as \"\n \"positive, negative, or mixed.\\nContent: %s\\nAnswer:\" % content\n )\n sentiment = await llm(prompt)\n sentiments[doc_id] = sentiment.strip()\n\n# 4. Count the results\ncount = {\"Positive\":0, \"Negative\":0, \"Mixed\":0}\nfor s in sentiments.values():\n if s in count:\n count[s] += 1\n\n# 5. Return the counts (this example prints them; in practice you could return a dict)\nprint(\"Sentiment counts:\", count)\n"} + reasoning: Seems the previous code didn't persist variables across calls. So need one single script that does all. + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '1776' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to output JSON via tool call? Actually final response must be JSON. Use functions.final_result. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"There were 3 quarterly updates. The sentiment analysis shows:\n- **1 positive** quarter + (the Q1 update).\n- **1 negative** quarter (the Q2 update).\n- **1 mixed** quarter (the Q3 update).","program":"# + Consolidated script to analyze quarterly update sentiments\n\n# Define document IDs for the quarterly updates\nq_ids + = [\n ''e3940914-d6f7-4455-889a-79c595be1760'', # Q1\n ''63b7a740-5678-4347-8d7b-37f869c0f300'', # + Q2\n ''76498bad-c7f7-4c90-a4aa-4caf826c05df'' # Q3\n]\n\n# Retrieve contents of each document\nqs = []\nfor + doc_id in q_ids:\n content = await get_document(doc_id)\n qs.append((doc_id, content))\n\n# Classify + sentiment of each update using the LLM\nsentiments = {}\nfor doc_id, content in qs:\n prompt = (\n \"Classify + the overall sentiment of the following business update as \"\n \"positive, negative, or mixed.\\nContent: + %s\\nAnswer:\" % content\n )\n sentiment = await llm(prompt)\n sentiments[doc_id] = sentiment.strip()\n\n# + Count the results\ncount = {\"Positive\":0, \"Negative\":0, \"Mixed\":0}\nfor s in sentiments.values():\n if + s in count:\n count[s] += 1\n\n# Output the counts\nprint(count)"}' + name: final_result + id: call_tyjck0dw + index: 0 + type: function + created: 1772549446 + id: chatcmpl-663 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 408 + prompt_tokens: 5010 + total_tokens: 5418 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 3a9012b8..b986f8db 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: - - '7319' + - '7698' content-type: - application/json host: @@ -139,15 +139,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -167,6 +165,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -180,7 +198,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -188,17 +206,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -214,44 +230,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -261,7 +270,7 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: How many documents are available? role: user @@ -315,7 +324,7 @@ interactions: response: headers: content-length: - - '547' + - '519' content-type: - application/json parsed_body: @@ -328,20 +337,20 @@ interactions: role: assistant tool_calls: - function: - arguments: '{"code":"docs = await list_documents(limit=1000, offset=0)\nprint(len(docs))\nprint(docs[:3])"}' + arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' name: execute_code - id: call_pu34e9fx + id: call_qqoyb2of index: 0 type: function - created: 1771924517 - id: chatcmpl-236 + created: 1772548188 + id: chatcmpl-356 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 56 - prompt_tokens: 1621 - total_tokens: 1677 + completion_tokens: 44 + prompt_tokens: 1728 + total_tokens: 1772 status: code: 200 message: OK @@ -354,7 +363,7 @@ interactions: connection: - keep-alive content-length: - - '7939' + - '8137' content-type: - application/json host: @@ -365,15 +374,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -393,6 +400,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -406,7 +433,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -414,17 +441,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -440,44 +465,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -487,7 +505,7 @@ interactions: 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. + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. role: system - content: How many documents are available? role: user @@ -496,14 +514,13 @@ interactions: role: assistant tool_calls: - function: - arguments: '{"code":"docs = await list_documents(limit=1000, offset=0)\nprint(len(docs))\nprint(docs[:3])"}' + arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' name: execute_code - id: call_pu34e9fx + id: call_qqoyb2of type: function - - 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}' + - content: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))","stdout":"1\n","stderr":"","success":true}' role: tool - tool_call_id: call_pu34e9fx + tool_call_id: call_qqoyb2of model: gpt-oss reasoning_effort: low stream: false @@ -554,7 +571,7 @@ interactions: response: headers: content-length: - - '704' + - '430' content-type: - application/json parsed_body: @@ -562,20 +579,272 @@ interactions: - finish_reason: stop index: 0 message: - 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()"}' + content: '{"answer":"There is 1 document available in the knowledge base.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' role: assistant - created: 1771924519 - id: chatcmpl-487 + created: 1772548189 + id: chatcmpl-173 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 117 - prompt_tokens: 1793 - total_tokens: 1910 + completion_tokens: 40 + prompt_tokens: 1814 + total_tokens: 1854 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '8443' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: How many documents are available? + role: user + - content: null + reasoning: Need to list documents. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' + name: execute_code + id: call_qqoyb2of + type: function + - content: '{"code":"docs = await list_documents(limit=1000)\nprint(len(docs))","stdout":"1\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qqoyb2of + - content: '{"answer":"There is 1 document available in the knowledge base.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '620' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to output JSON in a tool call. Use final_result. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"There is 1 document available in the knowledge base.","program":"docs = await list_documents(limit=1000)\nprint(len(docs))"}' + name: final_result + id: call_hli4bq9m + index: 0 + type: function + created: 1772548191 + id: chatcmpl-603 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 65 + prompt_tokens: 1865 + total_tokens: 1930 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 0a78fe8b..34bce792 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: - - '7390' + - '7769' content-type: - application/json host: @@ -99,15 +99,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -127,6 +125,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -140,7 +158,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -148,17 +166,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -174,44 +190,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -221,7 +230,7 @@ interactions: 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. + 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 @@ -275,7 +284,7 @@ interactions: response: headers: content-length: - - '610' + - '532' content-type: - application/json parsed_body: @@ -284,25 +293,24 @@ interactions: index: 0 message: content: '' - reasoning: Need check documents variable? + reasoning: Need to inspect documents. 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"}' + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' name: execute_code - id: call_2ao52bvz + id: call_c10kdj1q index: 0 type: function - created: 1771924591 - id: chatcmpl-850 + created: 1772549571 + id: chatcmpl-647 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 68 - prompt_tokens: 1635 - total_tokens: 1703 + completion_tokens: 45 + prompt_tokens: 1742 + total_tokens: 1787 status: code: 200 message: OK @@ -315,7 +323,7 @@ interactions: connection: - keep-alive content-length: - - '8037' + - '8227' content-type: - application/json host: @@ -326,15 +334,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -354,6 +360,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -367,7 +393,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -375,17 +401,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -401,44 +425,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -448,24 +465,22 @@ interactions: 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. + 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? + reasoning: Need to inspect documents. 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"}' + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' name: execute_code - id: call_2ao52bvz + id: call_c10kdj1q 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}' + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_2ao52bvz + tool_call_id: call_c10kdj1q model: gpt-oss reasoning_effort: low stream: false @@ -516,7 +531,7 @@ interactions: response: headers: content-length: - - '524' + - '523' content-type: - application/json parsed_body: @@ -525,24 +540,24 @@ interactions: index: 0 message: content: '' - reasoning: The environment may not provide dir(); use globals(). Check. + reasoning: No preloaded? Let's list documents. role: assistant tool_calls: - function: - arguments: '{"code":"print(globals().keys())\n"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' name: execute_code - id: call_rjj1hq9z + id: call_2jgdkdf1 index: 0 type: function - created: 1771924592 - id: chatcmpl-798 + created: 1772549572 + id: chatcmpl-95 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 43 - prompt_tokens: 1777 - total_tokens: 1820 + completion_tokens: 45 + prompt_tokens: 1827 + total_tokens: 1872 status: code: 200 message: OK @@ -555,7 +570,7 @@ interactions: connection: - keep-alive content-length: - - '8486' + - '8931' content-type: - application/json host: @@ -566,15 +581,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -594,6 +607,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -607,7 +640,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -615,17 +648,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -641,44 +672,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -688,36 +712,36 @@ interactions: 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. + 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? + reasoning: Need to inspect documents. 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"}' + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' name: execute_code - id: call_2ao52bvz + id: call_c10kdj1q 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}' + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_2ao52bvz + tool_call_id: call_c10kdj1q - content: null - reasoning: The environment may not provide dir(); use globals(). Check. + reasoning: No preloaded? Let's list documents. role: assistant tool_calls: - function: - arguments: '{"code":"print(globals().keys())\n"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' name: execute_code - id: call_rjj1hq9z + id: call_2jgdkdf1 type: function - - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + - content: '{"code":"docs = await list_documents(limit=20)\nprint(docs)","stdout":"[{''id'': ''c59247d8-8db5-45d9-9cf4-12841241dbc7'', + ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:48.188978''}, {''id'': ''b1843b4d-5e41-4c91-86e0-7eb6040ac5c5'', + ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:49.871504''}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_rjj1hq9z + tool_call_id: call_2jgdkdf1 model: gpt-oss reasoning_effort: low stream: false @@ -768,7 +792,7 @@ interactions: response: headers: content-length: - - '581' + - '593' content-type: - application/json parsed_body: @@ -777,293 +801,24 @@ interactions: index: 0 message: content: '' - reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could - search. + reasoning: Let's get documents. role: assistant tool_calls: - function: - arguments: '{"query":"founded mission company","limit":5}' - name: search - id: call_ee8bn4w7 - index: 0 - type: function - created: 1771924593 - id: chatcmpl-566 - model: gpt-oss - object: chat.completion - system_fingerprint: fp_ollama - usage: - completion_tokens: 51 - prompt_tokens: 1862 - total_tokens: 1913 - status: - code: 200 - message: OK -- request: - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate, zstd - connection: - - keep-alive - content-length: - - '8970' - 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 - 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: 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_o1c87rz5 + id: call_0mpn1a7r index: 0 type: function - created: 1771924595 - id: chatcmpl-37 + created: 1772549573 + id: chatcmpl-587 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 51 - prompt_tokens: 1948 - total_tokens: 1999 + completion_tokens: 65 + prompt_tokens: 2039 + total_tokens: 2104 status: code: 200 message: OK @@ -1076,47 +831,7 @@ interactions: connection: - keep-alive content-length: - - '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: HaaUuUY7DjwkkL886DwEPD/tp7p9I6g9BJSDPULKvzwghos8OX6tPOKTEb3FQ9q8nrkju0dJKby3ovg8ewSku9oQTTsy4ZC9eXlwvBkVB7weOoq82C2qPPmziD1J+DU9cNzaPO5/Uryt+Oy8NIIjvcUdirzmCxQ8KhmXvNdJ5bwuilA8N/WFvMOL6TpOhVG8BNb0u6FlPrzURxM8f5dtPFmuEj2vYRS95l+NPAGbpzxBVLG83cy+PH4DHjvNTts74otTu0Rwory48kg8HJGcu0bjtTw1rM+8j0UIPJoNKrzovP88wxZ0uwBPG73FS6Y6Z3IbO4lwiryI+q+8G1QJvePwXLu5VLG8QeFtvNSJDb2qoro7v90fu/gFqLxO9D88zeIwvGaNuThV7ym7vUoOvT5hrrsFebg8C+J+vKdAyLqTZbE78cQ6OyCvyDyOEw07DwrCPHGMjbycNzc81UQAPJFvoLwbRsw7vDuJPDkTjLoY0KI4+uqHPB6dP7sZSSM8Ze6kvKx2Q7zVL/28WVZ1O+hYYbwQgnq7RQaAO0Pl2rzr0lc7eHH6vPMbzbx7JxS8pdCgO8axP7wCRmU8dY7MOtN6uTyiUaY8mPaUvNAYUrzF0pC8BBuTPKFZlTuskDQ8ltznu8sGzzzZUiQ8SvBHPHyJD7wfcw+8NIb9uolZA72rxKa6qnrfPIkCDz3vyMW8FHYwvKBDhLyMoTC8n/YBOsyOAj3aObi8IZMhvXOxg7pouH28m9s8PM/+njx++rk723yuu+Ppx7zCfS68RrCMPL5okDwjSA88zErtPOSz0LtPBXM8ECBYPKQcmrsuUvA79IPmvMKoCz0ZYK88JamwPLBcyzztK2Y854SqugZSuTqSry47vYkrPOC8cbxJTK48VaDKvB0wX705Vlc8FYrEu7mXybzj0Fe8HK9svBuCODzmA+68f2lduoQiC7sQzAA9+sv9uqbXlzsYc3c8hXL/u20PdzxOTpw7afxQvPwaazx4x2U8CsujuAjwgLztaO+8HxTiu/mJqDpeFxm8BbFevGMg3LsZ96W8WmktPAqJED1tRDE7w2wcvPd+oLz1EgM8OHV5PF5grLt20Ym851bCuxyK8bo+/6K8x2lcu3jgjLzKQlW8JqI6PG9OP7umISg8FSEZvNsHn7x0W888fzayuvLfxzukZ5M6B2KauyBZkTxq2te8icqXO5rPSjzwCHe7Z2EUvIKid7txjxI9nHQPPYh5jrkp+B28F4wmOx/mCbs1Tdc8Wb9lvCDXRDxm3bA7JGaZu4bForwrPAg8hg4nu1ddgrw+VdG8cPtDPKOhBjt45h87xbuZvJRVKbyYU827/hC6OzYW17xjcv08Bbl7vBuh0bywLCI8VDpEu2S5Zjyt3WU743gbvBSmuLyRTiW8qqxou2DkEzxxz8w5d5FWO8zyF7otYRQ6GmVTPeHvSryJxAc7x1kRvLLsmDweyty84ps1O3xYMjzA6Rg8MACVPH1IBL296I07fWSpPAmnMTwYYye8tRtCvNRHID3ahpE8+UMYvZ0EfTyB+X47CULlPKc7qzwJ6PW7GFKrPNr5pbwCHM47EyE4vA93CLy0Zeq8fSMEvJAQxLthfz06ozCpuwTz2Du2I+s8k8Wruqu7FLxnBgq7R5QrvV8ziTs2AMG8Hsr9O+Ps5DsK+xm8cDMDvQMe37lggsA7c46svC9/rLv493k70R2DvfnCxLqWWFO8EMDDuuZEYTzb3tc8piuAuzlOlTyQMZ68RH9PvKL4kjzW0ay8HGPAujVz3LtV6HO61qeiuapRkzxe1Io8iLUPPOaHOzzkoZa8uCrSvON4q7zHpIC7FWIuvExZj7xvt7Q4xMpPvCMwMr240Jk6+VWqvL7/djzdcu+7IA7AvD/84TvQWB+8kpJwul8UGbwd3BK8zXmjvMmL+LssArA7/KDXuIt2cDwb/5q7ZhJvvEWwMD3/Rwc8HZ91vDDdhjxW7Qy9VEvcOwNeGb2rHJO8cWqWvEB9rjrLxHS8KIRCO1W/gLxFNrM8+DiKvH3IB70WzAU8BNx7vPzFqjx2em27+XE4PP3HsTy3/ga8OYuIvOxKMTxXe1w8tFbxOp6rBr0QucI7Z+HeO9YnjTwVZC88BJ9EvL5fATztnuy8PQqzvFOjIzzxy+e8EqbIvHS3wjqWE/g8dVW1PD2MH7xUxPi8vqG0PKT1ujvZpxW95OYaPBrIvDuN05W8XDJivcps5zzi9sY7/iLEvJN2froipo47nhRHO3WrqbxavC07HUKGPBZTDD3F9oM7qdZVO5tZX7yMcvA8vbCfPAFyHD1LhHw8ZL5qPCb4h7wqLgq8+p2aPH3ffLsD0py7e32fPGriAz170+k8SuPuvKAmnLyMXo88FK6dPGlCnjw64Ry9DR5JvG95ST2DuDg8SzkxOk/ObzyaZfg6gBNAPOAzUTtIKTi96gh0OzWTmb3x3qU65AxjPRRzEr2kjZw7dldavDVXgrw4s7e7gXWAvBJ59TygnRe9gJIrvNqMBb2Ojew8DbiGPGtQJDtveBE7pkZhvKEQSzwL4t08EOJHPIZOXro3BEo90g6pPBvB1bwqcM08FPmKPDz/7zwW1vE8b69+vKEx/TsEWbI8c2rsvIs47bziwsy7kES7u6GNBT1gcsm80dNiPOsTzDvjbmA8ofnGu3sjGz1ksCK8Txq2vHqMz7odUnk8RBi7PJBrB7vYmYs85rQKPA1C4jyk9zW9t68cvTXjPjxqIVS8LR4HvNaSnDzyrIC854iEvHSSnLxrKJI863cWPWTEJb1xY0u8s3rNPM5kQDytLA29v5G4ulvUujvHdAa9tDYAO7ORdLtcGAk86GtRvWHjdTwDFXQ88jEyPe/QiLxKUCe7F0uIOim417srQMI8Vgf+u8kU/zuIX3m8fS7zO4OVoTw/WcW8NbBGPXgP5Ttg+NS63YjgPAt2MLyBn4e8L9dfPLToyTyL85y8nPkUvEWagrsnbxm8uZtVvGx/4zvI3iU88MDJPB9WRzxc8is9A7AlvDMB/juw+Py88IoGO3rdTTvrzNc776SdPOIm4DsVxsW8tZXRvAegc7ltBYq8ItytvLKYeTsJDaU8P6EGO/hx+7tc1q088eeAPJna8DwjYK88OP2dO3Vfg7szNBG82QIuu3nE9rrF4Be8GCYCPdbvE73R1Ti7kPGvvG1iEbwfBry8XdYJPT/0urzg+aS8oaIvPbWD7rvuxdu8MajsPNlYZzztxx69JdoyPFmnvTrPCyq8HfMzvPveHDzFsRQ9nzZsvVXnGr0Vfrw6ezmyu3eJR7wovnW8SoQdvG4gP7zEbRu8fSfUuqIAR71Guhq8q80/uvGPhTuiQcU8XFpAO9hUBb2j8s48wcm1vND6EzwVyLW8hp9FvextabymhZA4Nx49O8HXbjtCv2W8BXzqPNNc/DqlACO8B8UHvexj6LxfFXq8N+LQvLPlDrviDvc5JtrEPP36u7yRbVs8eeCPPBZwPbzXRWg6mSDYvCsS9zz7upY7HfhFuiC2JbyQh0c8ruafPBRHBL0ea4o7d7b5vOfbcLu4t2o9a+6MvIWNyLxACRU9b/C3vFfd0Lz0qfE8ZB4dvcWzV7vsRKw55PAmPacfibxHWKi8F9mpu4xlBz3EFum7IoomO87/Yzwbni68F0vrvJ00OTwdQKA8gHMHPBudIbvNUR49Xee7PB+1KzxDCfI8zVzUu4PkzjxT0VK8attDvYj4Bbslmos8EwhrPPJnbTsch5S7or3OOxqPzTzdaBI8lpnqPIQ85DzSLsM7KTwrvUn9k7zjwei8/l+SO91T+rzBhTq9mpF9vNgVerwKwLs7gFpyPGZfMb3qKQm9IyAhvFCT6bxczNA8heRevCziIDwEr7w6LpaPPVjdAjwKQoe8q5eqvCkA+Tz1Od67xN07PBljMrzX0oS7KuKyuwYfJrwYJMg8hnskPIxfbrvmY0s8CQgPvRsLrTtoEuy8a16xO6UZbjyoVIo8fcvmPKiA5Ds5Izk8uUG1vEDsoTthnwE9BS1mPOUpITwwLfQ6nZDmvIIPNTvdzZi7x8CAvOm2hTv98JO8ePzhuzxbJDroIxI8/jwUvddBOjyFzQG9LlDkPBtn6TtFPpE7KwGcPO7shbzpRgS9RlBLvFwcVz3kSFK8loPwPEom3ryHiYI7lMykPA+CDL0AFso8vDlrvAn+Qzr6mV+8inr+uzY+fDwflug5OtQvvFZjhryyIHU6db6PvLjmYjuZX407Si3QvBHGNbz483K8igbYvNUmEruW5SA9spLvO2MaETystSs8LgYPuxb4qTzml4s8bW6eO8X5Hb2xqsS8MK/ZO3Ryk7z+6ZY8CbJ1O1O4CDryJi28K/HhO/z8c7y4W808cqLXvELrBrsr/Uo8Lq6ZO2irKz0CDBk8b4Gxu1s6t7vqWHy8KBjFPACgYDxO7qo8BVyDu9WH3brl7727QW0VvQaD+ru5lAk8jvhiPD/A1DyEgSo9+RjtvMFwhjwkwAC9VbS9u4tZ4rtWNii9mzlHO+tdHz2xjhA96doyPPl+hzw8Jby88FQqvIDylzynI5o8V3DAvLCn5bxuGTs8o8xqvQw8KzpMlKu8VYYLvFJHY7yY/5E8m/AMvRyllTsBUCS9QOKRPOqKxDvxcze8lcQEPIngaD2q0bo6WKj6uey4DTyYwxa7idVLvP/WHLv34IC8tnMHPTUQMTwRYsi853d1PIdoXrttVDg5ZdVjvARllbzHwU87gHJrPOV2DT0I+kE8ZFlFPP3zBjywr+c7CZ2XvPQNHj1sH1i8dS4ZvHkmfjw0+w48h9/RO9HBLT1DthU8hCPlPKU6gLwJig48zUjKO0FsRzz/QsS7EkYzO/HGqrmFVdq8CuqMu64icTt5z4m6qiBOvOPDQryswrU8GR/APFJa5bwL/UY51S0fPUeJSbyXG7W7zMcrOwekM73mBzC8O+8UvLaPID2SNwS9xhjMO04No7xcmDe74g5OvCaMQjyLho48wY3PPIePML1pZGM91fbEO8l58TyE5HG8G/wuPOlO+jvTMgo8OX0evADq1Dz810U8IFBDujayyLz0C5s5iJbyO0tPSDumdh48ruZ9PJWRmDxEnAQ8ZePlvOGT07wM4ju8L2LJPPVLqTu2Lxy7iu9hPLDI6jof2ai858wAvCb0nbwqWnO77EG0vPLjPDsAhDw6WiCqu/V7qrtBRAw9GmfmPGvq3jmM7je9E8fwO272Wzx4rnq7NpkZvHqNT7xJJYg86sBcPO3HRTtQTTe8yWzovO51aTxCyMs7RzDSPO8cRjt3Ut88i9pvvMrjVjxgxeS6PfJxPOVBhLynkmY8iX50Owma8bxyHN+8XwU4vZDgcLxZ53075bGEPFldZryGGIu85IeqPOz4EDz1rsO8CCRaPCfqDz3EVUe8Bx68PHDiW7wv2CM9HffMu6l4HLyNP327BhlpPGHw4jy6t4W81HTSPKuJTbzvFJi8uSowvGKWPbxaqkG8OWejPIYqgjy4c128S0S9PPAzJDw2Fd+7Q+YEPTNBlbq8fwu9PpqUuxC5Pb3wYgy91VDFvK2VvbwmjAI9uXglO32RRDyP3ii8uwEvPHrPCj3yjso7BBu4u3xAJruIyDA8c5qbvOJHcjuQO3M8UfAIPV7BnTyIYCq8CJf7uwggT7sYDO+8AXCUvAkX0Tw65yE7I9luvCfgD7yac0K9dHNxvL82obw1At67bvEOPHpGZTuJ5wm7CzXpu9QngLxknAi9oRRXu/4ckjwfiK+8/IEKvZAY+btjGqC8IvbAPDKx3jocs0W82FVEPKapAz253c07oZrgvFCh/TsS1w893umQvDD/ibndKXc8eV5PvIpJOzyCnaw8mbK7PCFHpjwf8Qw8YKEtPFMwfLpt4Ae7ye8xPTsdarx4vwI80yxsvK8uQ7tJaRM57iNWPJmXNjxTYuy8A6eYvJw3ALw5ZCo8+A35O6zwhDxFShE8LRF3vFiyoTxANx08MdUCOx1LJD0GLtM7I40JPBNL1zz8/9y7lSWPPFeFKTyYauI52zVTPVOJ5zwP5Jc8nhD2vMfo3jwzT648mKtIvKxfIzz8C9M8nocBvF85STyj8xg9bynjO4RPWj1eYVS7qS1oOl7rbrwyI5c8m7B0vP8eyjunSSo8f2SnPG0nrbsjBaY8/Bk2vUkzl7y7F4k7tmqzPAGYHDzf3ZQ8XPEqveErBjx1EKC8OBUcO7j4vbyU9XW8lut9Oypmxrxd/NY83YDJO6PHEDzqzLE80CcDPNekuTzX/w68/5yYvAXybjzWRai8pddcPOevSr3V/We8JWAIPbORSjw8GBY8g6EfPKxCebwy2gQ9+LOlu2mQnzbk8XW7DJUqu8eDO71zIpQ8f/+VPDiK0bzU4KK8GBYhu7q+jLy+8Ec7mS/bOkDpwrw0Gjy8tDUFvNfyljwxvZm7P1PdPOJ4tzyYJP08WuESu0W1Hzy7nh47ELzpuLK/ozzh1wa9au3FPHOjS7xvhJS8fiABPFZLTDuCHgK9PJnHvKpGBb2c1XG8+uABPSvVMDyri3m81skmu7BofTrtKRM9HUe+u+AeT7zRm688MZg5OqdaqLypDmo8rbiHPProETwwI8w71KsYPcwWLjyQX4o997W7PClahryB7Og7/6X+urndgbyMwR88IcgPvS22V7zFeuS8ISCqO+PsIDx+WNg7Vn7HuzD32jsBpD49LS8dvdHs4bwJqqY6piN+vK6ktDzCKDw76ywJOwhm3bt6pb281fdRPD8qAD074nc8euUyPLhLmDzmeHC8mvvQupeVGLzZksC8Q2DYuzDif7uLj6q7dUsGO+0WGDw+PBg7Nu1wO+PHK72s2um8HRGCPN0kpbzL8XO8xDtVOxZf3TwU2ka8mhTrvJ/oxzv7XCi7JSrJPLezMbpiKKW7wIaYu2QaSjyMku47VUC3PBZEl7x6ZA68T8z/vDK3Cz2VVZg84XONu0UewbzBM5c8RuMkvcNTy7zye8U8XheovPOVVbxMQ6K8q5mduz98hLxJD8y6/HD4vAtmm7z44Xe6TbuSuxFmBT0XBYO8ehwivJ4ujbzHHIe7MU3ZvDKoHzwUthY812kLvVQlcDy6BW67uS7UO6lXST1e03M8ajURPaQiK70QBfq82Q/TO/V79rwnhx486ejdOwJ5EDtjMi+93cY3PY9RI7zBDKS7nRIVPL9G57wc0ys8D7aOvPVpq7w57xm8EOOZPOJnvbtORL28BlgePFvfAzwmyQ69uqXoO0gz5LxaNke8+K9lPHpjFz0fVi08bXLKvPcU7zzpqfm2r7FwPQQtGry/lpw8Q0jBO9TxljtKc5w8Tq0kPJvw6TyfUxW8ViTRPB5Z7LyLeWQ8IXysuyz3sTwS/AU8Bco9vRbNIj0sPTw8YK54PEE2hby7ry28x5n9OksV6DujUSA4FwwUvayQID1mc9o8J8w1vJyeSzwbg4w8UsLAPC0MDrw1M5m8lbKAPJr/BDt9xzY9RlM2PMEqDLvMXAC9PfSovPHJCj2tOFW7vvkAvYASJbym8yA8c0yWu95ODb0zdB48UgDRvATtUTxex/q6B3CHumT1jrxg8XG7exYfvSh+HD08W9q8z5JtvFk+xjuG6d467Xi8vL/riLz85Cq8w0qZPDq3ojpYDWQ8URi4PO88Az3jvCw8YiGbvKyWnby+pz08Y99jvNanhjxKa2A7+RlevLaOaDufOAG99AzxPNSQX7yH3WA7MdsgPRfB0DtzQU48BOy6vPd7gDxU5AA7p7vEOowp8Lv69ZE8PfukPL2+ZLys8Rc8V1uEPDBxDL1/g7a8PKl9vOnpaDzhWga9zV8IvcAYrTybJyM8FXg+vC+Av7sfVbs8+NLPPPhe3LzTIis8KFTJvDeCdzxtwJ48uAIUPK5ESDwVN5q85dwGvShTkrx+RJy8IlPWPKnjfLwK7h897e8qPMy1lDzknkq83C6EPITtCbYYV4i80O7svOm3NrtUdxi96+zSO5vzkrxJCsI8D1FSu54xzzrTVhe8UqUuPaQ3Fjp89Sw9R9S3u1No6ruFXlA8o2X2OzuO4btzWQO9SxryPCsVSjrteby70GI2vLD3wbsRR607SeumvGlu9jzf0A69xFC1PJTrrrzx+2M8DFmjPDIhgjwCICE7nap+vGbLULy6/5w85SOCPE/zWbwQEbQ7eR2zvPzad7xTap+8su4Qu7a5VbyJiZc7ScyDvEKKljszeBQ8U2nfvAwLgrxaFZ+87akoO26KtDxVv908y6WLO7Ihmjyk/wI95092us8G/TnpEGg8voKuPOzUy7xRGYY7SuKtuojQEj2z02e72LnTPOK1irynl/68ojO2vLD+vjzSLs+8L2acPBTlrryy1WI8wS9LPLKRRDzGpqC8IfoJO+f1JDxKT4o8GD5UPFoEyDymNyw8vQmMO1q0uzvRfTW7wlv5u3YjsbsHs+88WJkAPUUPOjwE7+e8Js4NvLH0jjvbSWs8Zxw3vFI0DTyIFN68VaTAPLGSh7yk07s8CJGEux9EdryTrrG8kcpZvOT2zTzehw+9SBhovJzLnTybc8C8Yc2oumvdYboIAQA86S6Eu7V137sYt/I7zpofvLmmm7xOvJM7NZPrPGejcbzQUEQ8o1dqPaVg5zvgMw88/h6tuyxzTzw66oc8jFgouqpq4rucbuI76xo7vMiHmDxxw5O8XgQvvAvHbjx/F9e8i46CvPtvQbovNzU95OUqvMQouLyRCTC7EB8Wu/2J5jteBNa7r1aSO3wJZ7wT7oc7WKVeutYEkbxTw3c7gvzPO5ccDT1OIze8BNMAvOZhGLu8yxE8505JvB9/Mbx97Iw8vr6RPGoi9zupKKa6Cky2PNPJtDwPSju9YhEDPbBnaDsWDOW82M8dvCQyrDvshto7Ye03vfD9jTyOZSk92FoqvNYuE7xBEd47wzNYPHVLF71Rc4g7NTu1vBEYDL1NQHO7TqRau/xhb7zWqE+90KRCPFR4oTy0LxW7HxwRPfnKo7vXDYi8isbdOhCw+TvgjQW8wGVSPMTZgbpmJgO9cU7PvI6Q6DzBgIS8pAbqugmjbLrtM8q71jRDvPN29Dz80SK9fhdEuQU6i7yzXsG7sUgGugvn6jtS0iE8fzfSPB5pKTzpb7w6+llRO+kjwDvOgHC776svPJpxTjyvygo91JNXvDBxhDzn3ts7k0uyPL/XQzyM40s8/X8HPA2GyzxL49e7CTfTPJlamTynAe286ZGBvCOjgrt0ZoM8g8dEPL4DyzsjGui8PFmJu5OTvDxilIK8gO5WPJmqVDtvwPG7OMXjvAhZkTxAZTw84Q3Hu6epsLyW/o48NfXnu6UjFLv7dja8KNzMPAapHzzYhTE9Wt6aO575oTz/MXY6QDNSvBRcnLwvvZu7L7bIOofOX7w0Crm8sjgausMhmTu52Oq6+YgFu0dbEzzK3Ow8I9sAPaPUhzzt3xS8Tf7Mu2lBtzypoO+7R1ePvMEChzuRybA7saUFvXT3jrxVkBQ9ZjdMOyCPELvgq0S8LP9cPEJPvzwprVI78kcXPJchnLtj6J+8NKeBPAkfFTug1u05HKT5PB6eFL03VsI8Y/OcOpx9Ar09d/g8VMbau+6hfjzv8pY6XUH9vHFyoTxp6Og8QsKjO6lVOz0UkjA8Ka6hOEJYMTzV75A60YTFu4peJzs7GZk8NeiqvLOsV7wjOqi86a11u7WtzrweNmU85PmTvE14mbtYPsU7qFlmvCAppzyvela8mSmRvH5TOjyvXqy8HhgtveQotDwxrMu703mxPHo6yrwZG3U5GwzCvFlsTDwzVbo8QNLDu8LNqzrRhOa83NhPvG3Pw7yYxZ68094fvHiswjxOF58835PbvFWWC7zxFfo7Lvt6vKJCY7pBUNy7GjrGPOToiTwtCw281JqVvMEayTu3G508x1D/PCA7jjsEJK68MbW1vFvkdrxiIga9Joq5PLZ+azqvwlI7hi9COrhIJrwECjW86SURPUVZrDwEMyu8W97KPE7DG7zR9Wc8mdiOPBd9djy+6ym6vk2VPBFaFzygL5s8SCc9PfPJnTuQnMa847GOuzf+9DwKLZc8K/gruzO6HDxoWQe8+SAdvEIuAT1+VqW8ODOPvKiMy7y4SY66KZguvfZ1EDzFJcM8UoUSPPH0rjzuQYS8h7izvDOZAzwtWBW9/NLEuyt0I7xr+rO7ocbFu4IEkTyR6oI8vE4TPbEVubv2RMG7+05nvE2u4zzazCI9CthAO0yUnjyI7yo8bbf8Oz7qILxuQ3o6pN+rPHVpurvz0L28WW8oPAW51LxLkHA8vMugPHFTBTzH7866rRzbPJiTbTzXvXy8hQ6nO4G9v7oGiD0613TQPG/tibzEQ5g8zBdYvIXw3jwRsdS8eTLZOq83VbzBT1y8HqgzvGbTYbwHQ2q7/2ixPGoni7yyB8I7p1MQPUZNGzw3lIo8fHl3vXDc77zSZz+8qaR/vKc6+LrsoAa98wxVPGdQPL3C2ES7at2/ugOCb7xDAUi809TiPHQrOjyKfPS8M+QsvEwwB72CNlO9DfOAPHdtp7xHZfq8hjDCPPxVrjvgQ7C8ebn8POjuDjwreBY8/00ZPZF3H7wJQSK8gZlFvaRKt7tubpc8LtCmusz+iDwyDj09NhPyu1owLDwE5gu9Y/6YPMEpuTyT5q87m3covGRtoLzqK1G8d6nLvB/vdLyfucy8I/ZmO1oQNL2dmam7Mbp+vKlf+rtIUTY7wG76Oj6tzDzlc+Q7saCGvKZlQrx8DH67v2aTvILmC7w82cW6yJGcvA4jqDxHq5e8nY5FvJXDLDylv087cIv/u7qCVryhaS0985sPPHUzMr3pI0i90XbAvCkr5DwoPh48QcWKPL4YCj0cmjK7FOkFPYAChrzhDOc7Ev6JvLe+9rvhxLA7qLz7PAESg7xwXY886yVBvM8djjrg7ai8CZ2cvMwNaz196Zy6RIRyvNK7nTq+eY28rwvgPGB95TuU6Ku8rnLhuTAtdLuJ8UG89LXnOzbuMrvSBWy81RmHu3j0+zshIuq8quGJO0ZWfLxAnrm81mAjvMC5BT0r16a8zE40vPpVgryEeS69+myKu3JjlzxvV227aL0GvZ6sTryjI3a81f1iOfVl/7tiQze8xBuOO5jxujxk0Tg88lctO2b0Uzwc2Sk8M1BfPHEYibySzwW9cG5dPKwc8TzDrYU7eT4UumIumDwkJDw8eSwFPWXrEjsDOna8VMsJvZGdHr0IiDW9mJzqPL/qhDuUtHQ8tNMGPJ1WMLwFRXe8OEUwPMHLijwQ1wE9HQI5PAMcTrxc3ss7EXJDO7av6Lzf8148Ap+BPN8/R7x7Iw68oVPcu9ZEI7zqJkU8RNcPPKNMDTzn9VG8vzzLPFRSgDxxfcS864KWvLRChrxbFOK8ekccPQGOMbw052G8C16PO20/tDzJM3A8H3TZPBw9jzyqKrK8VgIwPUzpH7zptO87MIKKPOFVJrz0rGk81fG8OwhQSLzGcYK7wb5xPMgrW7wDBKg8cbczvIaXAbsBRZG8kP3pvMrJq7xpMn670pOGvN5uoLxEJIC8U0YOPaINvLsg8aY7x886vR9Sb7wGsxA7xUomux2xJTtKf687axBgOy7N/bvb9iU81wFWu4l/abvBZwO9MQMouhIwNTwYyC08J9iPPDYdwzv36vo8kP9FvKsjYbq4ZCI8heWyPH4+lLzLmm29CDOFtwdF7bssgCU8URJrOsXuKTyRRv27Q0irvHitVjwDWHg7S81DPEhGRzuDIRO9PSfIvK6D7Lx5OPQ8OBgsvfYaTjz3WJu7JlYIvWUh5Tt6VB28IiKaup0iN7uJsJs7fdqvOZair7u12ZA8997AO4bhujzh2y+8tlv5Oy0KADzOb4M6VTkMPfkjILw3RZQ8mYGiu4JgEjuYDww8FPkTPBoj9TwWUgm8B5qIvBXzAr2zCD69LIk9PLGDxjzMvbE6XVClvEQ4g7wJxhm97ysHvRnOvbuH9De8nAF2u/YbK7xLlQS7OJIkvFM6EbsVoW48yuZdumJ2gjx3RrA71oWlPCgyAD30yBa88/SRvECH3LlQpC+9OZEPvT6JIb1FvZg8V8o4vGWPmrwBOO48Sqh5PHI5uDzhCsO8oGtGvAXkhTuy1Fy8ulI7vKdBEbxrExA9n0EFvXibg7ypY0m8EA+Duyx/mjuYjS08HJHNPDbg0jvvpMQ8JQEMu8zIuTumSm87mKv7O8D8sTxLTb87z0kMu1ag9jv9xOk7j/QGvCcdIjwQTqK7ON4mu/95mLxtukq8NJy5vCaZ17s5X+o8fREFPK9jfjxOxAo860R5OoO48LyBKok7q0WNvIrKnzzPMKo8/T0evUJpgbpUuJo887QhPVCNcjx7xHU82dwMPD/+KbvLuwU88VsVPGb1IbyXkHS8Ib4XvUkNmzqwjqy8TvdLuWg5hjwD24W8y+2UvEHrPzu47P88KNIPPSLcbLzwNs67NE0YvOiQ3TtF3o67uYIYvJpMHT23pza9v5nPuld1R7vjZ+u7aQkJPCSAe7xAnIc88llpvDP8zDuPDL68XmXVvDmCSLwWWiO9+qP3O8XaSzvWrYw8mj/0O5gSczzWypa7v2azO7qWG71igAI8aOuoO3jstjeV5Qa9UtbvPBZGK7lxq6+7sqcjPYhl+bxOFTm7PdFrPMPhw7x3VJI8dRFlu+krkzsPTbg6xojtvAPcHDzdwrc75uTPO7OI0TuIoIY8vSizvEHCx7xrlK08mNvgPL54tbtc6gk9w0AWvQQm6roRU2G8DluDPFNjiLuFaSK8aQQhPCSRPrwQuOW8l3uovJnu7LuN0R47QAiIvLZ8Gz2RYDG8Hb8vvfU4TTwMLhG9YvIvvMQRojxRCB88+vMzvJEXTbw+Rui8OPrKO8E61Tn6i788x9QPvJ+JCb3exvG86wj5PCF+RjyccUK92aF3vCzu/bsnfpQ8Vt2HvMDnkDwL9sa8+oOYPC4J+LxiJyC9njElPXZmbTxbFtQ7ImEAPWXm2bwUTwe6Uby6O/x67jvUlsA8baffO3aKIDxbz4o8/HR8POhSKTxlABU8hJV1PMwyVLzv/rG8Y9UFvZsW4jwgFhu8BZM9PK23LrzGQwg8RMJpPJt+Sjsp2tW7nGEeO4WeF71LS+E7hq9Au2qLxjxg1x07XpspvLRKuDsywiu9sAURvMtOADsqESq8CQR/vI5Hq7wFrFA7v2KzPFcnlLxPkqU8LfCfvMHIPrxDpMm6TfSvvP6bdzsnpP285FS3POriirxb3Wo8CHTsO9fH3LuwdIY82v3/POHJmLtNTsm7I7xeOxFBSTrDvfe7LMpVvF6tarsN7bu8crHMu1liEbzI/O+7SwEvO4iLbbyCngc85h9XPPhgyDqC52c8MQk9unYurDzVcAI8xX+yvMDENDvP+XQ8IuR7u1dc0zp81FG7cgAXu818UDybR5i8C0ACPMVF/biNMMA7CYZbPHfIcjvK28C6jsQ9vAC37bxh0J087o/LOkYjlbwEvyk98uEUuoKS7jz3FPa8aJogPLDPDLss+Ke7qu+GPJRahDtKE2M6uAP3Ox+Dfrs4YYW7h2gVPDnHBzxgst48PREGOgJCVLsoWwc84wGivHIcPrw71N46JVQIvA== - 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' + - '9561' content-type: - application/json host: @@ -1127,15 +842,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -1155,6 +868,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -1168,7 +901,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -1176,17 +909,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -1202,44 +933,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -1249,70 +973,49 @@ interactions: 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. + 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? + reasoning: Need to inspect documents. 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"}' + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' name: execute_code - id: call_2ao52bvz + id: call_c10kdj1q 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}' + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_2ao52bvz + tool_call_id: call_c10kdj1q - content: null - reasoning: The environment may not provide dir(); use globals(). Check. + reasoning: No preloaded? Let's list documents. role: assistant tool_calls: - function: - arguments: '{"code":"print(globals().keys())\n"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' name: execute_code - id: call_rjj1hq9z + id: call_2jgdkdf1 type: function - - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + - content: '{"code":"docs = await list_documents(limit=20)\nprint(docs)","stdout":"[{''id'': ''c59247d8-8db5-45d9-9cf4-12841241dbc7'', + ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:48.188978''}, {''id'': ''b1843b4d-5e41-4c91-86e0-7eb6040ac5c5'', + ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:49.871504''}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_rjj1hq9z + tool_call_id: call_2jgdkdf1 - content: null - reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could - search. + reasoning: Let's get documents. 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_o1c87rz5 + id: call_0mpn1a7r 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}' + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_o1c87rz5 + tool_call_id: call_0mpn1a7r model: gpt-oss reasoning_effort: low stream: false @@ -1363,7 +1066,7 @@ interactions: response: headers: content-length: - - '509' + - '703' content-type: - application/json parsed_body: @@ -1372,325 +1075,25 @@ interactions: index: 0 message: content: '' - reasoning: Got info. Need citation get_chunk. + reasoning: json module not allowed? It says only import json allowed. Actually imports allowed only for json. But + not imported. Let's import. 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_1o9o5lvl + id: call_x6xxq7h0 index: 0 type: function - created: 1771924598 - id: chatcmpl-401 + created: 1772549575 + id: chatcmpl-348 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 71 - prompt_tokens: 2356 - total_tokens: 2427 + completion_tokens: 87 + prompt_tokens: 2177 + total_tokens: 2264 status: code: 200 message: OK @@ -1703,7 +1106,7 @@ interactions: connection: - keep-alive content-length: - - '11056' + - '10301' content-type: - application/json host: @@ -1714,15 +1117,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -1742,6 +1143,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -1755,7 +1176,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -1763,17 +1184,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -1789,44 +1208,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -1836,98 +1248,63 @@ interactions: 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. + 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? + reasoning: Need to inspect documents. 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"}' + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' name: execute_code - id: call_2ao52bvz + id: call_c10kdj1q 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}' + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_2ao52bvz + tool_call_id: call_c10kdj1q - content: null - reasoning: The environment may not provide dir(); use globals(). Check. + reasoning: No preloaded? Let's list documents. role: assistant tool_calls: - function: - arguments: '{"code":"print(globals().keys())\n"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' name: execute_code - id: call_rjj1hq9z + id: call_2jgdkdf1 type: function - - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + - content: '{"code":"docs = await list_documents(limit=20)\nprint(docs)","stdout":"[{''id'': ''c59247d8-8db5-45d9-9cf4-12841241dbc7'', + ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:48.188978''}, {''id'': ''b1843b4d-5e41-4c91-86e0-7eb6040ac5c5'', + ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:49.871504''}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_rjj1hq9z + tool_call_id: call_2jgdkdf1 - content: null - reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could - search. + reasoning: Let's get documents. 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_o1c87rz5 + id: call_0mpn1a7r 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}' + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_o1c87rz5 + tool_call_id: call_0mpn1a7r - content: null - reasoning: Got info. Need citation get_chunk. + reasoning: json module not allowed? It says only import json allowed. Actually imports allowed only for json. But + not imported. Let's import. 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_1o9o5lvl + id: call_x6xxq7h0 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}' + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_1o9o5lvl + tool_call_id: call_x6xxq7h0 model: gpt-oss reasoning_effort: low stream: false @@ -1978,7 +1355,7 @@ interactions: response: headers: content-length: - - '550' + - '707' content-type: - application/json parsed_body: @@ -1987,24 +1364,25 @@ interactions: index: 0 message: content: '' - reasoning: Similarly mission. + reasoning: Seems import json not allowed? But spec says only import json allowed. Maybe environment disallow imports. + I'll just not import. No need. Use print. role: assistant tool_calls: - function: - arguments: '{"code":"chunk2 = await get_chunk(''d43b4445-8425-4434-bc36-e28e6f7e573e'')\nprint(chunk2[''content''])\n"}' + arguments: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_dn2a823n + id: call_a12792ao index: 0 type: function - created: 1771924600 - id: chatcmpl-408 + created: 1772549578 + id: chatcmpl-777 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 67 - prompt_tokens: 2502 - total_tokens: 2569 + completion_tokens: 89 + prompt_tokens: 2337 + total_tokens: 2426 status: code: 200 message: OK @@ -2017,7 +1395,7 @@ interactions: connection: - keep-alive content-length: - - '11618' + - '11025' content-type: - application/json host: @@ -2028,15 +1406,13 @@ interactions: - 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. + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. - CRITICAL: Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + 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 + - 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] @@ -2056,6 +1432,26 @@ interactions: Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels Use this to retrieve full chunk details and metadata for citation. + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + ### 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 @@ -2069,7 +1465,7 @@ interactions: for doc in documents: print(doc['title'], len(doc['content'])) ``` - Check if it exists with: `if 'documents' in dir(): ...` + Check if it exists with: `try: documents ... except NameError: ...` ## Available Python Features @@ -2077,17 +1473,15 @@ interactions: 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. + For pattern matching or text extraction, use the `regex_*` functions, 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. + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. - 4. **Use 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. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. ## Example Patterns @@ -2103,44 +1497,37 @@ interactions: print(f"Total: {count}") ``` - ### Extracting data with llm() + ### Extracting data with regex ```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)) + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) if numbers: print(f"Average: {sum(numbers) / len(numbers)}") ``` - ### Using search results with get_chunk for citations + ### Extracting tables from a document ```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]}") + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") ``` - ### 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: + 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"} ``` @@ -2150,111 +1537,77 @@ interactions: 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. + 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? + reasoning: Need to inspect documents. 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"}' + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' name: execute_code - id: call_2ao52bvz + id: call_c10kdj1q 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}' + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' role: tool - tool_call_id: call_2ao52bvz + tool_call_id: call_c10kdj1q - content: null - reasoning: The environment may not provide dir(); use globals(). Check. + reasoning: No preloaded? Let's list documents. role: assistant tool_calls: - function: - arguments: '{"code":"print(globals().keys())\n"}' + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' name: execute_code - id: call_rjj1hq9z + id: call_2jgdkdf1 type: function - - content: '{"code":"print(globals().keys())\n","stdout":"","stderr":"NameError: name ''globals'' is not defined","success":false}' + - content: '{"code":"docs = await list_documents(limit=20)\nprint(docs)","stdout":"[{''id'': ''c59247d8-8db5-45d9-9cf4-12841241dbc7'', + ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:48.188978''}, {''id'': ''b1843b4d-5e41-4c91-86e0-7eb6040ac5c5'', + ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:49.871504''}]\n","stderr":"","success":true}' role: tool - tool_call_id: call_rjj1hq9z + tool_call_id: call_2jgdkdf1 - content: null - reasoning: Probably no python environment? Might need simple print? Might fail. Probably cannot see documents. Could - search. + reasoning: Let's get documents. 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_o1c87rz5 + id: call_0mpn1a7r 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}' + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_o1c87rz5 + tool_call_id: call_0mpn1a7r - content: null - reasoning: Got info. Need citation get_chunk. + reasoning: json module not allowed? It says only import json allowed. Actually imports allowed only for json. But + not imported. Let's import. 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"}' + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_1o9o5lvl + id: call_x6xxq7h0 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}' + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' role: tool - tool_call_id: call_1o9o5lvl + tool_call_id: call_x6xxq7h0 - content: null - reasoning: Similarly mission. + reasoning: Seems import json not allowed? But spec says only import json allowed. Maybe environment disallow imports. + I'll just not import. No need. Use print. role: assistant tool_calls: - function: - arguments: '{"code":"chunk2 = await get_chunk(''d43b4445-8425-4434-bc36-e28e6f7e573e'')\nprint(chunk2[''content''])\n"}' + arguments: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' name: execute_code - id: call_dn2a823n + id: call_a12792ao 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}' + - content: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"NameError: + name ''docs'' is not defined","success":false}' role: tool - tool_call_id: call_dn2a823n + tool_call_id: call_a12792ao model: gpt-oss reasoning_effort: low stream: false @@ -2305,7 +1658,323 @@ interactions: response: headers: content-length: - - '1388' + - '672' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: We lost docs variable due to separate execution. Need to run in same call. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_qpt2msnm + index: 0 + type: function + created: 1772549580 + id: chatcmpl-40 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 84 + prompt_tokens: 2495 + total_tokens: 2579 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '11872' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: 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. + role: assistant + tool_calls: + - function: + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' + name: execute_code + id: call_c10kdj1q + type: function + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_c10kdj1q + - content: null + reasoning: No preloaded? Let's list documents. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' + name: execute_code + id: call_2jgdkdf1 + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nprint(docs)","stdout":"[{''id'': ''c59247d8-8db5-45d9-9cf4-12841241dbc7'', + ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:48.188978''}, {''id'': ''b1843b4d-5e41-4c91-86e0-7eb6040ac5c5'', + ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:49.871504''}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_2jgdkdf1 + - content: null + reasoning: Let's get documents. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_0mpn1a7r + type: function + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_0mpn1a7r + - content: null + reasoning: json module not allowed? It says only import json allowed. Actually imports allowed only for json. But + not imported. Let's import. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_x6xxq7h0 + type: function + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_x6xxq7h0 + - content: null + reasoning: Seems import json not allowed? But spec says only import json allowed. Maybe environment disallow imports. + I'll just not import. No need. Use print. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_a12792ao + type: function + - content: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"NameError: + name ''docs'' is not defined","success":false}' + role: tool + tool_call_id: call_a12792ao + - content: null + reasoning: We lost docs variable due to separate execution. Need to run in same call. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_qpt2msnm + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"--- + Company History\nThe company was founded in 1985 by Jane Smith.\n--- Mission Statement\nOur mission is to make technology + accessible to everyone.\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qpt2msnm + 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: + - '867' content-type: - application/json parsed_body: @@ -2313,26 +1982,353 @@ interactions: - finish_reason: stop index: 0 message: - 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())"}' + content: '{"answer":"The company was founded in 1985. Its mission is to make technology accessible to everyone.","program":"docs + = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n if content:\n if + \"founded\" in content.lower():\n print(''Founded year:'', content.split(\"founded in\")[1].split(\"\\n\")[0].strip())\n if + \"mission\" in content.lower():\n print(''Mission:'', content.split(\"Mission is to\")[1].split(\"\\n\")[0].strip())"}' + reasoning: Found info. role: assistant - created: 1771924607 - id: chatcmpl-899 + created: 1772549583 + id: chatcmpl-237 model: gpt-oss object: chat.completion system_fingerprint: fp_ollama usage: - completion_tokens: 296 - prompt_tokens: 2645 - total_tokens: 2941 + completion_tokens: 150 + prompt_tokens: 2680 + total_tokens: 2830 + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '12614' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + messages: + - content: |- + You are a Recursive Language Model (RLM) agent that solves complex research questions by writing and executing Python code. + + You MUST use the `execute_code` tool to run Python code. The functions described below are ONLY available inside execute_code. Always execute code to answer questions; do not just describe what code would do. + + Inside execute_code, these functions are ALREADY available in the namespace. Do NOT import them - just call them with `await`: + - results = await search("query") ✓ CORRECT + - import search ✗ WRONG - will fail + - results = search("query") ✗ WRONG - must use await + + ## Available Functions + + ### await search(query, limit=10) -> list[dict] + Search the knowledge base using hybrid search (vector + full-text). + Returns list of dicts with keys: chunk_id, content, document_id, document_title, document_uri, score, page_numbers, headings + + ### await list_documents(limit=10, offset=0) -> list[dict] + List available documents in the knowledge base. + Returns list of dicts with keys: id, title, uri, created_at + + ### await get_document(id_or_title) -> str | None + Get the full text content of a document by ID, title, or URI. + Returns the document content as a string, or None if not found. + + ### await get_chunk(chunk_id) -> dict | None + Get a specific chunk by its ID (from search results). + Returns dict with keys: chunk_id, content, document_id, document_title, headings, page_numbers, labels + Use this to retrieve full chunk details and metadata for citation. + + ### await get_docling_document(document_id) -> dict | None + Get the full document structure as a dict (DoclingDocument format). + Use `list_documents()` or search results to get document IDs first. + - `texts`: list of text items, each with `text`, `label` (e.g. "title", "text", "section_header", "list_item"), and `prov` (provenance with page/bounding box) + - `tables`: list of tables, each with `data` containing `grid` (list of rows, each row a list of cells with `text`), `num_rows`, `num_cols` + - `pictures`: list of figures/images with metadata + - `pages`: page dimensions and metadata + + ### await regex_findall(pattern, text) -> list[str] + Find all non-overlapping matches of a regular expression pattern in text. + + ### await regex_sub(pattern, repl, text) -> str + Replace all occurrences of a regular expression pattern with a replacement string. + + ### await regex_search(pattern, text) -> dict | None + Search for the first match of a pattern. Returns a dict with keys: group, groups, start, end — or None if no match. + + ### await regex_split(pattern, text) -> list[str] + Split text by a regular expression pattern. + + ### await llm(prompt) -> str + Call an LLM directly with the given prompt. Returns the response as a string. + Use this for classification, summarization, extraction, or any task where you + already have the content and just need LLM reasoning. + + ## Pre-loaded Documents Variable + + If documents were pre-loaded for this session, a `documents` variable is available: + ```python + # documents is a list of dicts with keys: id, title, uri, content + for doc in documents: + print(doc['title'], len(doc['content'])) + ``` + Check if it exists with: `try: documents ... except NameError: ...` + + ## Available Python Features + + The interpreter supports: variables, arithmetic, strings, f-strings, lists, dicts, tuples, sets, loops, conditionals, comprehensions, functions, async/await, `map()`, `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 the `regex_*` functions, string methods (`str.split`, `str.find`, `str.startswith`, `in` operator), or the `llm()` function. + + ## Strategy Guide + + 1. **Explore First**: Start by listing documents or searching to understand what's available. Document `title` is often None — use `uri` or `id` to identify documents instead. + 2. **If get_document returns None**: Use `await list_documents()` to see available documents (check `uri` and `id`), or `await search()` to find relevant content. + 3. **Iterative Refinement**: Run code, examine results, adjust your approach based on what you find. + 4. **Use llm() for Classification/Extraction**: When you need to classify, summarize, or extract structured data from content you already have, use `await llm()`. + 5. **Cite Your Sources**: Use get_chunk() to retrieve chunk metadata for citations. Track which documents/chunks informed your answer. + + ## Example Patterns + + ### Counting documents matching a condition + ```python + docs = await list_documents(limit=100) + count = 0 + for doc in docs: + content = await get_document(doc['id']) + if content and 'keyword' in content.lower(): + count += 1 + print(f"Found in: {doc['title']}") + print(f"Total: {count}") + ``` + + ### Extracting data with regex + ```python + numbers = [] + results = await search("financial data", limit=20) + for r in results: + amounts = await regex_findall(r'\$([\d,]+)', r['content']) + for a in amounts: + numbers.append(int(a.replace(',', ''))) + if numbers: + print(f"Average: {sum(numbers) / len(numbers)}") + ``` + + ### Extracting tables from a document + ```python + docs = await list_documents(limit=10) + for d in docs: + doc = await get_docling_document(d['id']) + if doc: + tables = doc.get('tables', []) + if tables: + print(f"{d['title']}: {len(tables)} table(s)") + for i, table in enumerate(tables): + grid = table.get('data', {}).get('grid', []) + for row in grid: + cells = [cell.get('text', '') for cell in row] + print(f" Table {i}: {cells}") + ``` + + ## Output Format + + Your final response MUST be valid JSON matching this exact schema: + ```json + {"answer": "Your complete answer here as a string", "program": "Your final consolidated program here as a string"} + ``` + + - `answer`: A clear answer to the user's question with key findings and references to specific documents/chunks. + - `program`: A single, self-contained Python program that produces the answer. Consolidate your exploratory code executions into one clean script. + + Do NOT return arbitrary JSON structures. Always use the exact format: {"answer": "...", "program": "..."} + + You MUST call execute_code at least once before providing your answer. Never give up without trying to execute code first. + role: system + - content: 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. + role: assistant + tool_calls: + - function: + arguments: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')"}' + name: execute_code + id: call_c10kdj1q + type: function + - content: '{"code":"try:\n documents\nexcept NameError:\n print(''no documents'')","stdout":"","stderr":"","success":true}' + role: tool + tool_call_id: call_c10kdj1q + - content: null + reasoning: No preloaded? Let's list documents. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nprint(docs)"}' + name: execute_code + id: call_2jgdkdf1 + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nprint(docs)","stdout":"[{''id'': ''c59247d8-8db5-45d9-9cf4-12841241dbc7'', + ''title'': ''Company History'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:48.188978''}, {''id'': ''b1843b4d-5e41-4c91-86e0-7eb6040ac5c5'', + ''title'': ''Mission Statement'', ''uri'': None, ''created_at'': ''2026-03-03 16:52:49.871504''}]\n","stderr":"","success":true}' + role: tool + tool_call_id: call_2jgdkdf1 + - content: null + reasoning: Let's get documents. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_0mpn1a7r + type: function + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_0mpn1a7r + - content: null + reasoning: json module not allowed? It says only import json allowed. Actually imports allowed only for json. But + not imported. Let's import. + role: assistant + tool_calls: + - function: + arguments: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_x6xxq7h0 + type: function + - content: '{"code":"import json\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"ModuleNotFoundError: + No module named ''json''","success":false}' + role: tool + tool_call_id: call_x6xxq7h0 + - content: null + reasoning: Seems import json not allowed? But spec says only import json allowed. Maybe environment disallow imports. + I'll just not import. No need. Use print. + role: assistant + tool_calls: + - function: + arguments: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_a12792ao + type: function + - content: '{"code":"for doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"","stderr":"NameError: + name ''docs'' is not defined","success":false}' + role: tool + tool_call_id: call_a12792ao + - content: null + reasoning: We lost docs variable due to separate execution. Need to run in same call. + role: assistant + tool_calls: + - function: + arguments: '{"code":"docs = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])"}' + name: execute_code + id: call_qpt2msnm + type: function + - content: '{"code":"docs = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n print(''---'',doc[''title''])\n print(content[:200])","stdout":"--- + Company History\nThe company was founded in 1985 by Jane Smith.\n--- Mission Statement\nOur mission is to make technology + accessible to everyone.\n","stderr":"","success":true}' + role: tool + tool_call_id: call_qpt2msnm + - content: '{"answer":"The company was founded in 1985. Its mission is to make technology accessible to everyone.","program":"docs + = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n if content:\n if + \"founded\" in content.lower():\n print(''Founded year:'', content.split(\"founded in\")[1].split(\"\\n\")[0].strip())\n if + \"mission\" in content.lower():\n print(''Mission:'', content.split(\"Mission is to\")[1].split(\"\\n\")[0].strip())"}' + reasoning: Found info. + role: assistant + - content: |- + Validation feedback: + Please include your response in a tool call. + + Fix the errors and try again. + role: user + 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: + - '1108' + content-type: + - application/json + parsed_body: + choices: + - finish_reason: tool_calls + index: 0 + message: + content: '' + reasoning: Need to wrap answer in final_result tool call. + role: assistant + tool_calls: + - function: + arguments: '{"answer":"The company was founded in 1985. Its mission is to make technology accessible to everyone.","program":"docs + = await list_documents(limit=20)\nfor doc in docs:\n content = await get_document(doc[''id''])\n if + content:\n if \"founded\" in content.lower():\n founded_year = content.split(\"founded in\")[1].split(\"\\n\")[0].strip()\n print(''Founded + year:'', founded_year)\n if \"mission\" in content.lower():\n mission_text = content.split(\"mission + is to\")[1].split(\"\\n\")[0].strip()\n print(''Mission:'', mission_text)"}' + name: final_result + id: call_b25e3tdq + index: 0 + type: function + created: 1772549589 + id: chatcmpl-799 + model: gpt-oss + object: chat.completion + system_fingerprint: fp_ollama + usage: + completion_tokens: 178 + prompt_tokens: 2717 + total_tokens: 2895 status: code: 200 message: OK