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: HEPuuKbNCTwuZW67Cua2PCM64LnpilM9BWpVPUYq6zwjklk8fLraPIWV1Lvw0jC7KRL5OZYbWbxzx8K6ZOWvvIP6NrwNARm6kAOUPBbp5LuNSXO7XkECPR7hyDxPh1S9Na4EvfWoKb2xHZ28dZWDvQLs4Lxbne27Zq2rvCKAmLx/NwM9hsvkO2qn8TlzgQo8EvqdOwUcObyAl4m8GrlzPCo+/jwouIu8aOgoPNMJRDsePnw7af4IPcbffjyVchK8kkWdvLcxTrhKeC07p8iJOyWmSL0oGPW8uUSGPGed4LvrHuA8J6qYuypKPL2stLG73EUPu3rrkzvLIs+8hQyAum3VwbtfjNi8e5udvNS5dL16Y+k7R5fxPD0bEbrOJJg71YlXuwY4szuVMjk8axyXvGjjbbySPQA9r8UdPG6ZvjvhrRg9Qz2ZO7RJJLvZmCy9eXqBO+v8QbvJSIg67Qo3O0PuxLz7nQ+8IJnqO7khBj2kXC08KuIyPPn2DTxeBIa7L15zuofjgrzZjZs7ZRITukMiZrybFE87FnDHvGXvFryEGLq8Dyz1vPGedruhfRU8p0RAPIvdTDso4uq7zh2ZvGz3pDtwgw08z9mGu4etPjtKGyK72xyuPGE7urpfYvm82WDwuz8VuDwwhfW5cZHWO0wXJTwsllc83bm0O2R5rzvsf0i8GRcTPYU4kjuwERy85pAlPP9jWboV9NK8ya60O3laA7zXYGI7J5ZivF2R5TxgyI+7hI2IPKxa9juxS4e8WNCevGJegrxPeUQ8BsrxuxGHDTw1uU+78gqRPGndAbyLuEY7RjAwO85q/DmDZFk8A0s3PMjs+jyDM5i7Dd8LPT0AZbuSNi08BC7WOy1l1zydtAM4T5tLPLGMSbwT9888e+eRvGtu/ryqJAw8rZnhu3Shlrzt2p67+pxRvIx1nDzLiKi8ZlsvO8KS+rugGMc7XqpNuz6qbTuvYd88oyZEO87j/zuLZsM8KYvQu+rPFLm1jGE65dOXO3eiFr3t5Eg86zJFPAcg6juAdi68W9N+u3r93bxbey8853fQOyL8YjwfZGw8hqHMuhFXG7wlyLK7kEqnOsKdbTxM4Au8Ku+fO51/RDynRA07kYe5PFRjEbx+Onu8qBG2vJ4sHTyF22M8BEoKvMOggLyLA5Q8jMRgPSYuojwFveO7wnLDuizyRzv9iA29Xw8BPEgohbxnyY+7Bc34O7u2LDykVwE9QBOnPNamfrzGo2y7kIJ6O5lSlDyuxey7fNXpu/xXjrpmDYs7gj/gPHh2mryLjzS7UvI5POWc5zrzVX67HeZqu/jNOrsUBay87kW+OhiBXbuSZLc8ZFGGPMELaLxr3uy77WMGPRbFNLucPSe8+goVO38mxboAOr060p6lO2NQ3bwBmw275ztFvM2ikLz+k+I7FfATvB2GpDsokAA7DiO6u0WhC7xYUPW7d4IsOw4rCbyrXpA6b/jKO+0Xqjxz3OA7O9XgPEIrxbxZWf87kC2KvFhGzLzEbZ28OhvEPItEkzwid0U8dLDTvCf+Nbqehje7qIztvDXDVDvYE4C7wMcNu+5cJTyGugm7TwMfOuS3Nrr9hUS838E4PJ9SKzyeWJ2707KwvKHQtjp6vL68m+TbuwCLIjz4bYw7GoVSvSyNurzYYnq8FIydu880ILjnrcA7Hcq4vFKIO7ywwCc7fd7TvCJ14byoheU7hpxGvYLpKrygIxy80L5lPMN3ebtHaGE82OV7Oz6eCDw6n+m7yYhWvLYG9TyGiPq827gyOuiAD7wajw68I93gu9OrGzzyNo48dfU+PDj92Tp9U/W7T8quvNC3DL3VrSi9kEoEvcbsPLzniJ88WGP3vKzHbrxXqmO8kjANvXx5nzxJxPG8pwEkvIc0pjyATve6ic20PFCQJTyiKq68r/R+vKylcTxDeRO7/f4Jvc8JQ7zPnLy7Z+MVvBdZqTyBpsS8PWb+OdbAEzw5z/+7BrBePLPuCb1hmgE8i915O/Sbh7vp1H68xovjO9iRgzrqQQA9pI7+PESvNbzDfoU8jC9AvALEabnXRHm8vrHQu4DXv7vINA49/nxPvE0aS7xG1HY83s3IO23n1LsXRgU99eYXO0IKhLgvXqi5zMf4vH2YlbynnEC8VHoUvbFzRDkBTqO8xdsNvfnyb7xj2/o86+W1u9dsNTs3PSu8jg1/PMkSiztPoQe8COEFvZC6MzyedBg8ynBxvNR/+bvgb9E8ThT9O8KmKLy0Mwa86ZUQvEwJ/7sSviY8Nm93vAPf8Dsn7Bi983O4vArAGbuGdQc7UR+COr/OxLshLhA7wbkZPZmrCzvUshK7sWGeuwNJDL341tk64z8ivNcBBz3zbD26bycWvfb3CLyDN6U7YCcWvJROJT1v7EG8ISTru/4fhbwPvaq7pZiqOzaJ0LwgFEA7jle7POe1eromjHm7B2V4u+k8hL1hAAo9A2mXPORKqbzayfe7BwS6vE6OFL34p4I82JIKPKOOajxwffa87PQTO6ccczxE3iO7sG9xOIQk4jyYHrC6TvmRvO4gQDxYoqM82jAfPRQe6jxd7gU9PBckPVH83TzxYrs7+h9xO5v7qzwyRxk9zmBVuyAizTxOPr48i+7NvCZE47yhiq88zoRVusTDjzsLA9u8APJaPPU4vjzi0Lc8M49yvJ+0eLzUOJm88oeRPBnkrjyXe+G7+adavGFDy7xwIjE8PbqcPGQnhzwzWxG81YgdvXMESDyeNUg8le2svLqlJTm1iIs8kofHvOYT+jl4Xty7bRf7O4n44bzoEm+8ga80Pd1PUbrQo9W4Lue5O0v4AzzrkVo86P3ru6uZbLtVk9k8kWxiva7mELzmRAc8em0DPQOoobzHoyK7/TTQO7SJHTymBMw7vzXjvIylOj0klc08sv55OtS08bxNZV69K1fmPI9erzxEsoq8aGDNO3PAz7yHzT+7chRlPdn2y7smdXM8IsNpPLVei7xhwBs9WTFfvMEi/ztOoiQ96tNsulY5gLzERBo9Q2ncuwKxKzxYAWS8ZJX+uw56SDzHEPW7Q/SZPPTyPDyWKQu95tLQO5SHTbzxNwk89rfAvLGKvbvI/V68P+ZcPDcarrsJVQY7V9iEu+4ae7wc2r48ue9qPIxTPTuivgm9S/icuHML2Tw6Ewc6TNIGPch9orw8Lx887FgIO7M52rzYFoG8wY3xPKfn2rxPWta8kccfvAgXSb1knMw715orPMW62Dy0iEQ8I4UePAUm0TuEAlk7hBoRvA/muTwEqfg7BTDIObVToLylKsy8Mxb7u91eOLzhKwW8RDOXvM6T7rxjgLw8482hu4rQSTpxkxy8IJRwPB5u/Lg1K8g6MTIVvImBALxo+hY9ZYCMOyUetjxX1Qm6DysEvSN6F7slBnQ8SYcMvGF++zsOHNQ8nhXJPFmAbjzO5ca7gz7zvEWVt7zxcNA7y2gMvY9D3TzK5yY6eKHJO7hxjbt8v6G8yC6gOmkXLr0hSRA85ny1vLCfCj1JGBC8QP+fvF9io7wwC5c745EgPcCKXb3IljU7WCAWOpPQGb1oFhM95oNkvH9QgDv4AHi8fh9PvdD4gTuIat08pj8vvHwNjjwa4Xi8Mvj3O6ODkbs0THU8wH11OlOinLrfbJS53KEGva3I7Tqym8s742ewPD4HnTwBRu08w0FlvNMNAb3XA1E9yE8jPVssVztZIe07/1eSuzmklzyQqry8VqYVvdMWZ7wtM1s8ETx5PLNSHTy+jso70p4SPBI6GT0BR8c8syjPPHLo+TvAUYG8oilMvZo6TDt2cGe8X+IHvKXVdLy2jD296sfPO3ftkbyNR5g6d7NGO+qIvbnSE3W89ogiOE6CiztUcKE8X5ctPP5AtbyQ6Fm7GfsbPSViKzy5ZvC772U1PJkBEjr5hJg7bDNIPLP4wbzpLLs8JxVcOXVfdbxBSig8dDHQu4+QkDzZetg5JDOQvPeR+DzS9+q8imJDvNFbxLwojlO8lZgSvbX1Lzxk/JC6MuWcvGxocbwevIQ86jacu51d5Lu+B4A8iNmtPFcTmTsMr9S6/bwHvRByHTzdnhC9N0oNPV3hDj1NRyC8eL9jvEot+rsmDAi8RWydPHAdhryLIPC7VrbCu1K0ITyAjfG8SqAkPHHXLTytKuM7tCniu6KFRrxusj230w5cPIkFMDyiC4o8oXTevPWCGLytUee7IYDVvHihu7ovOIo88VtuvPYJeTy4z2k9UC0EvDyMsrwZtcy84/ItvZDthrxGIvQ77tcOO37zlDuEwH09gRAvPEPptDxV2zU89oSyu8Rvkbtptpa7ROeLPGlfUL2dNbW8cadxvC0snryDl+y8yslsPGjZA7wpvnA8hsMmuwMxCTwbgAc8b4vjO6H2Z7xxPRg9he6TvLo72bpVkKK8sFUWPH56aTxNH5u7dXB1vJP8E72MsMk7aov5u4tD0LzJroY8iMU8vacUOb1mO628FS6hO3OBIDz5BjY9/Zaqu7hbcLx9bxi7e1eaPNH84Ls2f6y83z+8PAk2MD0XonI9nCpVOiAWXT0R5I48YabEu1YKSTxcXNk8XmNBPEKqhTpRsou7g+jpu/4D37tZBmm8vDGROgesVjybFQA8bFZrvPrvOzxr6Cq9/psmPRtLUzz1pxM9Gq8EumvofTtdwlO6rW59vAUhQ7xS0IY8lcuiO+NqaTxCqPK8c9ERPV5LDTwLHmG8c4dBPZPhGTuMjZQ8wKkMukcjUrw03A27xjsOPRwzBbw+Gz08/OMFvc69xDws6JA7nZUuvXQG1zzq03y8r6+1O8zKyDyIHvq7Eo0LPCwtUD3GBPa7Ak6avDIGPzyQXz+6PIBRO3/v6Tvl/NS8drW6vOiMbzyYz3E7KI7nOsKTTrsIQR68l4IVvaPNNLyd4Ec8UYxZvHtD6rzenFu73GtePYlKpbxhlcC7bSj9ui+nf7w36tM7Q+a8vGsgnbwNjBu8xLMGvG21GbzUsIA5UfUQPanC0zx1O6Q8eQnJOwpNl7xxnEW8vfY0PBfV/TvZ3dm77ey4PLC0Yj2D4+E8SVniusdlvzoKf548NVQTvYC+9jw+LW26AeG9OYFes7zhRDA8Rr7VvNni3TvQd3o8irtRvAUGWjwvk/S7zwVjPK7Vszwn4J28yWeuOiCS8bp88rO8mxSdvN+zizv8BPa7iLq9ujlV5rvxFqc7M/VGOzjhQzw0uqc8tE5mPF2eKj2kbCq9yE2zumbb77uwfxu8cvnVOU/f+rwaiO07LVPzu98KgDw3juW8pUoJu3882zueTYE8D1w3PRs/sjvQrRs9cB2mu2eOvjyqtqs8BetZvAVCbzuS7u47H/lBvJqpBL2CVA68SKievK8GCD19D1k8IfmBPbyan7zlK+m54eK1vKZhyLtiXiG8BbRDPA3qvbvo5Ws8FWO5PCBFBr2r3PU7I1MNvMdtjTw7o5q8qJjbPAA/DjwxJ0u9e4BUPAjnWLwu2608W4etO1jt5bokePq6f2IsvKhmkjwqvnW88oUfPE2VnjtD1tQ8X6eUO9oTybxZzoK8cusPvOX9nrz4ELu74EiFPNH8szzIcaW7sY7pO3ozQrzcbAs9JoEhOqBGrzxacQ68oXKGPHBAzLwjZvw8XBmcOwOebzyopLQ8Ppe1PHw8GDoQdL66D4invExlazzwi+G7bIcIvUUAAbzUG6k8uxlDvcrs5Dowceu8AU3rPC39o7wAB3A8P2heO1Zi1btJwBO8geqvuRtl9LyMQxm9Jw4RvQLsOTvNu3q8328OvE5wc7xAp668kOgxPFWrKTtxzYg8ZSkWPZsnbzzYmzC8EGABvQZyUDvphO480JZSvB5KpbxMnQQ8SGYTvFwEqTtEu2+6dhNtPQ5L7zuvd4c8yX+SvMFQo7ysBzG9ZE38uznHZLybvsA8/jjYOuvHdjxamQ67LYUBPU6CWDx3GoQ8vwWmvNMfprx/9yi9chPQvNLCQT2kDhe8KJwbvJYvyLvt0MG8dhmuvI4DIT3KWy89bkoBPJsQNDz/tow8AYMzPQot6Dsklz67F1Q8PbvlmjzUfoy8m8cQvUeHAzzoOg69pNSVuXmCkLsVQQw9t+ewO2BHv7rR2JU8QzoBvcqXQzyIqdg8rUEqvVxMAb2cVBk8LMumvNRearuG1QC8ajyVOzVeF7wyTto8BmVVvKfpszx9RcO890IWO29qGjwG4iM9HjKBOds1vTyBaxG9ec3aO3u9absR7qm8DfaLO36/pLy1zQ08xsX6umyhervL9Co7O+HbPDdkKjmhh6w8i5GQvC/1gTsebWk88SoNPRsIB72cVUI8J2GDO8UAFz0ZBPS6/+qqO8U2Kzy0vlI82FanvIyeorx1Vfm88q2DPEW3mbzrVQM8qNaTvA6teL1W5hK9j8uzvKc3+byf6pu7nvi9OvjFjbt1ixM9mOTDPK+Mlzyh/hy8SFBoPYz3kzz/9Ao9lTVwPBXQIDzlZEO9GesDPK62UTzfim08tnxWPK3XHbtE9Gq8bF2YPJPtO7sjF4i7nurOvKOdjryY6gq8/x6ovAJfXLtyoYq83DM3PWJF2jxZ7SI9Yk+/PEYAzby4JNY6D1MJuz0vkbxlubI8Tt3dPGsi/jwsUoI8G84BvPh+gru3oUk9BaaWPIgxo7zxytA7MGLeOzT7BTwwiQw69/8DvKyqm7z+ZXQ8O8uCuwanizsqxvA8n89CvNJtprzCRe48UPinvGKiRr1zg8q8aGmIvE3E4DxNRVa8SbCGu9Cscrwo1yq7hPblPOkJxjx3EBM9jF3MvCmyBj0ndx27YpjlOuBY0Lw8l2i74roFvFkaA7qiYlM8MJyEOahSVrzh31C7flEDPG8jyLyNJAy9zdR8PemCMDy2aZG8/SziPL1Y8jzT18e7yO8UvAzBFT2j5JQ7ARvBPAPwwbyesyk9tF8GPCkBM7uJHDM8W5VkOzUh/7yih2o89qVHvR0ZnDyzkGg8DenyuymwUzwVduM8MTkJvVeUMrzyV148Tf+XO89fwDpR/0u8/vTPvLA3EDsvI2e88iS6uhvG57xij788RHRSPFQu7juK99Q8AA10Ojr4n7uxr9q8nxHcPDt2BDz2wpQ8R7cAvLPa6zxCuE+5EpuzPMuP0Ty7IQM9/txjPIALF70i5dM7PUZbu3fTLr0AU3K8D58kPDWQErwaxLC8cMr+O+YqVbymRGa8lyyAuwODOLxSdHI8mQ4WPVsp4LthHnq8bsPRPFw+5DrtGeI8tDZRPCib9jxGKgK9WdL/PBGvYTw9l4G8iRoLPCtKbzzzypq8tjy7uiwF2ju9nrY8s8wGPcD+RbxBIH+7V3rZPGOOVrxmwAC7Fq6DPFTtD7xhPYE8nw7UPCjkYbvOHbA7luOzvGundD2A34g7jPYKPL3lBz13yg08HlsYPVC6hbyzaH470kBlu7t35Dzipsc8rJI9u+udi7rLxi+8Bw2UuyswK7v27KQ8M1WfOg4bp7xIVG089IQ6vExVwbr3KWI7PQg6vNRRYrw5TbC6N66HvDecPz2H63a8GTnWvGh6gTzUqB+9JQLEPG1csrx9v4c7pNXZuqZXxDx/eru8zQXmO6TBwLzfNDS8Kkx+vHBvtDyWmLu8MDR3O7ZO+zutQoe7QJiSvNi/i7wAYtq8c+nWuTyFwTt6mz48iTT2vEUuezxF05Y8KUoXu1c1/zvkKf484nlDO+ncQrvUC048LOBqvEWOFr1m9Hc8bDKfPIY307vaAeU8bRAjPdi+jbzxf8k7wlTzvJ2ozTyzZSs8o6bfPGYaxTuUhAa8SorXutk0CryOGiA8RHY+PSiijzy1Rfq8H2wmu/m9fLs3vuq8rcwVvbIgLDzXWJm84Nj+OpdmiLxtz5M8Bp0uOkWVGL1eOek8PKuxuy+KmLxSJq+7MJZHvID/yzvM8427QtH+POs25jzI9q+8MiyoPK9gEDxvjKw8kSlNvE0EgjwDN9i5ISGoPOx5pjvVZBy9Ptb1u//vvryTS6K8j6sJPMeEPjtRG+07O+AJvEUvMzzoflg6By1+PHqC5rocja87Y4z5vDCkzjzIk768DEMAOofHLTygmpG8b/IBPFdnwjzum4c8bT7eO5MuVzz6XA08bjPNvFsEezwZeUu9vU5jPPpbDb1rOl481eQjPAzW1Ty5H4084w8FPI/Rq7serB084yZJO2eDKb1VlyE7IRmPvPdCsDwcAAA8vf6HuyY1Tzxs4RS9blyCPDEuDT36o648wDKovHZw27uwd7k7UlEAPD/hZzyVNDe6fluau+qrNDtvzXc8OBu7vOQ3QDwRXfQ8kd2FvMHY5Ltz4oO6E06gPCTk/jz9FkQ8jY3DvPV+H71H/e27ZaeKPEFeET101Mq7pdfbOpFlhbsMn7o8Yd+XPJ2X6jlsk1Y8MPntuj5TW7zmjZs82wcZPDkMdbzZGcO8d6chvQHaeLzsVk+8W2ajPGYdZ7zrSH880kQQPQIKg7ygdeG8JdCiPPyLvrqn3yQ9xj0JO/aHf7tDRM68DN8Hu1qLdLv4nmQ9wKg2vETfBDyksoa86hl8O7CC/zv3qtG7z1Swu1/BgTwxy2o7hpjku1F9I7vocO48EJXtux5UvrvGU1E8pelsvPQGRTzBpaY8ZSjbPG29mrwiFeq7qlFkO8vax7okqPK6Z9BfOqmz/jx5AXK8RmAQPA10Qruj3c080N7nvLtzAz1rW/W8gPq7PNVAIzz+QSq9iganuynGJTypEUw9rYc3PJzXYDkbkgC9WxSdvKy9WTx1vSq9lLz9vKcMJLpYX5g8x+v5uiJ2Fzy19xy9LNLHPBmvYjzwbyW9X74bPLiwdLs1fvI7/s7OPH3vi7uB+qK8muQRvbi117zAxmY81PqePLogWzvF1/07vOigPMfESbuVu4e7zVpyvKYzY7w43Q28XnjlvESU1ztlrto8o0pjvOQaKbwtjnE8zI7dPAJ6bLxy9xy8vnsYu23/3Lwalci8SN7hvPGfFbo3sX68DcdsO6XHlTwgGt06QQsNvGCcLjohe/k8UJHcuxm6rjt7yZi7Apaou8D/gTvvayY8nh+0vKTFajg/qzK9OQALPD+thDs++wi9emF2O9FijjvhfvC8Cfl0vBY50LxYr2C8mqS1O7yB4TxoAlG8LpkDvJojiTz64GI8KFwJPIQgAj2V14M8ebCjPJ2QMTvOle88aHiYPHWvxrs0G4o7Sxj0O4amc7s8OfC7iul0PH/9wDzRHNQ8JP4RPJIgSD0uioG9cSvQuzBHAL3IvcW8aHYvPIbCYLxoKpu8drlHOkHFpzoAp9K8qJ2TPB3XnLsJ3mo8QjgUvGUqdTzrETI8ly0qvEYvfzkg7LI8vqwXPG0uJrxLghK9hFYvPRu3rTz6uFc9xp7zvPs/1rsUW9m8JMtKvIY6Kb0T8cu7i4rAvGgvSryVCUS8hhpaO6LSHbzfzg29T9eLvGA7UD2QUlw8Upu9O8odjzvAsqy8C6TCPAK07jxa1Re7NaCWvNAGLD1TWyk88lhAvUPYX7xEYDw8oBQbPJGESrx4YIe8ezUeO/BaDLycDke86FAOPcCzhztv7PO82RBZPBWXrzwj7Gs8WQO5OwDcnbpDdZ07V/ZmPBv0Mr1s0Kw8KywKO6VM7bzfyV08UV07OwEplTuCiAk9dN/aOeK/JT3tPw09dGiKvFkvErojg5m7XHQyvC9maLtRpIw8Z045vDZYP7ygoAW9MTlwupxRnDs4QfC7DwfZvLz1gLxUHT89vh3qu3L8jbxPYTy88KAQOzuBqbsj+Pa8Bt8SvddFOj1Gc868jtXwPND/rbyF+xg93VUtvOCXnru5Qyg9frzluy46ejwk5gO9qtxSvIIAj7xLJS07kyH6uwEDgjz83H4711hzvLZ1i7yRvZI7KdtFvImOCTzqoYW60D3eu8Kq0DztBB68KE4HvKZOm7wGnrq88bEbPUV0kDyaYhe8Mw1BPKTgZ73Vnbg7pJHcOn5A5Lv3rQa9j+ByvE8BFDyVqE88XZ0DPfar/zxMvHO8NJEQO7B9ULw4fNE8RQ21POMq4TsciaQ8vAdxPFDZIzyzCyS8rUVOPcufh7zTAxe8kxOfPAUKAD01gKy7B+y2OgBvATtC/Ui80rEaPLaHqjwtGAK8a01pu7LA3rzxSZi8AeRRPIlqsrvfhSi6xpuxvHr/rrxiYsm8LtAZOi6LlzwAtxS9IA0HPBcCPzwRxDG8c6kkPM81RTwdpnK5rq9sPJdt87yaVjG8l+RyPMncZTzRD688xyw7vNIkcDwCSj68DX77O7CYj7xYpMm79l8QvMRk9zwMTEK8SfBOPPJal7zVE1a8/v9TPMW3Sju3+5S6L82FPHf6XrwE0S68dtsxPY/Ty7yZkF08jPFqO7AOy7yfveO8V2zdu6UGODq5yu88CPwCvdcrFbt94/68uxGkPJPHRryN2lG8w+buOiaWAb1zJ9m6opIdPDvMCDwE9Xg8YmAsvZmlYrxBQV+8wDFPPG7Z2Dz0QpW8WkQGPemrC7zHzZ+7fLsrvKp9J7y2aQi7skHfPFrJwTwsvzi7p46SPBqaPbzA0tK8Ss3iPKCMZDzWmci6BbZLPFs7FjwXrwY9LLhsPBiLMzsuwKc7hR8IPaEOtjxd4ha7dF8GvI5OozwgUqU83cFiPPoY3TwKEhY9eTt4O9r/RjzR7LW79FAYO+v2YDzZIAo8CGkMPEenUb2ukPc7ARifPETcbLupiGW8b7aivJ7577yGgDK88NBgvDkznTsjPhS79IhXPL5VAD2bBZC8UXhavCBTGj04fpY7xAqYuj7EiLxdQWW8VT6OvGfnYzyYWSG8KI52PBPQSbzEnNk6fNK3u0LxX7uKJtA8eMkaPPmUnLzjyHy9/mtkvOOfWDy4UqK8bYwcuySkHjxvE5o8S/ygvEJoObzVgZ28uAjOPAI3X7w/VCS8csBoPM9kg7z8cX48U5ikPGge+7soKN+8rUhbvdz6bDzj0Ai8Xx2cu2MZVzxBpoa80MQDPXKSXzyBoBK9jt5FPEBC7ryWE507dt4KPWEywDzCOIE8AdAXvOqt/jtLk+a8YKQ8uxR4h7wS9hk84ZJvuFK36zx3RUS92pBrvKshSbzRwaA7gYC0POKalzz+e7k7XZ/dvMcIe7wmvwe8+h2kPPcK8bz8Sgi8TXYXOnzosLs1pra6q2AdPJBBUjtxNmm8WcHsOpb/UDziEhQ6MQNtvGhKqDs6K0Y7Jd8DPS+u7rtEzDs8KUsaPfuzqDz7FJg7DlaMPNqoVzyw7wi9Y5BkPIBvVTth6c07OPaaPJBi8DsDb9S8ssLRvF8wojz0pgQ82hJaPLDsvbw/bF88lWXEuytiq7wX1JO8yUf/uh0dvLxlGtm8grsUPNjJ+bsv1wg8UlxIuxIyvLztsNy7RYoGvMze6ruUFcw6ukKyO2MIGbvIPwQ9fPiOO+jNjzyfV/+8IXhbPANmAjwLbKE8tH8MvJofFDyjjMq8ycrDu/cnqTvVJ9m8QmTiO/t+N72Crcc8sKWIPDxWebz2ik28uZbLuzWCRzz5zC+8Kdg6vEpkOTyJO128WiLLvEeDzbugrOw8kUneuuCC8TysCbu8lQiBvBTxmDw/BiQ9IMDqvIrHfrvAZxk7XdeKO1fheTxvrzu7KqDBuywjL72RiI48yESLvBrr+DvI+do7SI4uvKfsHjyyFM88/IvsPCyLHjwtLsA8ILL3vA+/E7xjglS7uokqPPTXcTz4w6e86bFAPGTqxLxb3QG7zM6tvBDnRbt+jwS97OMsve4RErxH6T68DNTvvPUYmjqvUvi8XwhSvNjoK70viqM8nXXGvE+rL7xhoqG8IQO8vM+6Jb3q9Gg6bO6RPO269Tv7oWO7DZrrPJqzfTuAA4W86TXiO+JhUjxGvOQ7M8l6u291/jqA9m88NpgtPNB3Cb0Ijh08ms8SPaxUbLym2D28PJeFvFg4PLtatYG8Oa+5vCYB67sjr6477z5qvHZ3iLqtwdw6IUE7u41klrtFPDw7N2dpvQk8lbyiz6a8zBzlPEEmJLzl+YW7MYuivDwFtryLzY47alQbvJSY4jxbJZ2407mEuzmTVD3CIrU8/hTtO3caojxJoQ+9X8IYOA+TsbybCpq84ScqvErwhzvCa8k6//LUu41Kozv9ReC8AwY3vKzBHDxFDt68YIxyOf+KZ7ufIoo8TU8YvAb1AL095AC9LSJUvKDA2ry+UWY7C2XcPI/uiLyOWwA9MVMSPXezEDx3vzQ6l+UMvL6W1zvzxrg7pz4GvbnyHTxS/ec7UMTmO6B2rrzov5S71nn/POo40ztbVQ87QRRPPLXx2rzWK406zkrYvLJmqTsToB+7zaTFvN6e27urbW87FH71PIpb5TvvTGY8IspVvTmULb0rj5U8eJfrOsFjEzyCSvu6V4tMuqC3mDvL07E8RvOOu7s1kzlbBBM8cOVUvNYSkbyam0S8KpQQO+IjFTsbeII8bX2pO12KwDv/ugm8HouhvDokTryvhZ68k/oiPRa0P7yjxVy77aGdvBr8ljzprBA82UQWvVB01TprmMC8Uh1bPMWsqrsJYJC8lsISvHeuHDz11DM8Q2WwvIcwAr3Hh3Q70xrOPFr9XD3g/3A8Axt/PIrcdzprTVa8lL+GPDwwIbtLfrS6xQQHvfGtk7x72EW8NR+hPN2VEjx0uVm8EhaPPBLzyTw45jA821xKO2/L9TzPU5Y7KX7NuncnRzsIN1Q7hxpdvCSRqzsHcdM8oHlBPBwCibuBjA48X6TIvC5K6rwR7Vg9xx+9PA4nGrxvaBm8z0rMuPBNnDz8gA28lTkOPY1UOb0/bAg9q9WyuhOavbxJ9Ic884eUOqzahbs0Dnu61/1TvB7aHbwKHgU7Z+G6uy5gVzuw6186NM5CvA1OaTxNBPM8fuPWvLG1Mb2D6aO8aCUDPIAErTuSQus8z4SDvBY40Lx1jEQ8Vf1avJ9NiDszYYc71i2sPFqEVzzSVek8vpkgvbCVZjwUS7684bB/PNInAr2xR1C8Qu3IO9SDwDsgLt684TijO9mlGLyTsgM9T3ozPUEgDTxknIE8BM47ux24WzyO5Fu7h2vGO85Sw7yVSMw7rT2bOh23Bjz47le8lu5fvEFNxjyS8zC69qK1u+AyvbtMLCM8WbyuO6muMzvkqkg7xqiSPHA77LyhXXe8GfmPPH1Pxjyq20u8uTt2uy0ljbyiyxu86u/AusHWkT3Wt827omnevPFbZLxDu7i8WIGTPEeyrzyqbvY72/ykPKVaArujhZA7TZgBvPumdLy17Hy88ZQNPABSJjo2jyY9MQG8uxK/szxNzmu7rVeju+F1HLsie+66dt4XPaLMnrwcnoA7RteMvJZk0LzjDMy8XUfQOkLKF7xqUXI7WNKFvKLANrxFMj08KuMEvAMMB7vmmbo8EU5yu91KVbzGNYG8/UieO9HaWLtOfpS8TsdMu4eEGr22JJG8a1weuyrVtLscU4M8kAkGvcCdSrwEg2A85RF+PJ6ZrrxeqMo8QgLdvBi6irvZpt079GCXPNU3jrxbDkS8lVSpPHQ4DjzTq+w8eVHyvE/whjzPZl680dwRPDm4GbwARWu8HC6QOxzXlLzo8Bc7PYq2PIzddbxIavE7iVcyPGY/YDxUIug77k91u9Gky7zDLJw8j6ErvA==
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: 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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:
- - '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: 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
- index: 0
- object: embedding
- model: qwen3-embedding:4b
- object: list
- usage:
- prompt_tokens: 4
- total_tokens: 4
- status:
- code: 200
- message: OK
-- request:
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate, zstd
- connection:
- - keep-alive
- content-length:
- - '9968'
+ - '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: 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
- index: 0
- object: embedding
- model: qwen3-embedding:4b
- object: list
- usage:
- prompt_tokens: 5
- total_tokens: 5
- status:
- code: 200
- message: OK
-- request:
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate, zstd
- connection:
- - keep-alive
- content-length:
- - '10069'
+ - '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