mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* fix(view-image): read remote sandbox images from sandbox * docs(tools): clarify view_image sandbox behavior * fix(view-image): address sandbox lifecycle review * fix(view-image): preserve image provenance across sandbox replacement * fix(view-image): address provider recovery review * fix(view-image): drain cancelled tool reads
131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
import base64
|
|
import hashlib
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from deerflow.agents.thread_state import ViewedImageData
|
|
from deerflow.tools.builtins.view_image_tool import _is_file_not_found_error, view_image_tool
|
|
|
|
PNG_BYTES = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==")
|
|
|
|
|
|
class _HttpNotFoundError(Exception):
|
|
status_code = 404
|
|
|
|
|
|
class _ProviderFileNotFoundError(Exception):
|
|
pass
|
|
|
|
|
|
_ProviderFileNotFoundError.__name__ = "FileNotFoundError"
|
|
|
|
|
|
class _HttpMissingSandbox:
|
|
id = "remote-new"
|
|
|
|
def __init__(self) -> None:
|
|
self.downloads: list[str] = []
|
|
|
|
def download_file(self, path: str) -> bytes:
|
|
self.downloads.append(path)
|
|
try:
|
|
raise _HttpNotFoundError("not found")
|
|
except _HttpNotFoundError as error:
|
|
raise OSError(f"cannot read '{path}' from remote provider") from error
|
|
|
|
|
|
class _Provider:
|
|
def __init__(self, sandbox: _HttpMissingSandbox) -> None:
|
|
self.sandbox = sandbox
|
|
|
|
def get(self, sandbox_id: str):
|
|
return self.sandbox if sandbox_id == self.sandbox.id else None
|
|
|
|
|
|
def _thread_data(tmp_path: Path) -> dict[str, str]:
|
|
user_data = tmp_path / "threads" / "thread-1" / "user-data"
|
|
workspace = user_data / "workspace"
|
|
uploads = user_data / "uploads"
|
|
outputs = user_data / "outputs"
|
|
for directory in (workspace, uploads, outputs):
|
|
directory.mkdir(parents=True)
|
|
return {
|
|
"workspace_path": str(workspace),
|
|
"uploads_path": str(uploads),
|
|
"outputs_path": str(outputs),
|
|
}
|
|
|
|
|
|
def test_classifier_accepts_explicit_http_404_cause():
|
|
try:
|
|
try:
|
|
raise _HttpNotFoundError("not found")
|
|
except _HttpNotFoundError as cause:
|
|
raise OSError("wrapped remote read failure") from cause
|
|
except OSError as error:
|
|
assert _is_file_not_found_error(error)
|
|
|
|
|
|
def test_classifier_accepts_provider_defined_file_not_found_type():
|
|
try:
|
|
raise _ProviderFileNotFoundError("not found")
|
|
except _ProviderFileNotFoundError as error:
|
|
assert _is_file_not_found_error(error)
|
|
|
|
|
|
def test_classifier_ignores_implicit_exception_context():
|
|
try:
|
|
try:
|
|
raise FileNotFoundError("unrelated cleanup miss")
|
|
except FileNotFoundError:
|
|
raise OSError("transport timeout")
|
|
except OSError as error:
|
|
assert error.__cause__ is None
|
|
assert isinstance(error.__context__, FileNotFoundError)
|
|
assert not _is_file_not_found_error(error)
|
|
|
|
|
|
def test_http_404_replacement_sandbox_recovers_verified_host_copy(tmp_path, monkeypatch):
|
|
thread_data = _thread_data(tmp_path)
|
|
host_path = Path(thread_data["outputs_path"]) / "plot.png"
|
|
host_path.write_bytes(PNG_BYTES)
|
|
sandbox = _HttpMissingSandbox()
|
|
monkeypatch.setattr(
|
|
"deerflow.sandbox.sandbox_provider.get_sandbox_provider",
|
|
lambda: _Provider(sandbox),
|
|
)
|
|
runtime = SimpleNamespace(
|
|
state={
|
|
"thread_data": thread_data,
|
|
"sandbox": {"sandbox_id": sandbox.id},
|
|
"viewed_images": {
|
|
"/mnt/user-data/outputs/plot.png": {
|
|
"mime_type": "image/png",
|
|
"size": len(PNG_BYTES),
|
|
"actual_path": str(host_path),
|
|
"sha256": hashlib.sha256(PNG_BYTES).hexdigest(),
|
|
"source_sandbox_id": "remote-old",
|
|
}
|
|
},
|
|
},
|
|
context={"thread_id": "thread-1"},
|
|
config={},
|
|
)
|
|
|
|
result = view_image_tool.func(
|
|
runtime=runtime,
|
|
image_path="/mnt/user-data/outputs/plot.png",
|
|
tool_call_id="tc-http-not-found",
|
|
)
|
|
|
|
assert result.update["messages"][0].content == "Successfully read image"
|
|
viewed = result.update["viewed_images"]["/mnt/user-data/outputs/plot.png"]
|
|
assert viewed["sha256"] == hashlib.sha256(PNG_BYTES).hexdigest()
|
|
assert "source_sandbox_id" not in viewed
|
|
assert sandbox.downloads == ["/mnt/user-data/outputs/plot.png"]
|
|
|
|
|
|
def test_viewed_image_state_contract_includes_provenance_keys():
|
|
assert "sha256" in ViewedImageData.__required_keys__
|
|
assert "source_sandbox_id" in ViewedImageData.__optional_keys__
|