Fix: add codeexec attachments output (#14787)

### What problem does this PR solve?

add codeexec attachments output

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
buua436
2026-05-11 19:16:33 +08:00
committed by GitHub
parent 39ee2fb120
commit daf8a58c4b
5 changed files with 37 additions and 6 deletions

View File

@ -37,6 +37,7 @@ SYSTEM_OUTPUT_KEYS = frozenset(
{
"content",
"actual_type",
"attachments",
"_ERROR",
"_ARTIFACTS",
"_ATTACHMENT_CONTENT",
@ -312,7 +313,10 @@ module.exports = { main };
self.lang = Language.PYTHON.value
self.script = 'def main(arg1: str, arg2: str) -> dict: return {"result": arg1 + arg2}'
self.arguments = {}
self.outputs = {"result": {"value": "", "type": "object"}}
self.outputs = {
"result": {"value": "", "type": "object"},
"attachments": {"value": [], "type": "Array<String>"},
}
def check(self):
self.check_valid_value(self.lang, "Support languages", ["python", "python3", "nodejs", "javascript"])
@ -468,11 +472,13 @@ class CodeExec(ToolBase, ABC):
self.set_output("_ARTIFACTS", artifact_urls or None)
attachment_text = self._build_attachment_content(artifacts, artifact_urls)
self.set_output("_ATTACHMENT_CONTENT", attachment_text)
self.set_output("attachments", self._build_attachment_markdown_list(artifact_urls))
if attachment_text:
content_parts.append(attachment_text)
else:
self.set_output("_ARTIFACTS", None)
self.set_output("_ATTACHMENT_CONTENT", "")
self.set_output("attachments", [])
self.set_output("content", "\n\n".join([part for part in content_parts if part]).strip())
@ -641,6 +647,23 @@ class CodeExec(ToolBase, ABC):
return f"attachment_count: {len(sections)}\n\n" + "\n\n".join(sections)
return "attachment_count: 0"
def _build_attachment_markdown_list(self, artifact_urls: list[dict]) -> list[str]:
markdown_items = []
for art in artifact_urls:
name = _art_field(art, "name")
url = _art_field(art, "url")
mime_type = str(_art_field(art, "mime_type") or "").strip().lower()
if not name:
continue
if mime_type.startswith("image/") and url:
markdown_items.append(f"![{name}]({url})")
elif url:
markdown_items.append(f"[Download {name}]({url})")
else:
markdown_items.append(name)
return markdown_items
def _normalize_attachment_type(self, name: str, mime_type: str) -> str:
mime_type = str(mime_type or "").strip().lower()
if mime_type.startswith("image/"):

View File

@ -140,7 +140,7 @@ def test_select_business_output_ignores_system_outputs():
"actual_type": {"value": "", "type": "string"},
"_ERROR": {"value": "", "type": "string"},
"_ARTIFACTS": {"value": [], "type": "Array<Object>"},
"_ATTACHMENT_CONTENT": {"value": "", "type": "string"},
"attachments": {"value": [], "type": "Array<String>"},
"raw_result": {"value": None, "type": "Any"},
"_created_time": {"value": 1.0, "type": "Number"},
"_elapsed_time": {"value": 2.0, "type": "Number"},
@ -297,7 +297,7 @@ def test_legacy_multi_output_schema_is_rejected():
)
@pytest.mark.parametrize("name", ["content", "actual_type", "_ERROR", "_ARTIFACTS", "_ATTACHMENT_CONTENT", "raw_result"])
@pytest.mark.parametrize("name", ["content", "actual_type", "attachments", "_ERROR", "_ARTIFACTS", "raw_result"])
def test_reserved_business_output_names_are_rejected(name):
module = _load_module()
with pytest.raises(module.ContractError, match="reserved output name"):
@ -387,7 +387,6 @@ def test_process_execution_result_returns_early_for_stderr_only_without_artifact
def test_process_execution_result_appends_artifact_content_to_canonical_content():
tool = _build_code_exec("Object")
tool._upload_artifacts = lambda _artifacts: [{"name": "chart.png", "url": "/artifact/chart.png", "mime_type": "image/png", "size": 12}]
tool._build_attachment_content = lambda _artifacts, _artifact_urls: "attachment_count: 1\n\nattachment1 (image): chart.png\nparsed artifact"
result = tool._process_execution_result(
'{"foo": "bar"}',
@ -400,8 +399,7 @@ def test_process_execution_result_appends_artifact_content_to_canonical_content(
assert result["content"] == '{\n "foo": "bar"\n}\n\nattachment_count: 1\n\nattachment1 (image): chart.png\nparsed artifact'
assert result["_ARTIFACTS"] == [{"name": "chart.png", "url": "/artifact/chart.png", "mime_type": "image/png", "size": 12}]
assert result["_ARTIFACTS"][0]["mime_type"] == "image/png"
assert result["_ATTACHMENT_CONTENT"] == "attachment_count: 1\n\nattachment1 (image): chart.png\nparsed artifact"
assert "attachment1 (image): chart.png" in result["_ATTACHMENT_CONTENT"]
assert result["attachments"] == ["![chart.png](/artifact/chart.png)"]
def test_process_execution_result_without_artifacts_clears_stale_artifacts_output():

View File

@ -4,6 +4,7 @@ import { CodeOutputContract } from '../../form/code-form/utils';
const SYSTEM_OUTPUT_NAMES = new Set([
'_ERROR',
'_ARTIFACTS',
'attachments',
'_ATTACHMENT_CONTENT',
]);

View File

@ -14,6 +14,7 @@ const CodeExecReservedOutputKeys = [
'content',
'actual_type',
'raw_result',
'attachments',
'_ERROR',
'_ARTIFACTS',
'_ATTACHMENT_CONTENT',
@ -30,6 +31,10 @@ export const CodeExecPanelSystemOutputs: ICodeForm['outputs'] = {
type: 'String',
value: '',
},
attachments: {
type: 'Array<String>',
value: [],
},
};
const CodeExecReservedOutputKeySet = new Set<string>(

View File

@ -73,6 +73,10 @@ function getNodeOutputs(x: BaseNode) {
type: JsonSchemaDataType.String,
value: '',
},
attachments: outputs.attachments ?? {
type: 'Array<String>',
value: [],
},
};
}