mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 03:12:21 +02:00
fix write_file fenced source artifacts
This commit is contained in:
@@ -25,6 +25,31 @@ _BINARY_ARTIFACT_SUFFIXES = _STRUCTURED_DOCUMENT_SUFFIXES | frozenset({
|
|||||||
".png", ".wav", ".webm", ".webp", ".zip",
|
".png", ".wav", ".webm", ".webp", ".zip",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Models frequently put source artifacts in a Markdown code fence even when a
|
||||||
|
# tool schema asks for the raw file body. Persisting that fence makes HTML,
|
||||||
|
# CSS, JavaScript, and source files invalid. Restrict normalization to
|
||||||
|
# code-like targets so a user can still write a literal fence to Markdown.
|
||||||
|
_FENCED_SOURCE_SUFFIXES = frozenset({
|
||||||
|
".css", ".csv", ".html", ".htm", ".js", ".json", ".jsx", ".mjs",
|
||||||
|
".py", ".sh", ".sql", ".svg", ".ts", ".tsx", ".xml", ".yaml", ".yml",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _unwrap_fenced_source_body(body: str, path: str) -> str:
|
||||||
|
"""Remove an accidental outer Markdown fence from a source artifact.
|
||||||
|
|
||||||
|
An opening fence is enough to normalize: generation can end during a tool
|
||||||
|
call while its argument remains otherwise usable, and retaining the fence
|
||||||
|
corrupts the artifact. This only applies to source-like file extensions.
|
||||||
|
"""
|
||||||
|
if os.path.splitext(path)[1].casefold() not in _FENCED_SOURCE_SUFFIXES:
|
||||||
|
return body
|
||||||
|
match = re.match(r"^(\s*)```[^\r\n]*\r?\n", body)
|
||||||
|
if not match:
|
||||||
|
return body
|
||||||
|
unwrapped = body[match.end():]
|
||||||
|
return re.sub(r"\r?\n```\s*$", "", unwrapped)
|
||||||
|
|
||||||
|
|
||||||
def _glob_to_regex(pat: str) -> "re.Pattern":
|
def _glob_to_regex(pat: str) -> "re.Pattern":
|
||||||
"""Translate a forward-slash glob (**, *, ?) into a compiled regex.
|
"""Translate a forward-slash glob (**, *, ?) into a compiled regex.
|
||||||
@@ -247,6 +272,7 @@ class WriteFileTool:
|
|||||||
path = _resolve_tool_path(raw_path)
|
path = _resolve_tool_path(raw_path)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return {"error": f"write_file: {e}", "exit_code": 1}
|
return {"error": f"write_file: {e}", "exit_code": 1}
|
||||||
|
body = _unwrap_fenced_source_body(body, path)
|
||||||
# A frequent multimodal artifact failure is writing SVG markup to a
|
# A frequent multimodal artifact failure is writing SVG markup to a
|
||||||
# path whose extension promises a raster image. The file exists, so
|
# path whose extension promises a raster image. The file exists, so
|
||||||
# ordinary artifact checks pass, but image judges cannot decode it.
|
# ordinary artifact checks pass, but image judges cannot decode it.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.agent_tools.filesystem_tools import EditFileTool, ReadFileTool, WriteFileTool
|
from src.agent_tools.filesystem_tools import EditFileTool, ReadFileTool, WriteFileTool
|
||||||
@@ -90,3 +92,34 @@ async def test_write_file_still_rewrites_existing_text_file(tmp_path, monkeypatc
|
|||||||
|
|
||||||
assert result["exit_code"] == 0
|
assert result["exit_code"] == 0
|
||||||
assert target.read_text(encoding="utf-8") == "new"
|
assert target.read_text(encoding="utf-8") == "new"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_file_unwraps_markdown_fence_for_html_artifact(tmp_path, monkeypatch):
|
||||||
|
import src.tool_execution as tool_execution
|
||||||
|
|
||||||
|
target = tmp_path / "output.html"
|
||||||
|
monkeypatch.setattr(tool_execution, "_resolve_tool_path", lambda _path: str(target))
|
||||||
|
|
||||||
|
result = await WriteFileTool().execute(
|
||||||
|
'{"path":"output.html","content":"```html\\n<div>clock</div>\\n```"}', {}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["exit_code"] == 0
|
||||||
|
assert target.read_text(encoding="utf-8") == "<div>clock</div>"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_write_file_preserves_literal_fence_for_markdown(tmp_path, monkeypatch):
|
||||||
|
import src.tool_execution as tool_execution
|
||||||
|
|
||||||
|
target = tmp_path / "notes.md"
|
||||||
|
monkeypatch.setattr(tool_execution, "_resolve_tool_path", lambda _path: str(target))
|
||||||
|
content = "```python\\nprint('literal example')\\n```"
|
||||||
|
|
||||||
|
result = await WriteFileTool().execute(
|
||||||
|
json.dumps({"path": "notes.md", "content": content}), {}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["exit_code"] == 0
|
||||||
|
assert target.read_text(encoding="utf-8") == content
|
||||||
|
|||||||
Reference in New Issue
Block a user