diff --git a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md index fb4b00a8..5dec8401 100644 --- a/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md +++ b/haiku_rag_slim/haiku/rag/capabilities/instructions/analysis.md @@ -17,7 +17,7 @@ Inside the code, these functions are available (use `await`): - `await list_documents()` → list of dicts with keys: id, title, uri, created_at Available modules: `json`, `re`, `math`, `pathlib` -Not supported: class definitions, generators/yield, match statements, decorators, `collections` +Not supported: class definitions, generators/yield, match statements, decorators, `collections`, iterating a file object (`for line in f`) ### analysis_search Search the knowledge base directly (outside code execution). Each result has a `Type:` (paragraph, table, code, list_item, picture). When the Type is `picture`, the corresponding figure may also be attached to the tool response as an image alongside the text — use it directly to answer questions about figures, diagrams, charts, screenshots. @@ -48,7 +48,7 @@ All documents are mounted as a virtual filesystem at `/documents/`: `{document_id}` is an internal identifier, not the user-facing `uri` (filename, URL, etc.). When you only know a document by its URI or title, use `await list_documents()` to enumerate ids and match against `uri` / `title` — that's a single call to the host. Iterating `/documents/` and reading every `metadata.json` works too but is much slower on portal-scale corpora. ### Reading files -Read with `Path.read_text()` or `open()` (including `with` blocks); file objects support `.read()`, `.readline()`, and `.readlines()`. File objects are NOT iterable — do not write `for line in f`; use `.readlines()` or `text.split(chr(10))` for line-wise processing. Files are read-only; writing raises `PermissionError`. +Read with `Path.read_text()` or `open()` (including `with` blocks); file objects support `.read()`, `.readline()`, and `.readlines()`. A file object cannot be iterated, so read line-wise with `.readlines()` or `.read().split("\n")` instead of `for line in f`. Files are read-only; writing raises `PermissionError`. ```python from pathlib import Path @@ -63,7 +63,7 @@ for doc_dir in Path('/documents').iterdir(): content = Path(f'/documents/{doc_id}/content.txt').read_text() # Read and parse items -for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split(chr(10)): +for line in Path(f'/documents/{doc_id}/items.jsonl').read_text().strip().split("\n"): item = json.loads(line) if item['label'] == 'table': print(item['text'][:200]) @@ -112,6 +112,6 @@ You MUST call `analysis_cite` with at least one chunk ID before producing your f - Use `print()` to output results — the output is your only feedback - When you write code, execute it — don't describe what code would do. But not every question needs code; simple lookups are best answered by `analysis_search → analysis_cite`. - Use `await` for all async functions inside `analysis_execute_code` (`search`, `list_documents`) -- Read files with `Path.read_text()` or `open()`/`with`; file objects are not iterable (no `for line in f`) and the `collections` module is unavailable +- Read files with `Path.read_text()` or `open()`/`with`. For lines use `.readlines()` or `.read().split("\n")`, never `for line in f`. The `collections` module is unavailable. - Do NOT include chunk IDs or UUIDs in your answer text — your answer should read naturally. Use the `analysis_cite` tool separately to register citations. `cite{...}` markdown-style inline references do nothing; only an actual `analysis_cite` tool call registers a citation. - **Before you write your final answer, invoke the `analysis_cite` tool with the supporting chunk_ids.** This is the last tool call before answering whenever your answer draws on retrieved evidence. diff --git a/tests/sandbox/test_sandbox.py b/tests/sandbox/test_sandbox.py index 27c4fa5c..0a19328c 100644 --- a/tests/sandbox/test_sandbox.py +++ b/tests/sandbox/test_sandbox.py @@ -7,6 +7,7 @@ import pytest from haiku.rag.client import HaikuRAG from haiku.rag.config.models import AppConfig from haiku.rag.sandbox import AnalysisContext, Sandbox, SandboxResult +from haiku.rag.store.models.chunk import Chunk @pytest.fixture(scope="module") @@ -454,6 +455,49 @@ class TestSandboxVFS: assert "DENIED" in result.stdout assert "WROTE" not in result.stdout + @pytest.mark.asyncio + async def test_open_file_objects_are_not_iterable(self, temp_db_path): + """Pins the limitation the instructions warn about: pydantic/monty#490. + + A failure here means Monty gained iteration support and the + `for line in f` prohibition in the analysis instructions is now wrong. + """ + from docling_core.types.doc.document import DoclingDocument + from docling_core.types.doc.labels import DocItemLabel + + config = AppConfig() + docling = DoclingDocument(name="d") + docling.add_text(label=DocItemLabel.TEXT, text="one\ntwo") + + async with HaikuRAG(temp_db_path, create=True) as client: + doc = await client.import_document( + docling, + [ + Chunk( + content="one\ntwo", + embedding=[0.1] * config.embeddings.model.vector_dim, + order=0, + ) + ], + uri="test://lines", + ) + + sb = Sandbox(db_path=temp_db_path, config=config, context=AnalysisContext()) + try: + result = await sb.execute( + f"for line in open('/documents/{doc.id}/content.txt'):\n print(line)" + ) + assert result.success is False + assert "not iterable" in result.stderr + + # The documented alternatives do work. + result = await sb.execute( + f"print(len(open('/documents/{doc.id}/content.txt').readlines()))" + ) + assert result.success, result.stderr + finally: + await sb.close() + @pytest.mark.asyncio @pytest.mark.vcr() async def test_context_filter_limits_vfs(self, temp_db_path):