Name the readlines workaround when sandbox code iterates a file
50 executions across 40 cases in an 822-case run died on '_io.TextIOWrapper' object is not iterable, and those cases scored 37.5% judged against 60.1% and cited 12.5% against 55.2%.
This commit is contained in:
parent
485a8f901e
commit
68238f0b6a
3 changed files with 49 additions and 1 deletions
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- A failed `analysis_execute_code` call that iterated a file object reports the `.readlines()` workaround alongside the `TypeError`.
|
||||
- A capability that reaches its request limit keeps its cite tool for two further model requests while its other tools are removed, so an exhausted run can still register citations.
|
||||
- A cited chunk id that does not match a retrieved id exactly resolves to the closest one above a 0.75 similarity cutoff, recovering ids mistyped from search results.
|
||||
- Dotfiles are parsed as their actual format instead of a single unstructured text block. Docling ignores the extension of a name starting with a dot, so converters strip leading dots from the name they hand it.
|
||||
|
|
|
|||
|
|
@ -37,6 +37,21 @@ def instructions() -> str:
|
|||
return _instructions_path.read_text().strip()
|
||||
|
||||
|
||||
def _recovery_hint(stderr: str) -> str:
|
||||
"""Name the workaround for sandbox limits models trip over repeatedly.
|
||||
|
||||
The instructions already say file objects are not iterable, and models write
|
||||
``for line in open(...)`` regardless. Carrying the fix in the error gives
|
||||
them something to act on for the retry.
|
||||
"""
|
||||
if "TextIOWrapper" in stderr and "not iterable" in stderr:
|
||||
return (
|
||||
"\n\nHint: file objects cannot be iterated here. Read lines with "
|
||||
'.readlines() or .read().split("\\n").'
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisCapability(RAGCapabilityBase[AnalysisState]):
|
||||
"""Deferred capability for sandboxed computation over a RAG corpus."""
|
||||
|
|
@ -103,7 +118,10 @@ class AnalysisCapability(RAGCapabilityBase[AnalysisState]):
|
|||
)
|
||||
)
|
||||
if not result.success:
|
||||
raise ToolFailed(f"{result.stderr}\n\nOutput: {result.stdout}")
|
||||
raise ToolFailed(
|
||||
f"{result.stderr}{_recovery_hint(result.stderr)}"
|
||||
f"\n\nOutput: {result.stdout}"
|
||||
)
|
||||
return result.stdout or "No output."
|
||||
|
||||
def get_toolset(self) -> FunctionToolset[Any]:
|
||||
|
|
|
|||
|
|
@ -589,6 +589,35 @@ async def test_cite_tool_is_withdrawn_after_the_grace_window(temp_db_path):
|
|||
assert kept == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("stderr", "expect_hint"),
|
||||
[
|
||||
("TypeError: '_io.TextIOWrapper' object is not iterable", True),
|
||||
("TypeError: 'list' object is not an iterator", False),
|
||||
],
|
||||
)
|
||||
async def test_sandbox_iteration_failure_carries_the_workaround(
|
||||
temp_db_path, stderr, expect_hint
|
||||
):
|
||||
"""A model that iterates a file object gets told what to do instead."""
|
||||
capability = create_analysis(db_path=temp_db_path, config=AppConfig())
|
||||
capability.state = AnalysisState()
|
||||
sandbox = AsyncMock()
|
||||
sandbox.execute.return_value = SandboxResult(
|
||||
stdout="", stderr=stderr, success=False
|
||||
)
|
||||
sandbox._search_results = []
|
||||
capability.sandbox = cast(Sandbox, sandbox)
|
||||
|
||||
with pytest.raises(ToolFailed) as failure:
|
||||
await capability._execute_code(
|
||||
"for line in open('/documents/x/items.jsonl'): pass"
|
||||
)
|
||||
|
||||
assert (".readlines()" in str(failure.value)) is expect_hint
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analysis_sandbox_failure_records_execution_and_fails_the_tool(
|
||||
temp_db_path,
|
||||
|
|
|
|||
Loading…
Reference in a new issue