Support open()/with in the analysis sandbox

Document files can be read with open() and with-blocks (.read(),
.readline(), .readlines()); writes raise PermissionError. File objects
remain non-iterable and the collections module is still unavailable.
This commit is contained in:
Yiorgis Gozadinos 2026-07-25 10:41:27 +03:00
parent 53084d6fdd
commit 5c8df37af1
No known key found for this signature in database
6 changed files with 204 additions and 3 deletions

View file

@ -1,6 +1,10 @@
# Changelog
## [Unreleased]
### Added
- Analysis sandbox supports `open()` and `with` blocks for reading document files, including `.read()`, `.readline()`, and `.readlines()`.
### Changed
- Require `pydantic-ai-slim>=2.18,<3`.

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
Available modules: `json`, `re`, `math`, `pathlib`
Not supported: class definitions, generators/yield, match statements, decorators, `with` statements
Not supported: class definitions, generators/yield, match statements, decorators, `collections`
### 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
Always use `Path.read_text()` — do NOT use `open()` or `with` statements (they are not supported).
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`.
```python
from pathlib import Path
@ -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`)
- Use `Path.read_text()` to read files — do NOT use `open()`, `with` statements, or `collections` module
- 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
- 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -383,6 +383,77 @@ class TestSandboxVFS:
assert result.success
assert result.stdout.count("True") == 6
@pytest.mark.asyncio
@pytest.mark.vcr()
async def test_open_read(self, temp_db_path):
"""open() and a with-block read document files through the VFS."""
config = AppConfig()
async with HaikuRAG(temp_db_path, create=True) as client:
doc = await client.create_document(
content="Content about foxes and dogs.",
uri="test://doc",
title="Fox Document",
)
context = AnalysisContext()
sb = Sandbox(db_path=temp_db_path, config=config, context=context)
result = await sb.execute(
f"with open('/documents/{doc.id}/content.txt') as f:\n"
" data = f.read()\n"
"print('foxes' in data.lower())"
)
assert result.success
assert "True" in result.stdout
@pytest.mark.asyncio
@pytest.mark.vcr()
async def test_open_readlines(self, temp_db_path):
"""readlines() splits a newline-delimited VFS file into lines."""
config = AppConfig()
async with HaikuRAG(temp_db_path, create=True) as client:
doc = await client.create_document(
content="The quick brown fox jumps over the lazy dog.",
uri="test://animals",
title="Animals",
)
context = AnalysisContext()
sb = Sandbox(db_path=temp_db_path, config=config, context=context)
result = await sb.execute(
f"lines = open('/documents/{doc.id}/items.jsonl').readlines()\n"
"print(len(lines) > 0)\n"
"import json\n"
"print('self_ref' in json.loads(lines[0]))"
)
assert result.success
assert result.stdout.count("True") == 2
@pytest.mark.asyncio
@pytest.mark.vcr()
async def test_open_write_denied(self, temp_db_path):
"""Opening a document file for writing raises PermissionError."""
config = AppConfig()
async with HaikuRAG(temp_db_path, create=True) as client:
doc = await client.create_document(
content="Content about foxes and dogs.",
uri="test://doc",
title="Fox Document",
)
context = AnalysisContext()
sb = Sandbox(db_path=temp_db_path, config=config, context=context)
result = await sb.execute(
"try:\n"
f" with open('/documents/{doc.id}/content.txt', 'w') as f:\n"
" f.write('nope')\n"
" print('WROTE')\n"
"except PermissionError:\n"
" print('DENIED')"
)
assert result.success
assert "DENIED" in result.stdout
assert "WROTE" not in result.stdout
@pytest.mark.asyncio
@pytest.mark.vcr()
async def test_context_filter_limits_vfs(self, temp_db_path):