diff --git a/dify-agent/src/dify_agent/runtime/runner.py b/dify-agent/src/dify_agent/runtime/runner.py index d10b1843e9a..65946736f02 100644 --- a/dify-agent/src/dify_agent/runtime/runner.py +++ b/dify-agent/src/dify_agent/runtime/runner.py @@ -38,6 +38,7 @@ from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults from agenton.compositor import CompositorSessionSnapshot, LayerProviderInput from agenton.layers.types import PydanticAITool from dify_agent.layers.ask_human.layer import get_ask_human_layer, validate_ask_human_layer_composition +from dify_agent.layers.dify_core_tools.layer import DifyCoreToolsLayer from dify_agent.layers.dify_plugin.llm_layer import DifyPluginLLMLayer from dify_agent.layers.dify_plugin.tools_layer import DifyPluginToolsLayer from dify_agent.layers.knowledge.layer import DifyKnowledgeBaseLayer @@ -285,6 +286,8 @@ async def _resolve_run_tools( layer = slot.layer if isinstance(layer, DifyPluginToolsLayer): resolved_tools.extend(await layer.get_tools(http_client=plugin_daemon_http_client)) + if isinstance(layer, DifyCoreToolsLayer): + resolved_tools.extend(await layer.get_tools(http_client=dify_api_http_client)) if isinstance(layer, DifyKnowledgeBaseLayer): resolved_tools.extend(await layer.get_tools(http_client=dify_api_http_client)) _validate_unique_tool_names(resolved_tools) diff --git a/dify-agent/tests/local/dify_agent/runtime/test_runner.py b/dify-agent/tests/local/dify_agent/runtime/test_runner.py index af95aaf535c..b9f5b5bf467 100644 --- a/dify-agent/tests/local/dify_agent/runtime/test_runner.py +++ b/dify-agent/tests/local/dify_agent/runtime/test_runner.py @@ -42,6 +42,12 @@ from dify_agent.layers.dify_plugin.configs import ( ) from dify_agent.layers.dify_plugin.llm_layer import DifyPluginLLMLayer from dify_agent.layers.dify_plugin.tools_layer import DifyPluginToolsLayer +from dify_agent.layers.dify_core_tools.configs import ( + DIFY_CORE_TOOLS_LAYER_TYPE_ID, + DifyCoreToolConfig, + DifyCoreToolsLayerConfig, +) +from dify_agent.layers.dify_core_tools.layer import DifyCoreToolsLayer from dify_agent.layers.knowledge.configs import DIFY_KNOWLEDGE_BASE_LAYER_TYPE_ID, DifyKnowledgeBaseLayerConfig from dify_agent.layers.knowledge.layer import DifyKnowledgeBaseLayer from dify_agent.layers.output import DIFY_OUTPUT_LAYER_TYPE_ID, DifyOutputLayerConfig @@ -1093,6 +1099,111 @@ def test_runner_passes_dynamic_dify_knowledge_tools_to_agent(monkeypatch: pytest assert [tool.name for tool in seen_tools] == ["knowledge_base_search"] +def test_runner_passes_dynamic_dify_core_tools_to_agent(monkeypatch: pytest.MonkeyPatch) -> None: + seen_tools: list[Tool[object]] = [] + + async def core_tool() -> str: + return "core" + + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="done") # pyright: ignore[reportReturnType] + + async def fake_get_tools(self: DifyCoreToolsLayer, *, http_client: httpx.AsyncClient) -> list[Tool[object]]: + assert self.config.tools[0].provider_type == "builtin" + assert self.config.tools[0].tool_name == "draft_message" + assert http_client.headers.get("X-Test-Client") == "dify-api" + return [Tool(core_tool, name="draft_message")] + + class FakeResult: + output: str = "done" + + def new_messages(self) -> list[ModelMessage]: + return [] + + class FakeAgent: + async def run(self, *_args: object, **_kwargs: object) -> FakeResult: + return FakeResult() + + def fake_create_agent(model: object, *, tools: list[Tool[object]], output_type: object) -> FakeAgent: + del model, output_type + seen_tools.extend(tools) + return FakeAgent() + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr(DifyCoreToolsLayer, "get_tools", fake_get_tools) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", fake_create_agent) + + request = CreateRunRequest( + composition=RunComposition( + layers=[ + RunLayerSpec( + name="prompt", + type="plain.prompt", + config=PromptLayerConfig(prefix="system", user="hello"), + ), + RunLayerSpec( + name="execution_context", + type=DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID, + config=DifyExecutionContextLayerConfig( + tenant_id="tenant-1", + user_id="user-1", + user_from="account", + app_id="app-1", + agent_mode="workflow_run", + invoke_from="service-api", + ), + ), + RunLayerSpec( + name=DIFY_AGENT_MODEL_LAYER_ID, + type="dify.plugin.llm", + deps={"execution_context": "execution_context"}, + config=DifyPluginLLMLayerConfig( + plugin_id="langgenius/openai", + model_provider="openai", + model="demo-model", + credentials={"api_key": "secret"}, + ), + ), + RunLayerSpec( + name="core-tools", + type=DIFY_CORE_TOOLS_LAYER_TYPE_ID, + deps={"execution_context": "execution_context"}, + config=DifyCoreToolsLayerConfig( + tools=[ + DifyCoreToolConfig( + provider_type="builtin", + provider_id="langgenius/dify-gmail/dify-gmail", + tool_name="draft_message", + credential_id="credential-1", + parameters_json_schema={"type": "object", "properties": {}, "required": []}, + ) + ] + ), + ), + ] + ) + ) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with ( + httpx.AsyncClient() as plugin_client, + httpx.AsyncClient(headers={"X-Test-Client": "dify-api"}) as dify_api_client, + ): + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-core-tools", + plugin_daemon_http_client=plugin_client, + dify_api_http_client=dify_api_client, + ).run() + + asyncio.run(scenario()) + + assert [tool.name for tool in seen_tools] == ["draft_message"] + + def test_runner_rejects_duplicate_tool_names_across_dynamic_tool_layers( monkeypatch: pytest.MonkeyPatch, ) -> None: