fix: normalize presented artifact paths (#998)

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Xinmin Zeng
2026-03-06 22:51:27 +08:00
committed by GitHub
parent 9d2144d431
commit 09325ca28f
2 changed files with 145 additions and 1 deletions

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Annotated
from langchain.tools import InjectedToolCallId, ToolRuntime, tool
@@ -6,6 +7,58 @@ from langgraph.types import Command
from langgraph.typing import ContextT
from src.agents.thread_state import ThreadState
from src.config.paths import VIRTUAL_PATH_PREFIX, get_paths
OUTPUTS_VIRTUAL_PREFIX = f"{VIRTUAL_PATH_PREFIX}/outputs"
def _normalize_presented_filepath(
runtime: ToolRuntime[ContextT, ThreadState],
filepath: str,
) -> str:
"""Normalize a presented file path to the `/mnt/user-data/outputs/*` contract.
Accepts either:
- A virtual sandbox path such as `/mnt/user-data/outputs/report.md`
- A host-side thread outputs path such as
`/app/backend/.deer-flow/threads/<thread>/user-data/outputs/report.md`
Returns:
The normalized virtual path.
Raises:
ValueError: If runtime metadata is missing or the path is outside the
current thread's outputs directory.
"""
if runtime.state is None:
raise ValueError("Thread runtime state is not available")
thread_id = runtime.context.get("thread_id")
if not thread_id:
raise ValueError("Thread ID is not available in runtime context")
thread_data = runtime.state.get("thread_data") or {}
outputs_path = thread_data.get("outputs_path")
if not outputs_path:
raise ValueError("Thread outputs path is not available in runtime state")
outputs_dir = Path(outputs_path).resolve()
stripped = filepath.lstrip("/")
virtual_prefix = VIRTUAL_PATH_PREFIX.lstrip("/")
if stripped == virtual_prefix or stripped.startswith(virtual_prefix + "/"):
actual_path = get_paths().resolve_virtual_path(thread_id, filepath)
else:
actual_path = Path(filepath).expanduser().resolve()
try:
relative_path = actual_path.relative_to(outputs_dir)
except ValueError as exc:
raise ValueError(
f"Only files in {OUTPUTS_VIRTUAL_PREFIX} can be presented: {filepath}"
) from exc
return f"{OUTPUTS_VIRTUAL_PREFIX}/{relative_path.as_posix()}"
@tool("present_files", parse_docstring=True)
@@ -33,7 +86,23 @@ def present_file_tool(
Args:
filepaths: List of absolute file paths to present to the user. **Only** files in `/mnt/user-data/outputs` can be presented.
"""
try:
normalized_paths = [
_normalize_presented_filepath(runtime, filepath) for filepath in filepaths
]
except ValueError as exc:
return Command(
update={
"messages": [ToolMessage(f"Error: {exc}", tool_call_id=tool_call_id)]
},
)
# The merge_artifacts reducer will handle merging and deduplication
return Command(
update={"artifacts": filepaths, "messages": [ToolMessage("Successfully presented files", tool_call_id=tool_call_id)]},
update={
"artifacts": normalized_paths,
"messages": [
ToolMessage("Successfully presented files", tool_call_id=tool_call_id)
],
},
)