fix: fixed workflow as tool files field return empty problem (#27925)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
This commit is contained in:
CrabSAMA 2025-11-26 14:00:36 +08:00 committed by GitHub
parent 1241cab113
commit 591414307a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 169 additions and 1 deletions

View File

@ -329,7 +329,15 @@ class ToolNode(Node):
json.append(message.message.json_object) json.append(message.message.json_object)
elif message.type == ToolInvokeMessage.MessageType.LINK: elif message.type == ToolInvokeMessage.MessageType.LINK:
assert isinstance(message.message, ToolInvokeMessage.TextMessage) assert isinstance(message.message, ToolInvokeMessage.TextMessage)
stream_text = f"Link: {message.message.text}\n"
# Check if this LINK message is a file link
file_obj = (message.meta or {}).get("file")
if isinstance(file_obj, File):
files.append(file_obj)
stream_text = f"File: {message.message.text}\n"
else:
stream_text = f"Link: {message.message.text}\n"
text += stream_text text += stream_text
yield StreamChunkEvent( yield StreamChunkEvent(
selector=[node_id, "text"], selector=[node_id, "text"],

View File

@ -0,0 +1,160 @@
import sys
import types
from collections.abc import Generator
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
import pytest
from core.file import File, FileTransferMethod, FileType
from core.model_runtime.entities.llm_entities import LLMUsage
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.utils.message_transformer import ToolFileMessageTransformer
from core.variables.segments import ArrayFileSegment
from core.workflow.entities import GraphInitParams
from core.workflow.node_events import StreamChunkEvent, StreamCompletedEvent
from core.workflow.runtime import GraphRuntimeState, VariablePool
from core.workflow.system_variable import SystemVariable
if TYPE_CHECKING: # pragma: no cover - imported for type checking only
from core.workflow.nodes.tool.tool_node import ToolNode
@pytest.fixture
def tool_node(monkeypatch) -> "ToolNode":
module_name = "core.ops.ops_trace_manager"
if module_name not in sys.modules:
ops_stub = types.ModuleType(module_name)
ops_stub.TraceQueueManager = object # pragma: no cover - stub attribute
ops_stub.TraceTask = object # pragma: no cover - stub attribute
monkeypatch.setitem(sys.modules, module_name, ops_stub)
from core.workflow.nodes.tool.tool_node import ToolNode
graph_config: dict[str, Any] = {
"nodes": [
{
"id": "tool-node",
"data": {
"type": "tool",
"title": "Tool",
"desc": "",
"provider_id": "provider",
"provider_type": "builtin",
"provider_name": "provider",
"tool_name": "tool",
"tool_label": "tool",
"tool_configurations": {},
"tool_parameters": {},
},
}
],
"edges": [],
}
init_params = GraphInitParams(
tenant_id="tenant-id",
app_id="app-id",
workflow_id="workflow-id",
graph_config=graph_config,
user_id="user-id",
user_from="account",
invoke_from="debugger",
call_depth=0,
)
variable_pool = VariablePool(system_variables=SystemVariable(user_id="user-id"))
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=0.0)
config = graph_config["nodes"][0]
node = ToolNode(
id="node-instance",
config=config,
graph_init_params=init_params,
graph_runtime_state=graph_runtime_state,
)
node.init_node_data(config["data"])
return node
def _collect_events(generator: Generator) -> tuple[list[Any], LLMUsage]:
events: list[Any] = []
try:
while True:
events.append(next(generator))
except StopIteration as stop:
return events, stop.value
def _run_transform(tool_node: "ToolNode", message: ToolInvokeMessage) -> tuple[list[Any], LLMUsage]:
def _identity_transform(messages, *_args, **_kwargs):
return messages
tool_runtime = MagicMock()
with patch.object(ToolFileMessageTransformer, "transform_tool_invoke_messages", side_effect=_identity_transform):
generator = tool_node._transform_message(
messages=iter([message]),
tool_info={"provider_type": "builtin", "provider_id": "provider"},
parameters_for_log={},
user_id="user-id",
tenant_id="tenant-id",
node_id=tool_node._node_id,
tool_runtime=tool_runtime,
)
return _collect_events(generator)
def test_link_messages_with_file_populate_files_output(tool_node: "ToolNode"):
file_obj = File(
tenant_id="tenant-id",
type=FileType.DOCUMENT,
transfer_method=FileTransferMethod.TOOL_FILE,
related_id="file-id",
filename="demo.pdf",
extension=".pdf",
mime_type="application/pdf",
size=123,
storage_key="file-key",
)
message = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.LINK,
message=ToolInvokeMessage.TextMessage(text="/files/tools/file-id.pdf"),
meta={"file": file_obj},
)
events, usage = _run_transform(tool_node, message)
assert isinstance(usage, LLMUsage)
chunk_events = [event for event in events if isinstance(event, StreamChunkEvent)]
assert chunk_events
assert chunk_events[0].chunk == "File: /files/tools/file-id.pdf\n"
completed_events = [event for event in events if isinstance(event, StreamCompletedEvent)]
assert len(completed_events) == 1
outputs = completed_events[0].node_run_result.outputs
assert outputs["text"] == "File: /files/tools/file-id.pdf\n"
files_segment = outputs["files"]
assert isinstance(files_segment, ArrayFileSegment)
assert files_segment.value == [file_obj]
def test_plain_link_messages_remain_links(tool_node: "ToolNode"):
message = ToolInvokeMessage(
type=ToolInvokeMessage.MessageType.LINK,
message=ToolInvokeMessage.TextMessage(text="https://dify.ai"),
meta=None,
)
events, _ = _run_transform(tool_node, message)
chunk_events = [event for event in events if isinstance(event, StreamChunkEvent)]
assert chunk_events
assert chunk_events[0].chunk == "Link: https://dify.ai\n"
completed_events = [event for event in events if isinstance(event, StreamCompletedEvent)]
assert len(completed_events) == 1
files_segment = completed_events[0].node_run_result.outputs["files"]
assert isinstance(files_segment, ArrayFileSegment)
assert files_segment.value == []