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:
parent
53084d6fdd
commit
5c8df37af1
6 changed files with 204 additions and 3 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue