Document that file objects cannot be iterated

Monty 0.0.19 supports open() and with blocks, but a file object is still not
iterable. See pydantic/monty#490, which is still open. The instructions now
state the limitation in three places and give the alternative next to each
one: readlines() or read().split("\n").

Replace chr(10) with "\n" in the prose and in the example. Both work on
0.0.19, and chr(10) implies that the escape is broken.

Add a test that pins the limitation. The test fails when Monty gains
iteration support, which is the signal to relax the instructions.
This commit is contained in:
Yiorgis Gozadinos 2026-07-28 15:22:27 +03:00
parent 5c8df37af1
commit feaca386d3
No known key found for this signature in database
2 changed files with 48 additions and 4 deletions

View file

@ -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 - `await list_documents()` → list of dicts with keys: id, title, uri, created_at
Available modules: `json`, `re`, `math`, `pathlib` 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 ### 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. 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. `{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 ### 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 ```python
from pathlib import Path 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() content = Path(f'/documents/{doc_id}/content.txt').read_text()
# Read and parse items # 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) item = json.loads(line)
if item['label'] == 'table': if item['label'] == 'table':
print(item['text'][:200]) 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 - 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`. - 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`) - 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. - 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. - **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.

View file

@ -7,6 +7,7 @@ import pytest
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
from haiku.rag.config.models import AppConfig from haiku.rag.config.models import AppConfig
from haiku.rag.sandbox import AnalysisContext, Sandbox, SandboxResult from haiku.rag.sandbox import AnalysisContext, Sandbox, SandboxResult
from haiku.rag.store.models.chunk import Chunk
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -454,6 +455,49 @@ class TestSandboxVFS:
assert "DENIED" in result.stdout assert "DENIED" in result.stdout
assert "WROTE" not 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.asyncio
@pytest.mark.vcr() @pytest.mark.vcr()
async def test_context_filter_limits_vfs(self, temp_db_path): async def test_context_filter_limits_vfs(self, temp_db_path):