mirror of
https://github.com/langgenius/dify.git
synced 2026-08-15 04:59:46 +08:00
fix(agent): normalize plugin provider identities (#40619)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
fb30dd2d58
commit
79b8d7a797
@ -30,6 +30,7 @@ from clients.agent_backend import (
|
||||
)
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom
|
||||
from core.plugin.provider_identity import normalize_plugin_daemon_provider_identity
|
||||
from core.workflow.nodes.agent_v2.dify_tools_builder import (
|
||||
WorkflowAgentDifyToolLayersBuilder,
|
||||
WorkflowAgentDifyToolsBuilder,
|
||||
@ -136,15 +137,16 @@ class AgentAppRuntimeRequestBuilder:
|
||||
append_runtime_warnings(metadata, config_warnings)
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
model_plugin_id, model_provider = normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID(agent_soul.model.model_provider),
|
||||
agent_soul.model.plugin_id,
|
||||
)
|
||||
|
||||
request = self._request_builder.build_for_agent_app(
|
||||
AgentBackendAgentAppRunInput(
|
||||
model=AgentBackendModelConfig(
|
||||
plugin_id=self._plugin_daemon_plugin_id(
|
||||
plugin_id=agent_soul.model.plugin_id,
|
||||
model_provider=agent_soul.model.model_provider,
|
||||
),
|
||||
model_provider=self._plugin_daemon_provider_name(agent_soul.model.model_provider),
|
||||
plugin_id=model_plugin_id,
|
||||
model_provider=model_provider,
|
||||
model=agent_soul.model.model,
|
||||
credentials=self._normalize_credentials(credentials),
|
||||
model_settings=agent_soul.model.model_settings.model_dump(mode="json", exclude_none=True),
|
||||
@ -220,20 +222,6 @@ class AgentAppRuntimeRequestBuilder:
|
||||
"agent_config_snapshot_id": context.agent_config_snapshot_id,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _plugin_daemon_plugin_id(*, plugin_id: str, model_provider: str) -> str:
|
||||
"""Return the transport plugin id expected by plugin-daemon headers."""
|
||||
if plugin_id.count("/") == 1:
|
||||
return plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
if plugin_id:
|
||||
return ModelProviderID(plugin_id).plugin_id
|
||||
return ModelProviderID(model_provider).plugin_id
|
||||
|
||||
@staticmethod
|
||||
def _plugin_daemon_provider_name(model_provider: str) -> str:
|
||||
"""Return the provider name expected by plugin-daemon dispatch payloads."""
|
||||
return ModelProviderID(model_provider).provider_name
|
||||
|
||||
@staticmethod
|
||||
def _normalize_credentials(credentials: Mapping[str, Any]) -> dict[str, str | int | float | bool | None]:
|
||||
normalized: dict[str, str | int | float | bool | None] = {}
|
||||
|
||||
26
api/core/plugin/provider_identity.py
Normal file
26
api/core/plugin/provider_identity.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Normalize persisted provider identities for plugin-daemon transport."""
|
||||
|
||||
from models.provider_ids import GenericProviderID
|
||||
|
||||
|
||||
def normalize_plugin_daemon_provider_identity(
|
||||
provider_id: GenericProviderID,
|
||||
plugin_id: str | None = None,
|
||||
) -> tuple[str, str]:
|
||||
"""Return the stable plugin ID and short provider name expected by plugin-daemon.
|
||||
|
||||
Explicit three-segment plugin IDs are legacy persisted provider IDs. They are
|
||||
normalized through the concrete provider-ID type to preserve model and tool aliases.
|
||||
"""
|
||||
if plugin_id:
|
||||
plugin_id_parts = plugin_id.split("/")
|
||||
if len(plugin_id_parts) == 3:
|
||||
normalized_plugin_id = type(provider_id)(plugin_id).plugin_id
|
||||
elif len(plugin_id_parts) == 2:
|
||||
normalized_plugin_id = plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
else:
|
||||
raise ValueError(f"Invalid plugin id {plugin_id}")
|
||||
else:
|
||||
normalized_plugin_id = provider_id.plugin_id
|
||||
normalized_provider_id = GenericProviderID(f"{normalized_plugin_id}/{provider_id.provider_name}")
|
||||
return normalized_provider_id.plugin_id, provider_id.provider_name
|
||||
@ -18,6 +18,7 @@ from sqlalchemy import select
|
||||
|
||||
from core.agent.entities import AgentToolEntity
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.plugin.provider_identity import normalize_plugin_daemon_provider_identity
|
||||
from core.tools.__base.tool import Tool
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from core.tools.errors import ToolProviderCredentialValidationError, ToolProviderNotFoundError
|
||||
@ -389,8 +390,8 @@ class WorkflowAgentDifyToolsBuilder:
|
||||
f"Dify Tool {tool_config.tool_name!r} has no runtime.",
|
||||
)
|
||||
|
||||
provider_id = self._provider_id(tool_config)
|
||||
plugin_id, provider = self._plugin_provider(tool_config, provider_id)
|
||||
provider_id = ToolProviderID(self._provider_id(tool_config))
|
||||
plugin_id, provider = normalize_plugin_daemon_provider_identity(provider_id, tool_config.plugin_id)
|
||||
parameters = self._prepared_parameters(tool_runtime)
|
||||
runtime_parameters = self._runtime_parameters(tool_runtime, parameters)
|
||||
description = self._description(tool_config, tool_runtime)
|
||||
@ -427,13 +428,6 @@ class WorkflowAgentDifyToolsBuilder:
|
||||
parameters_json_schema=tool_runtime.get_llm_parameters_json_schema(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _plugin_provider(tool_config: AgentSoulDifyToolConfig, provider_id: str) -> tuple[str, str]:
|
||||
if tool_config.plugin_id and tool_config.provider:
|
||||
return tool_config.plugin_id, tool_config.provider
|
||||
provider_id_entity = ToolProviderID(provider_id)
|
||||
return provider_id_entity.plugin_id, provider_id_entity.provider_name
|
||||
|
||||
@staticmethod
|
||||
def _credential_type(
|
||||
tool_config: AgentSoulDifyToolConfig,
|
||||
|
||||
@ -47,6 +47,7 @@ from clients.agent_backend import (
|
||||
)
|
||||
from configs import dify_config
|
||||
from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom
|
||||
from core.plugin.provider_identity import normalize_plugin_daemon_provider_identity
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text, get_system_value
|
||||
from graphon.file import File, FileTransferMethod
|
||||
from graphon.variables.segments import Segment
|
||||
@ -217,15 +218,16 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
soul_prompt_resolver = build_config_aware_soul_mention_resolver(agent_soul)
|
||||
soul_prompt = expand_prompt_mentions(agent_soul.prompt.system_prompt, soul_prompt_resolver).strip()
|
||||
knowledge_config = build_knowledge_layer_config(agent_soul)
|
||||
model_plugin_id, model_provider = normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID(agent_soul.model.model_provider),
|
||||
agent_soul.model.plugin_id,
|
||||
)
|
||||
|
||||
request = self._request_builder.build_for_workflow_node(
|
||||
AgentBackendWorkflowNodeRunInput(
|
||||
model=AgentBackendModelConfig(
|
||||
plugin_id=self._plugin_daemon_plugin_id(
|
||||
plugin_id=agent_soul.model.plugin_id,
|
||||
model_provider=agent_soul.model.model_provider,
|
||||
),
|
||||
model_provider=self._plugin_daemon_provider_name(agent_soul.model.model_provider),
|
||||
plugin_id=model_plugin_id,
|
||||
model_provider=model_provider,
|
||||
model=agent_soul.model.model,
|
||||
credentials=self._normalize_credentials(credentials),
|
||||
model_settings=agent_soul.model.model_settings.model_dump(mode="json", exclude_none=True),
|
||||
@ -305,20 +307,6 @@ class WorkflowAgentRuntimeRequestBuilder:
|
||||
return "single_step"
|
||||
return "workflow_run"
|
||||
|
||||
@staticmethod
|
||||
def _plugin_daemon_plugin_id(*, plugin_id: str, model_provider: str) -> str:
|
||||
"""Return the transport plugin id expected by plugin-daemon headers."""
|
||||
if plugin_id.count("/") == 1:
|
||||
return plugin_id.split(":", 1)[0].split("@", 1)[0]
|
||||
if plugin_id:
|
||||
return ModelProviderID(plugin_id).plugin_id
|
||||
return ModelProviderID(model_provider).plugin_id
|
||||
|
||||
@staticmethod
|
||||
def _plugin_daemon_provider_name(model_provider: str) -> str:
|
||||
"""Return the provider name expected by plugin-daemon dispatch payloads."""
|
||||
return ModelProviderID(model_provider).provider_name
|
||||
|
||||
@staticmethod
|
||||
def _idempotency_key(context: WorkflowAgentRuntimeBuildContext) -> str:
|
||||
# Stage 4 §7 / D-4: retries get distinct keys (``...:retry-{attempt}``) so
|
||||
|
||||
@ -325,6 +325,20 @@ class TestAgentAppRuntimeRequestBuilder:
|
||||
assert llm.config.plugin_id == "langgenius/openai"
|
||||
assert llm.config.model_provider == "openai"
|
||||
|
||||
def test_build_normalizes_legacy_three_segment_model_plugin_id(self):
|
||||
soul = _soul_with_model()
|
||||
soul.model.plugin_id = "langgenius/openai/openai"
|
||||
builder = AgentAppRuntimeRequestBuilder(
|
||||
credentials_provider=_FakeCredentialsProvider(),
|
||||
dify_tools_builder=_NoToolsBuilder(), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
result = builder.build(_ctx(soul))
|
||||
|
||||
llm = next(layer for layer in result.request.composition.layers if layer.name == "llm")
|
||||
assert llm.config.plugin_id == "langgenius/openai"
|
||||
assert llm.config.model_provider == "openai"
|
||||
|
||||
def test_build_maps_agent_soul_knowledge_to_knowledge_layer(self):
|
||||
soul = AgentSoulConfig.model_validate(
|
||||
{
|
||||
|
||||
59
api/tests/unit_tests/core/plugin/test_provider_identity.py
Normal file
59
api/tests/unit_tests/core/plugin/test_provider_identity.py
Normal file
@ -0,0 +1,59 @@
|
||||
import pytest
|
||||
|
||||
from core.plugin.provider_identity import normalize_plugin_daemon_provider_identity
|
||||
from models.provider_ids import ModelProviderID, ToolProviderID
|
||||
|
||||
|
||||
def test_normalizes_stable_explicit_plugin_id() -> None:
|
||||
assert normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID("google"),
|
||||
"langgenius/google",
|
||||
) == ("langgenius/google", "google")
|
||||
|
||||
|
||||
def test_normalizes_versioned_explicit_plugin_unique_identifier() -> None:
|
||||
assert normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID("google"),
|
||||
"langgenius/google:0.4.2@checksum",
|
||||
) == ("langgenius/google", "google")
|
||||
|
||||
|
||||
def test_normalizes_legacy_explicit_provider_id_with_typed_alias_rules() -> None:
|
||||
assert normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID("langgenius/openai/openai"),
|
||||
"langgenius/openai/openai",
|
||||
) == ("langgenius/openai", "openai")
|
||||
assert normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID("langgenius/google/google"),
|
||||
"langgenius/google/google",
|
||||
) == ("langgenius/gemini", "google")
|
||||
assert normalize_plugin_daemon_provider_identity(
|
||||
ToolProviderID("langgenius/jina/jina"),
|
||||
"langgenius/jina/jina",
|
||||
) == ("langgenius/jina_tool", "jina")
|
||||
|
||||
|
||||
def test_rejects_malformed_explicit_plugin_id() -> None:
|
||||
with pytest.raises(ValueError, match="Invalid plugin id"):
|
||||
normalize_plugin_daemon_provider_identity(
|
||||
ModelProviderID("langgenius/openai/openai"),
|
||||
"langgenius/openai:0.4.2/extra",
|
||||
)
|
||||
|
||||
|
||||
def test_derives_plugin_id_when_explicit_plugin_id_is_absent() -> None:
|
||||
assert normalize_plugin_daemon_provider_identity(ToolProviderID("langgenius/google/google")) == (
|
||||
"langgenius/google",
|
||||
"google",
|
||||
)
|
||||
|
||||
|
||||
def test_preserves_typed_provider_alias_rules() -> None:
|
||||
assert normalize_plugin_daemon_provider_identity(ModelProviderID("google")) == (
|
||||
"langgenius/gemini",
|
||||
"google",
|
||||
)
|
||||
assert normalize_plugin_daemon_provider_identity(ToolProviderID("jina")) == (
|
||||
"langgenius/jina_tool",
|
||||
"jina",
|
||||
)
|
||||
@ -262,6 +262,32 @@ def test_builds_dify_plugin_tools_layer_from_existing_tool_runtime():
|
||||
assert runtime_provider.last_agent_tool.provider_type.value == "plugin"
|
||||
|
||||
|
||||
def test_normalizes_fully_qualified_builtin_plugin_provider_for_daemon_transport():
|
||||
runtime_provider = FakeRuntimeProvider(_tool())
|
||||
builder = WorkflowAgentDifyToolsBuilder(tool_runtime_provider=runtime_provider)
|
||||
tools = AgentSoulToolsConfig.model_validate(
|
||||
{
|
||||
"dify_tools": [
|
||||
{
|
||||
"plugin_id": "langgenius/google",
|
||||
"provider_id": "langgenius/google/google",
|
||||
"provider": "langgenius/google/google",
|
||||
"provider_type": "builtin",
|
||||
"tool_name": "google_search",
|
||||
"credential_type": "unauthorized",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
result = _build(builder, tools)
|
||||
|
||||
assert result is not None
|
||||
prepared = result.tools[0]
|
||||
assert prepared.plugin_id == "langgenius/google"
|
||||
assert prepared.provider == "google"
|
||||
|
||||
|
||||
def test_builds_core_tool_with_file_llm_parameter():
|
||||
runtime_provider = FakeRuntimeProvider(_file_tool())
|
||||
builder = WorkflowAgentDifyToolsBuilder(tool_runtime_provider=runtime_provider)
|
||||
|
||||
@ -343,12 +343,22 @@ def test_build_includes_core_tools_layer_returned_by_injected_builder():
|
||||
assert DIFY_PLUGIN_TOOLS_LAYER_ID not in layers
|
||||
|
||||
|
||||
def test_normalizes_langgenius_model_provider_for_agent_backend_transport():
|
||||
@pytest.mark.parametrize(
|
||||
"plugin_id",
|
||||
[
|
||||
pytest.param(
|
||||
"langgenius/openai:0.4.2@21195ee1321849e0a7d4b3f6b2fd8c2be23ea6c7182e1b444ecc4c1711b52468",
|
||||
id="marketplace-unique-identifier",
|
||||
),
|
||||
pytest.param("langgenius/openai/openai", id="legacy-three-segment-provider-id"),
|
||||
],
|
||||
)
|
||||
def test_normalizes_langgenius_model_provider_for_agent_backend_transport(plugin_id: str):
|
||||
context = _context()
|
||||
context.snapshot.config_snapshot = AgentSoulConfig(
|
||||
prompt={"system_prompt": "You are careful."},
|
||||
model=AgentSoulModelConfig(
|
||||
plugin_id="langgenius/openai:0.4.2@21195ee1321849e0a7d4b3f6b2fd8c2be23ea6c7182e1b444ecc4c1711b52468",
|
||||
plugin_id=plugin_id,
|
||||
model_provider="langgenius/openai/openai",
|
||||
model="gpt-test",
|
||||
),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user