mirror of https://github.com/langgenius/dify.git
fix: tolerate missing tool icons in workflow history API (#33635)
Root cause: node-executions and agent logs called get_tool_icon without the same try/except used in workflow streaming.
This commit is contained in:
parent
9336935295
commit
e01fc9924d
|
|
@ -1012,11 +1012,15 @@ class WorkflowNodeExecutionModel(Base): # This model is expected to have `offlo
|
|||
if execution_metadata:
|
||||
if self.node_type == BuiltinNodeTypes.TOOL and "tool_info" in execution_metadata:
|
||||
tool_info: dict[str, Any] = execution_metadata["tool_info"]
|
||||
extras["icon"] = ToolManager.get_tool_icon(
|
||||
tenant_id=self.tenant_id,
|
||||
provider_type=tool_info["provider_type"],
|
||||
provider_id=tool_info["provider_id"],
|
||||
)
|
||||
try:
|
||||
extras["icon"] = ToolManager.get_tool_icon(
|
||||
tenant_id=self.tenant_id,
|
||||
provider_type=tool_info["provider_type"],
|
||||
provider_id=tool_info["provider_id"],
|
||||
)
|
||||
except Exception:
|
||||
# metadata fetch may fail, for example, the plugin daemon is down or plugin is uninstalled.
|
||||
logger.warning("failed to fetch icon for %s", tool_info.get("provider_id"))
|
||||
elif self.node_type == BuiltinNodeTypes.DATASOURCE and "datasource_info" in execution_metadata:
|
||||
datasource_info = execution_metadata["datasource_info"]
|
||||
extras["icon"] = datasource_info.get("icon")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -13,6 +14,8 @@ from libs.login import current_user
|
|||
from models import Account
|
||||
from models.model import App, Conversation, EndUser, Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentService:
|
||||
@classmethod
|
||||
|
|
@ -109,19 +112,23 @@ class AgentService:
|
|||
tool_meta_data = tool_meta.get(tool_name, {})
|
||||
tool_config = tool_meta_data.get("tool_config", {})
|
||||
if tool_config.get("tool_provider_type", "") != "dataset-retrieval":
|
||||
tool_icon = ToolManager.get_tool_icon(
|
||||
tenant_id=app_model.tenant_id,
|
||||
provider_type=tool_config.get("tool_provider_type", ""),
|
||||
provider_id=tool_config.get("tool_provider", ""),
|
||||
)
|
||||
if not tool_icon:
|
||||
tool_entity = find_agent_tool(tool_name)
|
||||
if tool_entity:
|
||||
tool_icon = ToolManager.get_tool_icon(
|
||||
tenant_id=app_model.tenant_id,
|
||||
provider_type=tool_entity.provider_type,
|
||||
provider_id=tool_entity.provider_id,
|
||||
)
|
||||
tool_icon = ""
|
||||
try:
|
||||
tool_icon = ToolManager.get_tool_icon(
|
||||
tenant_id=app_model.tenant_id,
|
||||
provider_type=tool_config.get("tool_provider_type", ""),
|
||||
provider_id=tool_config.get("tool_provider", ""),
|
||||
)
|
||||
if not tool_icon:
|
||||
tool_entity = find_agent_tool(tool_name)
|
||||
if tool_entity:
|
||||
tool_icon = ToolManager.get_tool_icon(
|
||||
tenant_id=app_model.tenant_id,
|
||||
provider_type=tool_entity.provider_type,
|
||||
provider_id=tool_entity.provider_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("failed to fetch icon for %s", tool_config.get("tool_provider", tool_name))
|
||||
else:
|
||||
tool_icon = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from uuid import uuid4
|
|||
|
||||
from constants import HIDDEN_VALUE
|
||||
from core.helper import encrypter
|
||||
from dify_graph.enums import BuiltinNodeTypes
|
||||
from dify_graph.file.enums import FileTransferMethod, FileType
|
||||
from dify_graph.file.models import File
|
||||
from dify_graph.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable
|
||||
|
|
@ -190,6 +191,42 @@ class TestWorkflowNodeExecution:
|
|||
node_exec.execution_metadata = json.dumps(original)
|
||||
assert node_exec.execution_metadata_dict == original
|
||||
|
||||
def test_extras_tool_node_skips_icon_when_fetch_fails(self):
|
||||
node_exec = WorkflowNodeExecutionModel()
|
||||
node_exec.tenant_id = "tenant-1"
|
||||
node_exec.node_type = BuiltinNodeTypes.TOOL
|
||||
node_exec.execution_metadata = json.dumps(
|
||||
{
|
||||
"tool_info": {
|
||||
"provider_type": "plugin",
|
||||
"provider_id": "missing-provider",
|
||||
}
|
||||
}
|
||||
)
|
||||
with mock.patch(
|
||||
"core.tools.tool_manager.ToolManager.get_tool_icon",
|
||||
side_effect=ValueError("provider not found"),
|
||||
):
|
||||
assert node_exec.extras == {}
|
||||
|
||||
def test_extras_tool_node_includes_icon_when_fetch_succeeds(self):
|
||||
node_exec = WorkflowNodeExecutionModel()
|
||||
node_exec.tenant_id = "tenant-1"
|
||||
node_exec.node_type = BuiltinNodeTypes.TOOL
|
||||
node_exec.execution_metadata = json.dumps(
|
||||
{
|
||||
"tool_info": {
|
||||
"provider_type": "plugin",
|
||||
"provider_id": "p1",
|
||||
}
|
||||
}
|
||||
)
|
||||
with mock.patch(
|
||||
"core.tools.tool_manager.ToolManager.get_tool_icon",
|
||||
return_value="https://example.com/icon.png",
|
||||
):
|
||||
assert node_exec.extras == {"icon": "https://example.com/icon.png"}
|
||||
|
||||
|
||||
class TestIsSystemVariableEditable:
|
||||
def test_is_system_variable(self):
|
||||
|
|
|
|||
|
|
@ -222,6 +222,37 @@ class TestAgentServiceGetAgentLogs:
|
|||
assert tool_calls[1]["tool_icon"] == ""
|
||||
mock_convert.assert_called_once()
|
||||
|
||||
def test_get_agent_logs_should_omit_tool_icon_when_fetch_raises(self) -> None:
|
||||
"""Uninstalled plugin or daemon down must not break agent log API."""
|
||||
agent_thought = _make_agent_thought()
|
||||
message = _make_message([agent_thought])
|
||||
conversation = _make_conversation(from_end_user_id="end-user-1", from_account_id=None)
|
||||
executor = MagicMock(spec=EndUser)
|
||||
executor.name = "End User"
|
||||
app_model_config = MagicMock()
|
||||
app_model_config.agent_mode_dict = {"strategy": "react"}
|
||||
app_model_config.to_dict.return_value = {"tools": []}
|
||||
app_model = _make_app_model(app_model_config)
|
||||
current_user = _make_current_user_account()
|
||||
agent_config = MagicMock()
|
||||
agent_config.tools = []
|
||||
|
||||
with (
|
||||
patch("services.agent_service.db") as mock_db,
|
||||
patch("services.agent_service.AgentConfigManager.convert", return_value=agent_config),
|
||||
patch(
|
||||
"services.agent_service.ToolManager.get_tool_icon",
|
||||
side_effect=RuntimeError("plugin daemon unavailable"),
|
||||
),
|
||||
patch("services.agent_service.current_user", current_user),
|
||||
):
|
||||
mock_db.session.query.side_effect = _build_query_side_effect(conversation, message, executor)
|
||||
result = AgentService.get_agent_logs(app_model, conversation.id, message.id)
|
||||
|
||||
tool_calls = result["iterations"][0]["tool_calls"]
|
||||
assert tool_calls[0]["tool_icon"] == ""
|
||||
assert tool_calls[1]["tool_icon"] == ""
|
||||
|
||||
def test_get_agent_logs_should_return_account_executor_when_no_end_user(self) -> None:
|
||||
"""Test agent logs fall back to account executor when end user is missing."""
|
||||
# Arrange
|
||||
|
|
|
|||
Loading…
Reference in New Issue