diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 99be960a20..850571c3f1 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -398,6 +398,8 @@ class ToolManager: user_id: str | None = None, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, variable_pool: "VariablePool | None" = None, + allow_file_parameters: bool = False, + use_default_for_missing_form_parameters: bool = False, ) -> Tool: """ get the agent tool runtime @@ -415,7 +417,12 @@ class ToolManager: runtime_parameters: dict[str, Any] = {} parameters = tool_entity.get_merged_runtime_parameters() runtime_parameters = cls._convert_tool_parameters_type( - parameters, variable_pool, agent_tool.tool_parameters, typ="agent" + parameters, + variable_pool, + agent_tool.tool_parameters, + typ="agent", + allow_file_parameters=allow_file_parameters, + use_default_for_missing_form_parameters=use_default_for_missing_form_parameters, ) # decrypt runtime parameters encryption_manager = ToolParameterConfigurationManager( @@ -1063,6 +1070,8 @@ class ToolManager: variable_pool: "VariablePool | None", tool_configurations: Mapping[str, Any], typ: Literal["agent", "workflow", "tool"] = "workflow", + allow_file_parameters: bool = False, + use_default_for_missing_form_parameters: bool = False, ) -> dict[str, Any]: """ Convert tool parameters type @@ -1081,6 +1090,7 @@ class ToolManager: } and parameter.required and typ == "agent" + and not allow_file_parameters ): raise ValueError(f"file type parameter {parameter.name} not supported in agent") # save tool parameter to tool entity memory @@ -1117,7 +1127,19 @@ class ToolManager: runtime_parameters[parameter.name] = parameter_value else: - value = parameter.init_frontend_parameter(tool_configurations.get(parameter.name)) + parameter_value = tool_configurations.get(parameter.name) + if use_default_for_missing_form_parameters and parameter_value is None: + if parameter.default is not None: + parameter_value = parameter.default + elif ( + parameter.required + and parameter.type == ToolParameter.ToolParameterType.SELECT + and parameter.options + ): + parameter_value = parameter.options[0].value + else: + continue + value = parameter.init_frontend_parameter(parameter_value) runtime_parameters[parameter.name] = value return runtime_parameters diff --git a/api/core/workflow/nodes/agent_v2/plugin_tools_builder.py b/api/core/workflow/nodes/agent_v2/plugin_tools_builder.py index ccf8f9fa17..80366cc519 100644 --- a/api/core/workflow/nodes/agent_v2/plugin_tools_builder.py +++ b/api/core/workflow/nodes/agent_v2/plugin_tools_builder.py @@ -42,6 +42,8 @@ class AgentToolRuntimeProvider(Protocol): user_id: str | None = None, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, variable_pool: Any | None = None, + allow_file_parameters: bool = False, + use_default_for_missing_form_parameters: bool = False, ) -> Tool: ... @@ -176,6 +178,8 @@ class WorkflowAgentPluginToolsBuilder: user_id=user_id, invoke_from=invoke_from, variable_pool=None, + allow_file_parameters=True, + use_default_for_missing_form_parameters=True, ) except ToolProviderNotFoundError as exc: raise WorkflowAgentPluginToolsBuildError( diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_plugin_tools_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_plugin_tools_builder.py index 1a2e09fd81..1ff87614b0 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_plugin_tools_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_plugin_tools_builder.py @@ -17,6 +17,7 @@ from core.tools.entities.tool_entities import ( ToolInvokeMessage, ToolParameter, ) +from core.tools.tool_manager import ToolManager from core.workflow.nodes.agent_v2.plugin_tools_builder import ( WorkflowAgentPluginToolsBuilder, WorkflowAgentPluginToolsBuildError, @@ -32,6 +33,8 @@ class FakeRuntimeProvider: self.tool = tool self.last_agent_tool: AgentToolEntity | None = None self.last_invoke_from: InvokeFrom | None = None + self.last_allow_file_parameters: bool | None = None + self.last_use_default_for_missing_form_parameters: bool | None = None def get_agent_tool_runtime( self, @@ -41,11 +44,25 @@ class FakeRuntimeProvider: user_id: str | None = None, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, variable_pool: Any | None = None, + allow_file_parameters: bool = False, + use_default_for_missing_form_parameters: bool = False, ) -> Tool: self.last_agent_tool = agent_tool self.last_invoke_from = invoke_from + self.last_allow_file_parameters = allow_file_parameters + self.last_use_default_for_missing_form_parameters = use_default_for_missing_form_parameters if isinstance(self.tool, Exception): raise self.tool + if self.tool.runtime is not None: + runtime_parameters = ToolManager._convert_tool_parameters_type( + self.tool.get_merged_runtime_parameters(), + variable_pool, + agent_tool.tool_parameters, + typ="agent", + allow_file_parameters=allow_file_parameters, + use_default_for_missing_form_parameters=use_default_for_missing_form_parameters, + ) + self.tool.runtime.runtime_parameters.update(runtime_parameters) return self.tool @@ -103,6 +120,67 @@ def _tool(*, runtime_parameters: dict[str, Any] | None = None) -> FakeTool: return FakeTool(entity=entity, runtime=runtime) +def _file_tool() -> FakeTool: + parameters = [ + ToolParameter( + name="audio_file", + label=I18nObject(en_US="Audio File"), + type=ToolParameter.ToolParameterType.FILE, + form=ToolParameter.ToolParameterForm.LLM, + required=True, + llm_description="The audio file to be converted.", + ) + ] + entity = ToolEntity( + identity=ToolIdentity( + author="langgenius", + name="asr", + label=I18nObject(en_US="Speech To Text"), + provider="audio", + ), + description=ToolDescription(human=I18nObject(en_US="Speech To Text"), llm="Convert audio file to text."), + parameters=parameters, + ) + runtime = ToolRuntime(tenant_id="tenant-1", user_id="user-1", credentials={}, runtime_parameters={}) + return FakeTool(entity=entity, runtime=runtime) + + +def _tts_tool() -> FakeTool: + parameters = [ + ToolParameter( + name="text", + label=I18nObject(en_US="Text"), + type=ToolParameter.ToolParameterType.STRING, + form=ToolParameter.ToolParameterForm.LLM, + required=True, + llm_description="The text to be converted.", + ), + ToolParameter( + name="model", + label=I18nObject(en_US="Model"), + type=ToolParameter.ToolParameterType.SELECT, + form=ToolParameter.ToolParameterForm.FORM, + required=True, + options=[ + {"value": "provider-a#model-a", "label": {"en_US": "model-a(provider-a)"}}, + {"value": "provider-b#model-b", "label": {"en_US": "model-b(provider-b)"}}, + ], + ), + ] + entity = ToolEntity( + identity=ToolIdentity( + author="langgenius", + name="tts", + label=I18nObject(en_US="Text To Speech"), + provider="audio", + ), + description=ToolDescription(human=I18nObject(en_US="Text To Speech"), llm="Convert text to audio file."), + parameters=parameters, + ) + runtime = ToolRuntime(tenant_id="tenant-1", user_id="user-1", credentials={}, runtime_parameters={}) + return FakeTool(entity=entity, runtime=runtime) + + def _build( builder: WorkflowAgentPluginToolsBuilder, tools: AgentSoulToolsConfig, @@ -157,6 +235,62 @@ def test_builds_dify_plugin_tools_layer_from_existing_tool_runtime(): assert runtime_provider.last_agent_tool.provider_type.value == "plugin" +def test_builds_dify_plugin_tool_with_file_llm_parameter(): + runtime_provider = FakeRuntimeProvider(_file_tool()) + builder = WorkflowAgentPluginToolsBuilder(tool_runtime_provider=runtime_provider) + tools = AgentSoulToolsConfig.model_validate( + { + "dify_tools": [ + { + "provider_id": "audio", + "provider_type": "builtin", + "tool_name": "asr", + "credential_type": "unauthorized", + } + ] + } + ) + + result = _build(builder, tools) + + assert result is not None + prepared = result.tools[0] + assert prepared.tool_name == "asr" + assert prepared.runtime_parameters == {} + assert prepared.parameters[0].name == "audio_file" + assert prepared.parameters[0].type == "file" + # The public Agent backend DTO carries non-scalar tool inputs in + # ``parameters``; legacy JSON schema generation omits file fields. + assert prepared.parameters_json_schema == {"type": "object", "properties": {}, "required": []} + assert runtime_provider.last_allow_file_parameters is True + assert runtime_provider.last_use_default_for_missing_form_parameters is True + + +def test_builds_dify_plugin_tool_with_missing_required_select_default(): + runtime_provider = FakeRuntimeProvider(_tts_tool()) + builder = WorkflowAgentPluginToolsBuilder(tool_runtime_provider=runtime_provider) + tools = AgentSoulToolsConfig.model_validate( + { + "dify_tools": [ + { + "provider_id": "audio", + "provider_type": "builtin", + "tool_name": "tts", + "credential_type": "unauthorized", + } + ] + } + ) + + result = _build(builder, tools) + + assert result is not None + prepared = result.tools[0] + assert prepared.tool_name == "tts" + assert prepared.runtime_parameters == {"model": "provider-a#model-a"} + assert runtime_provider.last_use_default_for_missing_form_parameters is True + + def test_rejects_duplicate_exposed_tool_names(): builder = WorkflowAgentPluginToolsBuilder(tool_runtime_provider=FakeRuntimeProvider(_tool())) tools = AgentSoulToolsConfig.model_validate(