mirror of
https://github.com/langgenius/dify.git
synced 2026-06-13 11:38:28 +08:00
feat(api): [§tool:<provider>/*§] selects all tools of one provider — ENG-616
Product clarification: "all tools" must be per-provider ("all DuckDuckGo
tools", like adding a whole MCP server), not agent-global. Replaces the
global [§tool:*§] sentinel from 0d872cfaad:
- mention: [§tool:<provider>/*§] = every tool of that provider;
[§tool:<provider>/<tool_name>§] = one tool (unchanged). The /* form
resolves against a provider-level config entry and expands to
"all <provider> tools"; single-tool mentions also resolve through a
provider-level entry (the tool is offered via "all"). Longest-prefix
matching guards plugin_id/provider alias overlaps.
- config: AgentSoulDifyToolConfig.tool_name optional again — omitted =
provider-level entry (credential_ref applies to all tools).
- candidates dify_tools: two granularities per provider — one
{granularity:"provider", id:"<provider>/*", tools_count} entry plus one
{granularity:"tool", id:"<provider>/<tool_name>"} per tool; ids double
as mention ids.
- runtime: provider-level entries expand to every tool the provider
declares (injectable ProviderToolsLister, default via ToolManager);
explicit per-tool entries of the same provider win over the expansion;
unknown/empty providers map to agent_tool_declaration_not_found.
- dangling [§tool:<provider>/*§] without a provider-level entry warns
mention_target_missing like any other mention (no special-casing).
cli_tool stable-id behavior from 0d872cfaad is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -45,11 +45,27 @@ class AgentToolRuntimeProvider(Protocol):
|
||||
) -> Tool: ...
|
||||
|
||||
|
||||
class ProviderToolsLister(Protocol):
|
||||
def __call__(self, *, tenant_id: str, provider_id: str) -> list[str]: ...
|
||||
|
||||
|
||||
def _list_provider_tool_names(*, tenant_id: str, provider_id: str) -> list[str]:
|
||||
"""Tool names a provider currently declares (provider-level config entries)."""
|
||||
provider = ToolManager.get_builtin_provider(provider_id, tenant_id)
|
||||
return [tool.entity.identity.name for tool in provider.get_tools() or []]
|
||||
|
||||
|
||||
class WorkflowAgentPluginToolsBuilder:
|
||||
"""Prepare Agent Soul Dify Plugin Tools for the public Agent backend DTO."""
|
||||
|
||||
def __init__(self, *, tool_runtime_provider: AgentToolRuntimeProvider | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tool_runtime_provider: AgentToolRuntimeProvider | None = None,
|
||||
provider_tools_lister: ProviderToolsLister | None = None,
|
||||
) -> None:
|
||||
self._tool_runtime_provider = tool_runtime_provider or ToolManager
|
||||
self._provider_tools_lister = provider_tools_lister or _list_provider_tool_names
|
||||
|
||||
def build(
|
||||
self,
|
||||
@ -73,7 +89,7 @@ class WorkflowAgentPluginToolsBuilder:
|
||||
|
||||
prepared: list[DifyPluginToolConfig] = []
|
||||
seen_names: set[str] = set()
|
||||
for tool_config in enabled_tools:
|
||||
for tool_config in self._expand_provider_entries(tenant_id=tenant_id, enabled_tools=enabled_tools):
|
||||
agent_tool = self._to_agent_tool_entity(tool_config)
|
||||
tool_runtime = self._fetch_tool_runtime(
|
||||
tenant_id=tenant_id,
|
||||
@ -96,6 +112,48 @@ class WorkflowAgentPluginToolsBuilder:
|
||||
|
||||
return DifyPluginToolsLayerConfig(tools=prepared)
|
||||
|
||||
def _expand_provider_entries(
|
||||
self,
|
||||
*,
|
||||
tenant_id: str,
|
||||
enabled_tools: list[AgentSoulDifyToolConfig],
|
||||
) -> list[AgentSoulDifyToolConfig]:
|
||||
"""Expand provider-level entries (``tool_name`` omitted = all tools).
|
||||
|
||||
An explicit per-tool entry of the same provider wins over the expansion
|
||||
(it may carry its own ``runtime_parameters``); expanded clones share the
|
||||
provider entry's ``credential_ref`` and start with default parameters.
|
||||
"""
|
||||
explicit_by_provider: dict[str, set[str]] = {}
|
||||
for tool_config in enabled_tools:
|
||||
if tool_config.tool_name is not None:
|
||||
explicit_by_provider.setdefault(self._provider_id(tool_config), set()).add(tool_config.tool_name)
|
||||
|
||||
expanded: list[AgentSoulDifyToolConfig] = []
|
||||
for tool_config in enabled_tools:
|
||||
if tool_config.tool_name is not None:
|
||||
expanded.append(tool_config)
|
||||
continue
|
||||
provider_id = self._provider_id(tool_config)
|
||||
try:
|
||||
tool_names = self._provider_tools_lister(tenant_id=tenant_id, provider_id=provider_id)
|
||||
except ToolProviderNotFoundError as exc:
|
||||
raise WorkflowAgentPluginToolsBuildError(
|
||||
"agent_tool_declaration_not_found",
|
||||
f"Dify Plugin Tool provider {provider_id!r} declaration not found: {exc}",
|
||||
) from exc
|
||||
if not tool_names:
|
||||
raise WorkflowAgentPluginToolsBuildError(
|
||||
"agent_tool_declaration_not_found",
|
||||
f"Dify Plugin Tool provider {provider_id!r} declares no tools.",
|
||||
)
|
||||
already_explicit = explicit_by_provider.get(provider_id, set())
|
||||
for tool_name in tool_names:
|
||||
if tool_name in already_explicit:
|
||||
continue
|
||||
expanded.append(tool_config.model_copy(update={"tool_name": tool_name, "runtime_parameters": {}}))
|
||||
return expanded
|
||||
|
||||
def _fetch_tool_runtime(
|
||||
self,
|
||||
*,
|
||||
@ -141,6 +199,8 @@ class WorkflowAgentPluginToolsBuilder:
|
||||
|
||||
@staticmethod
|
||||
def _to_agent_tool_entity(tool_config: AgentSoulDifyToolConfig) -> AgentToolEntity:
|
||||
# Provider-level entries are expanded into per-tool clones before this point.
|
||||
assert tool_config.tool_name is not None
|
||||
return AgentToolEntity(
|
||||
provider_type=ToolProviderType.value_of(tool_config.provider_type),
|
||||
provider_id=WorkflowAgentPluginToolsBuilder._provider_id(tool_config),
|
||||
@ -160,7 +220,9 @@ class WorkflowAgentPluginToolsBuilder:
|
||||
@staticmethod
|
||||
def _exposed_tool_name(tool_config: AgentSoulDifyToolConfig) -> str:
|
||||
# Stage 3.1 decision: no user rename yet. Keep the model-visible tool
|
||||
# name aligned with the plugin declaration identity.
|
||||
# name aligned with the plugin declaration identity. Provider-level
|
||||
# entries are expanded into per-tool clones before this point.
|
||||
assert tool_config.tool_name is not None
|
||||
return tool_config.tool_name
|
||||
|
||||
def _to_backend_tool_config(
|
||||
@ -190,11 +252,11 @@ class WorkflowAgentPluginToolsBuilder:
|
||||
return DifyPluginToolConfig(
|
||||
plugin_id=plugin_id,
|
||||
provider=provider,
|
||||
tool_name=tool_config.tool_name,
|
||||
tool_name=exposed_name,
|
||||
credential_type=self._credential_type(tool_config, runtime.credentials),
|
||||
name=exposed_name,
|
||||
description=description,
|
||||
credentials=self._normalize_credentials(runtime.credentials, tool_name=tool_config.tool_name),
|
||||
credentials=self._normalize_credentials(runtime.credentials, tool_name=exposed_name),
|
||||
runtime_parameters=runtime_parameters,
|
||||
parameters=parameters,
|
||||
parameters_json_schema=tool_runtime.get_llm_parameters_json_schema(),
|
||||
|
||||
@ -340,7 +340,11 @@ class AgentSoulDifyToolConfig(BaseModel):
|
||||
provider_id: str | None = Field(default=None, max_length=255)
|
||||
plugin_id: str | None = Field(default=None, max_length=255)
|
||||
provider: str | None = Field(default=None, max_length=255)
|
||||
tool_name: str = Field(min_length=1, max_length=255)
|
||||
# ``None`` = provider-level entry selecting ALL tools of the provider (a
|
||||
# provider hosts many tools, like an MCP server). The runtime expands the
|
||||
# entry into every tool the provider currently declares; ``credential_ref``
|
||||
# applies to all of them. Mention form: ``[§tool:<provider>/*§]``.
|
||||
tool_name: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
credential_type: Literal["api-key", "oauth2", "unauthorized"] = "api-key"
|
||||
credential_ref: AgentSoulDifyToolCredentialRef | None = None
|
||||
# Reserved for a future user-rename UX. Accepted but currently rejected at
|
||||
|
||||
@ -457,10 +457,28 @@ class AgentComposerService:
|
||||
return []
|
||||
tools: list[dict[str, Any]] = []
|
||||
for provider in providers:
|
||||
for tool in provider.tools or []:
|
||||
provider_tools = provider.tools or []
|
||||
# Provider-level entry first: selecting it means "all tools of this
|
||||
# provider" (a provider hosts many tools, like an MCP server). Its
|
||||
# ``id`` is also the mention id (``[§tool:<provider>/*§]``); the
|
||||
# write-back is one ``tools.dify_tools`` entry with ``tool_name``
|
||||
# omitted.
|
||||
tools.append(
|
||||
{
|
||||
"id": f"{provider.name}/*",
|
||||
"granularity": "provider",
|
||||
"name": provider.label.en_US if provider.label else provider.name,
|
||||
"description": provider.description.en_US if provider.description else None,
|
||||
"provider": provider.name,
|
||||
"plugin_id": provider.plugin_id or None,
|
||||
"tools_count": len(provider_tools),
|
||||
}
|
||||
)
|
||||
for tool in provider_tools:
|
||||
tools.append(
|
||||
{
|
||||
"id": f"{provider.name}/{tool.name}",
|
||||
"granularity": "tool",
|
||||
"name": tool.name,
|
||||
"description": tool.label.en_US if tool.label else tool.name,
|
||||
"provider": provider.name,
|
||||
|
||||
@ -57,12 +57,13 @@ _RESIDUAL_MENTION_PATTERN = re.compile(r"\[§([A-Za-z_][A-Za-z0-9_]*:[^§]*?)§\
|
||||
MAX_MENTIONS_PER_PROMPT = 200
|
||||
MAX_MENTION_FIELD_LENGTH = 255
|
||||
|
||||
# Reserved ``tool`` mention id meaning "every tool this agent has" (single tools use
|
||||
# ``<provider>/<tool_name>``, so ``*`` can never collide with a real tool id). The
|
||||
# menu row is a static frontend entry; selecting it writes nothing back to config,
|
||||
# and validation always resolves it, so it can never dangle.
|
||||
ALL_TOOLS_MENTION_ID = "*"
|
||||
_ALL_TOOLS_MENTION_TEXT = "all available tools"
|
||||
# Reserved ``tool`` mention id suffix: ``<provider>/*`` means "every tool of this
|
||||
# provider" (a provider hosts many tools, like an MCP server). Single tools use
|
||||
# ``<provider>/<tool_name>``, so ``*`` can never collide with a real tool name.
|
||||
# The mention points at a provider-level config entry (``tool_name`` omitted in
|
||||
# ``tools.dify_tools``); the runtime expands that entry into all of the
|
||||
# provider's tools.
|
||||
ALL_PROVIDER_TOOLS_SUFFIX = "*"
|
||||
|
||||
# Per-surface allowlists (design §2.4): the soul prompt may only reference
|
||||
# soul-owned entities; the workflow job prompt may only reference run-scoped ones.
|
||||
@ -184,14 +185,26 @@ def build_soul_mention_resolver(agent_soul: AgentSoulConfig) -> MentionResolver:
|
||||
if mention.ref_id in (file.id, file.name):
|
||||
return file.name or file.id
|
||||
case MentionKind.TOOL:
|
||||
if mention.ref_id == ALL_TOOLS_MENTION_ID:
|
||||
return _ALL_TOOLS_MENTION_TEXT
|
||||
for tool in agent_soul.tools.dify_tools:
|
||||
aliases = {tool.tool_name} | {
|
||||
f"{prefix}/{tool.tool_name}"
|
||||
for prefix in (tool.provider, tool.provider_id, tool.plugin_id)
|
||||
if prefix
|
||||
}
|
||||
prefixes = {prefix for prefix in (tool.provider, tool.provider_id, tool.plugin_id) if prefix}
|
||||
if tool.plugin_id and tool.provider:
|
||||
prefixes.add(f"{tool.plugin_id}/{tool.provider}")
|
||||
if tool.tool_name is None:
|
||||
# Provider-level entry = all tools of this provider.
|
||||
# ``[§tool:<provider>/*§]`` names the whole provider;
|
||||
# ``[§tool:<provider>/<tool>§]`` names one tool offered
|
||||
# through it.
|
||||
display = tool.provider or tool.provider_id or tool.plugin_id
|
||||
if any(mention.ref_id == f"{prefix}/{ALL_PROVIDER_TOOLS_SUFFIX}" for prefix in prefixes):
|
||||
return f"all {display} tools"
|
||||
# longest prefix first — shorter prefixes can be proper
|
||||
# prefixes of longer ones and would mis-split the ref.
|
||||
for prefix in sorted(prefixes, key=len, reverse=True):
|
||||
single = mention.ref_id.removeprefix(f"{prefix}/")
|
||||
if single != mention.ref_id and single and "/" not in single:
|
||||
return single
|
||||
continue
|
||||
aliases = {tool.tool_name} | {f"{prefix}/{tool.tool_name}" for prefix in prefixes}
|
||||
if mention.ref_id in aliases:
|
||||
return tool.name or tool.tool_name
|
||||
case MentionKind.CLI_TOOL:
|
||||
@ -258,7 +271,7 @@ def _selector_from_ref(ref: WorkflowPreviousNodeOutputRef) -> tuple[str, str] |
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ALL_TOOLS_MENTION_ID",
|
||||
"ALL_PROVIDER_TOOLS_SUFFIX",
|
||||
"MAX_MENTIONS_PER_PROMPT",
|
||||
"MAX_MENTION_FIELD_LENGTH",
|
||||
"MENTION_PATTERN",
|
||||
|
||||
@ -437,3 +437,84 @@ def test_legacy_provider_name_and_tool_parameters_normalized():
|
||||
assert tool.runtime_parameters == {"region": "us"}
|
||||
assert tool.credential_ref is not None
|
||||
assert tool.credential_ref.id == "credential-1"
|
||||
|
||||
|
||||
# ── provider-level entries (tool_name omitted = all tools of the provider) ───
|
||||
|
||||
|
||||
def test_provider_level_entry_expands_to_all_tools():
|
||||
runtime_provider = FakeRuntimeProvider(_tool())
|
||||
listed: list[tuple[str, str]] = []
|
||||
|
||||
def lister(*, tenant_id: str, provider_id: str) -> list[str]:
|
||||
listed.append((tenant_id, provider_id))
|
||||
return ["search", "image_search"]
|
||||
|
||||
builder = WorkflowAgentPluginToolsBuilder(tool_runtime_provider=runtime_provider, provider_tools_lister=lister)
|
||||
tools = AgentSoulToolsConfig.model_validate(
|
||||
{"dify_tools": [{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"}]}
|
||||
)
|
||||
|
||||
result = _build(builder, tools)
|
||||
|
||||
assert result is not None
|
||||
assert [tool.tool_name for tool in result.tools] == ["search", "image_search"]
|
||||
assert listed == [("tenant-1", "langgenius/search/search")]
|
||||
|
||||
|
||||
def test_explicit_tool_entry_wins_over_provider_expansion():
|
||||
builder = WorkflowAgentPluginToolsBuilder(
|
||||
tool_runtime_provider=FakeRuntimeProvider(_tool()),
|
||||
provider_tools_lister=lambda *, tenant_id, provider_id: ["search", "image_search"],
|
||||
)
|
||||
tools = AgentSoulToolsConfig.model_validate(
|
||||
{
|
||||
"dify_tools": [
|
||||
{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"},
|
||||
{
|
||||
"provider_id": "langgenius/search/search",
|
||||
"tool_name": "search",
|
||||
"credential_type": "unauthorized",
|
||||
"runtime_parameters": {"region": "eu"},
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
result = _build(builder, tools)
|
||||
|
||||
# the expansion skips "search" (explicit entry wins); no duplicate error
|
||||
assert result is not None
|
||||
assert sorted(tool.tool_name for tool in result.tools) == ["image_search", "search"]
|
||||
|
||||
|
||||
def test_provider_level_entry_with_no_tools_maps_to_declaration_not_found():
|
||||
builder = WorkflowAgentPluginToolsBuilder(
|
||||
tool_runtime_provider=FakeRuntimeProvider(_tool()),
|
||||
provider_tools_lister=lambda *, tenant_id, provider_id: [],
|
||||
)
|
||||
tools = AgentSoulToolsConfig.model_validate(
|
||||
{"dify_tools": [{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"}]}
|
||||
)
|
||||
|
||||
with pytest.raises(WorkflowAgentPluginToolsBuildError) as exc_info:
|
||||
_build(builder, tools)
|
||||
assert exc_info.value.error_code == "agent_tool_declaration_not_found"
|
||||
|
||||
|
||||
def test_provider_level_entry_unknown_provider_maps_to_declaration_not_found():
|
||||
from core.tools.errors import ToolProviderNotFoundError
|
||||
|
||||
def lister(*, tenant_id: str, provider_id: str) -> list[str]:
|
||||
raise ToolProviderNotFoundError("provider gone")
|
||||
|
||||
builder = WorkflowAgentPluginToolsBuilder(
|
||||
tool_runtime_provider=FakeRuntimeProvider(_tool()), provider_tools_lister=lister
|
||||
)
|
||||
tools = AgentSoulToolsConfig.model_validate(
|
||||
{"dify_tools": [{"provider_id": "langgenius/search/search", "credential_type": "unauthorized"}]}
|
||||
)
|
||||
|
||||
with pytest.raises(WorkflowAgentPluginToolsBuildError) as exc_info:
|
||||
_build(builder, tools)
|
||||
assert exc_info.value.error_code == "agent_tool_declaration_not_found"
|
||||
|
||||
@ -918,3 +918,43 @@ def test_dataset_rows_filters_malformed_ids(monkeypatch):
|
||||
captured.clear()
|
||||
assert AgentComposerService._dataset_rows(tenant_id="tenant-1", dataset_ids=["nope"]) == {}
|
||||
assert captured == {}
|
||||
|
||||
|
||||
def test_workspace_dify_tools_returns_provider_and_tool_granularities(monkeypatch):
|
||||
"""The slash-menu Tools tab needs both selection granularities: a provider
|
||||
hosts many tools (like an MCP server), so candidates return one
|
||||
provider-level entry (id = <provider>/*, = all tools) plus one per tool."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
provider = SimpleNamespace(
|
||||
name="duckduckgo",
|
||||
plugin_id="langgenius/duckduckgo",
|
||||
label=SimpleNamespace(en_US="DuckDuckGo"),
|
||||
description=SimpleNamespace(en_US="Privacy-first web search"),
|
||||
tools=[
|
||||
SimpleNamespace(name="ddg_search", label=SimpleNamespace(en_US="DuckDuckGo Search")),
|
||||
SimpleNamespace(name="ddg_news", label=SimpleNamespace(en_US="DuckDuckGo News")),
|
||||
],
|
||||
)
|
||||
|
||||
import services.tools.builtin_tools_manage_service as builtin_tools_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
builtin_tools_module.BuiltinToolManageService,
|
||||
"list_builtin_tools",
|
||||
staticmethod(lambda user_id, tenant_id: [provider]),
|
||||
)
|
||||
|
||||
entries = AgentComposerService._workspace_dify_tools(tenant_id="tenant-1", user_id="user-1")
|
||||
|
||||
assert entries[0] == {
|
||||
"id": "duckduckgo/*",
|
||||
"granularity": "provider",
|
||||
"name": "DuckDuckGo",
|
||||
"description": "Privacy-first web search",
|
||||
"provider": "duckduckgo",
|
||||
"plugin_id": "langgenius/duckduckgo",
|
||||
"tools_count": 2,
|
||||
}
|
||||
assert [entry["id"] for entry in entries[1:]] == ["duckduckgo/ddg_search", "duckduckgo/ddg_news"]
|
||||
assert {entry["granularity"] for entry in entries[1:]} == {"tool"}
|
||||
|
||||
@ -176,13 +176,35 @@ def test_unresolved_non_knowledge_mentions_warn_target_missing():
|
||||
assert findings["knowledge_retrieval_placeholder"] == []
|
||||
|
||||
|
||||
def test_all_tools_mention_never_warns_target_missing():
|
||||
# `[§tool:*§]` is the reserved all-tools reference — always valid, even with
|
||||
# zero configured tools, so it must produce neither hard error nor warning.
|
||||
payload = _soul_payload("use [§tool:*:ALL TOOLS§] when needed")
|
||||
def test_provider_all_tools_mention_resolves_against_provider_level_entry():
|
||||
# `[§tool:<provider>/*§]` = all tools of that provider; it points at a
|
||||
# provider-level config entry (tool_name omitted), so with the entry present
|
||||
# it must produce neither hard error nor warning…
|
||||
payload = ComposerSavePayload.model_validate(
|
||||
{
|
||||
"variant": "agent_app",
|
||||
"agent_soul": {
|
||||
"prompt": {"system_prompt": "use [§tool:duckduckgo/*:DuckDuckGo 全部§] when needed"},
|
||||
"tools": {
|
||||
"dify_tools": [
|
||||
{
|
||||
"plugin_id": "langgenius/duckduckgo",
|
||||
"provider": "duckduckgo",
|
||||
"credential_type": "unauthorized",
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"save_strategy": "save_to_current_version",
|
||||
}
|
||||
)
|
||||
ComposerConfigValidator.validate_save_payload(payload)
|
||||
assert _findings(payload) == {"warnings": [], "knowledge_retrieval_placeholder": []}
|
||||
|
||||
# …and without any entry of that provider it warns like any dangling mention.
|
||||
dangling = _findings(_soul_payload("use [§tool:duckduckgo/*:DuckDuckGo 全部§]"))
|
||||
assert [(w["code"], w["kind"]) for w in dangling["warnings"]] == [("mention_target_missing", "tool")]
|
||||
|
||||
|
||||
def test_duplicate_cli_tool_ids_rejected():
|
||||
payload = ComposerSavePayload.model_validate(
|
||||
|
||||
@ -140,14 +140,34 @@ def test_soul_resolver_cli_tool_resolves_by_id_and_keeps_name_alias(soul: AgentS
|
||||
assert expand_prompt_mentions("[§cli_tool:ct-1§]", build_soul_mention_resolver(soul)) == "ffmpeg-v7"
|
||||
|
||||
|
||||
def test_soul_resolver_all_tools_sentinel_never_dangles(soul: AgentSoulConfig):
|
||||
assert (
|
||||
expand_prompt_mentions("Use [§tool:*:ALL TOOLS§].", build_soul_mention_resolver(soul))
|
||||
== "Use all available tools."
|
||||
@pytest.fixture
|
||||
def soul_with_provider_entry(soul: AgentSoulConfig) -> AgentSoulConfig:
|
||||
# provider-level entry (tool_name omitted) = all tools of the provider
|
||||
soul.tools.dify_tools.append(
|
||||
soul.tools.dify_tools[0].model_copy(
|
||||
update={"plugin_id": "langgenius/duckduckgo", "provider": "duckduckgo", "tool_name": None}
|
||||
)
|
||||
)
|
||||
# resolves even with zero configured tools — the sentinel is always valid
|
||||
empty_resolver = build_soul_mention_resolver(AgentSoulConfig.model_validate({}))
|
||||
assert expand_prompt_mentions("[§tool:*§]", empty_resolver) == "all available tools"
|
||||
return soul
|
||||
|
||||
|
||||
def test_soul_resolver_provider_all_tools_mention(soul_with_provider_entry: AgentSoulConfig):
|
||||
resolver = build_soul_mention_resolver(soul_with_provider_entry)
|
||||
# [§tool:<provider>/*§] = all tools of that provider
|
||||
assert expand_prompt_mentions("Use [§tool:duckduckgo/*:DuckDuckGo 全部§].", resolver) == (
|
||||
"Use all duckduckgo tools."
|
||||
)
|
||||
# plugin-prefixed alias of the same provider
|
||||
assert expand_prompt_mentions("[§tool:langgenius/duckduckgo/duckduckgo/*§]", resolver) == "all duckduckgo tools"
|
||||
# without a provider-level entry the mention dangles -> degrades to label
|
||||
bare = build_soul_mention_resolver(AgentSoulConfig.model_validate({}))
|
||||
assert expand_prompt_mentions("[§tool:duckduckgo/*:DuckDuckGo 全部§]", bare) == "DuckDuckGo 全部"
|
||||
|
||||
|
||||
def test_soul_resolver_single_tool_resolves_via_provider_level_entry(soul_with_provider_entry: AgentSoulConfig):
|
||||
# one tool offered through the provider-level ("all") entry still resolves
|
||||
resolver = build_soul_mention_resolver(soul_with_provider_entry)
|
||||
assert expand_prompt_mentions("[§tool:duckduckgo/ddg_search§]", resolver) == "ddg_search"
|
||||
|
||||
|
||||
# ── node-job resolver ─────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user