feat: better artefact in convo UI

This commit is contained in:
Alex committed 2026-08-08 12:11:00 +01:00
1 parent e0649d25cf
commit 132a2de3bf
6 files changed
+183 -22

No files matched your search

+27
View File
@@ -944,6 +944,33 @@ class ToolExecutor:
artifact_id = str(artifact_id).strip() if artifact_id is not None else ""
if artifact_id:
tool_call_data["artifact_id"] = artifact_id
# A single call can produce several files (``run_code`` writing more
# than one). ``artifact_id`` names only the first, which left the rest
# with no way into the UI, so report the full set with display names.
get_artifacts = (
getattr(tool, "get_artifacts", None)
if tool_data["name"] != "api_tool"
else None
)
if callable(get_artifacts):
artifacts = []
try:
# Normalize inside the guard: a tool returning an unexpected
# shape must not break the call it just completed.
artifacts = [
{"id": str(a["id"]).strip(), "filename": a.get("filename")}
for a in (get_artifacts(action_name, **parameters) or [])
if isinstance(a, dict) and a.get("id")
]
except Exception:
logger.exception(
"Failed to extract artifacts from tool %s for action %s",
tool_data["name"],
action_name,
)
if artifacts:
tool_call_data["artifacts"] = artifacts
result_full = bound_result_full(str(result))
tool_call_data["resolved_arguments"] = resolved_arguments
tool_call_data["result_full"] = result_full
@@ -435,6 +435,7 @@ class ArtifactGeneratorTool(Tool):
self.workflow_run_id: Optional[str] = self.config.get("workflow_run_id")
self.message_id: Optional[str] = self.config.get("message_id")
self._last_artifact_id: Optional[str] = None
self._last_filename: Optional[str] = None
# ------------------------------------------------------------------
# Tool ABC
@@ -536,12 +537,19 @@ class ArtifactGeneratorTool(Tool):
"""Return the produced artifact id so the UI artifact rail lights up."""
return self._last_artifact_id
def get_artifacts(self, action_name: str, **kwargs: Any) -> List[Dict[str, Any]]:
"""Return the produced artifact with its filename, for UI labelling."""
if not self._last_artifact_id:
return []
return [{"id": self._last_artifact_id, "filename": self._last_filename}]
# ------------------------------------------------------------------
# Dispatch
# ------------------------------------------------------------------
def execute_action(self, action_name: str, **kwargs: Any) -> Dict[str, Any]:
"""Dispatch a create/edit/rewrite action."""
self._last_artifact_id = None
self._last_filename = None
if not self.user_id:
return {"status": "error", "error": "artifact_generator requires a valid user_id."}
if self.conversation_id is None and self.workflow_run_id is None:
@@ -595,6 +603,7 @@ class ArtifactGeneratorTool(Tool):
if ref is None:
return {"status": "error", "error": "failed to persist artifact."}
self._last_artifact_id = ref["artifact_id"]
self._last_filename = ref.get("filename")
return {"status": "ok", **ref}
def _edit(self, **kwargs: Any) -> Dict[str, Any]:
@@ -661,6 +670,7 @@ class ArtifactGeneratorTool(Tool):
if ref is None:
return {"status": "error", "error": "failed to persist artifact version."}
self._last_artifact_id = ref["artifact_id"]
self._last_filename = ref.get("filename")
return {"status": "ok", **ref}
# ------------------------------------------------------------------
+16
View File
@@ -73,6 +73,7 @@ class CodeExecutorTool(Tool):
# Static, deployment-level approval gate (mirrors the action metadata flag).
self._require_approval: bool = bool(self.config.get("require_approval", False))
self._last_artifact_id: Optional[str] = None
self._last_artifacts: List[Dict[str, Any]] = []
# ------------------------------------------------------------------
# Tool ABC
@@ -181,6 +182,15 @@ class CodeExecutorTool(Tool):
"""Return the primary produced artifact id so the UI artifact rail lights up."""
return self._last_artifact_id
def get_artifacts(self, action_name: str, **kwargs: Any) -> List[Dict[str, Any]]:
"""Return every artifact this call produced, with display names.
A single ``run_code`` can write several files. Reporting only the first
left the others reachable by API but invisible in the UI, and gave the
one button the tool's name rather than the file's.
"""
return list(self._last_artifacts)
def preview_decision(self, action_name: str, params: dict) -> Tuple[bool, bool]:
"""Return ``(requires_approval, denylist_forced)`` for the approval gate; never denylist-forced here."""
if action_name != "run_code":
@@ -195,6 +205,7 @@ class CodeExecutorTool(Tool):
if action_name != "run_code":
return {"status": "error", "error": f"unknown action: {action_name}"}
self._last_artifact_id = None
self._last_artifacts = []
return self._run_code(**kwargs)
def _run_code(self, **kwargs: Any) -> Dict[str, Any]:
@@ -404,6 +415,11 @@ class CodeExecutorTool(Tool):
)
if captured:
self._last_artifact_id = captured[0]["artifact_id"]
self._last_artifacts = [
{"id": a["artifact_id"], "filename": a.get("filename")}
for a in captured
if a.get("artifact_id")
]
return captured
def _shape_payload(