feat: organize tools and enable qwen realtime calls
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
from models import RuntimeTool
|
||||
from models import RuntimeTool, RuntimeToolBinding
|
||||
|
||||
from services.runtime_variables import (
|
||||
DynamicVariableError,
|
||||
@@ -40,15 +40,8 @@ class ToolExecutor:
|
||||
def set_client_tools(self, client_tools: ClientToolPort | None) -> None:
|
||||
self._client_tools = client_tools
|
||||
|
||||
def register_secrets(self, tool: RuntimeTool) -> None:
|
||||
dynamic = (tool.secrets or {}).get("dynamic_variables") or {}
|
||||
for name, value in dynamic.items():
|
||||
if not str(name).startswith("secret__"):
|
||||
raise DynamicVariableError(f"工具密钥变量必须以 secret__ 开头: {name}")
|
||||
self.store.secrets[str(name)] = str(value)
|
||||
|
||||
@staticmethod
|
||||
def schema_parts(tool: RuntimeTool) -> tuple[dict[str, Any], list[str]]:
|
||||
def input_schema(tool: RuntimeTool) -> tuple[dict[str, Any], list[str]]:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
if tool.type == "mcp":
|
||||
schema = config.get("input_schema") or {}
|
||||
@@ -72,19 +65,58 @@ class ToolExecutor:
|
||||
]
|
||||
return properties, required
|
||||
|
||||
@classmethod
|
||||
def schema_parts(
|
||||
cls,
|
||||
tool: RuntimeTool,
|
||||
binding: RuntimeToolBinding | None = None,
|
||||
) -> tuple[dict[str, Any], list[str]]:
|
||||
properties, required = cls.input_schema(tool)
|
||||
if binding is None:
|
||||
return properties, required
|
||||
visible = {
|
||||
name
|
||||
for name, source in binding.argument_bindings.items()
|
||||
if str((source or {}).get("source") or "") == "model"
|
||||
}
|
||||
missing_required = [
|
||||
name for name in required if name not in binding.argument_bindings
|
||||
]
|
||||
if missing_required:
|
||||
raise ToolExecutionError(
|
||||
"缺少必填工具参数绑定: " + ", ".join(missing_required)
|
||||
)
|
||||
return (
|
||||
{name: spec for name, spec in properties.items() if name in visible},
|
||||
[name for name in required if name in visible],
|
||||
)
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
*,
|
||||
binding: RuntimeToolBinding | None = None,
|
||||
result_assignments: dict[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
self.register_secrets(tool)
|
||||
normalized_arguments = dict(arguments or {})
|
||||
normalized_arguments = self.resolve_arguments(
|
||||
tool,
|
||||
binding,
|
||||
dict(arguments or {}),
|
||||
)
|
||||
template_values = self.resolve_template_bindings(binding)
|
||||
if tool.type == "http":
|
||||
result = await self._execute_http(tool, normalized_arguments)
|
||||
result = await self._execute_http(
|
||||
tool,
|
||||
normalized_arguments,
|
||||
template_values=template_values,
|
||||
)
|
||||
elif tool.type == "mcp":
|
||||
result = await self._execute_mcp(tool, normalized_arguments)
|
||||
result = await self._execute_mcp(
|
||||
tool,
|
||||
normalized_arguments,
|
||||
template_values=template_values,
|
||||
)
|
||||
elif tool.type == "client":
|
||||
result = await self._execute_client(tool, normalized_arguments)
|
||||
else:
|
||||
@@ -94,26 +126,111 @@ class ToolExecutor:
|
||||
return self._apply_result_assignments(
|
||||
tool,
|
||||
result,
|
||||
result_assignments=result_assignments,
|
||||
result_assignments=(
|
||||
result_assignments
|
||||
if result_assignments is not None
|
||||
else (binding.result_assignments if binding is not None else {})
|
||||
),
|
||||
)
|
||||
|
||||
def resolve_arguments(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
binding: RuntimeToolBinding | None,
|
||||
model_arguments: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
if binding is None:
|
||||
return model_arguments
|
||||
properties, required = self.input_schema(tool)
|
||||
visible_model_parameters = {
|
||||
name
|
||||
for name, source in binding.argument_bindings.items()
|
||||
if str((source or {}).get("source") or "") == "model"
|
||||
}
|
||||
resolved: dict[str, Any] = {}
|
||||
for name, source in binding.argument_bindings.items():
|
||||
if name not in properties:
|
||||
raise ToolExecutionError(f"工具参数不存在: {name}")
|
||||
resolved[name] = self._resolve_argument_source(
|
||||
name,
|
||||
source,
|
||||
model_arguments,
|
||||
)
|
||||
unknown_model_arguments = sorted(
|
||||
set(model_arguments) - visible_model_parameters
|
||||
)
|
||||
if unknown_model_arguments:
|
||||
raise ToolExecutionError(
|
||||
"模型提供了未授权工具参数: "
|
||||
+ ", ".join(unknown_model_arguments)
|
||||
)
|
||||
missing = [name for name in required if name not in resolved]
|
||||
if missing:
|
||||
raise ToolExecutionError("缺少必填工具参数: " + ", ".join(missing))
|
||||
return resolved
|
||||
|
||||
def resolve_template_bindings(
|
||||
self,
|
||||
binding: RuntimeToolBinding | None,
|
||||
) -> dict[str, Any]:
|
||||
if binding is None:
|
||||
return {}
|
||||
return {
|
||||
name: self._resolve_source(source, {})
|
||||
for name, source in binding.template_bindings.items()
|
||||
}
|
||||
|
||||
def _resolve_source(
|
||||
self,
|
||||
source: dict[str, Any],
|
||||
model_arguments: dict[str, Any],
|
||||
) -> Any:
|
||||
kind = str((source or {}).get("source") or "")
|
||||
if kind == "model":
|
||||
raise ToolExecutionError("模型参数来源缺少参数上下文")
|
||||
if kind == "variable":
|
||||
name = str(
|
||||
source.get("variable_name") or source.get("variableName") or ""
|
||||
)
|
||||
return self.store.value(name)
|
||||
if kind == "literal":
|
||||
return deepcopy(source.get("value"))
|
||||
if kind == "template":
|
||||
return self.store.render_data(deepcopy(source.get("value")))
|
||||
raise ToolExecutionError(f"不支持的参数来源: {kind or 'empty'}")
|
||||
|
||||
def _resolve_argument_source(
|
||||
self,
|
||||
name: str,
|
||||
source: dict[str, Any],
|
||||
model_arguments: dict[str, Any],
|
||||
) -> Any:
|
||||
if str((source or {}).get("source") or "") == "model":
|
||||
if name not in model_arguments:
|
||||
raise ToolExecutionError(f"模型未提供工具参数: {name}")
|
||||
return model_arguments[name]
|
||||
return self._resolve_source(source, model_arguments)
|
||||
|
||||
async def _execute_http(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
arguments: dict[str, Any],
|
||||
*,
|
||||
template_values: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
parameters = list(config.get("parameters") or [])
|
||||
url = self.store.render(str(config.get("url") or ""))
|
||||
configured_headers = self.store.render_data(
|
||||
renderer = self._tool_renderer(tool, template_values)
|
||||
url = renderer.render(str(config.get("url") or ""))
|
||||
configured_headers = renderer.render_data(
|
||||
deepcopy(config.get("headers") or {}), allow_secrets=True
|
||||
)
|
||||
secret_headers = self.store.render_data(
|
||||
secret_headers = renderer.render_data(
|
||||
deepcopy((tool.secrets or {}).get("headers") or {}), allow_secrets=True
|
||||
)
|
||||
headers: dict[str, str] = {}
|
||||
query: dict[str, object] = {}
|
||||
body = self.store.render_data(deepcopy(config.get("body") or {}))
|
||||
body = renderer.render_data(deepcopy(config.get("body") or {}))
|
||||
|
||||
for parameter in parameters:
|
||||
name = str(parameter.get("name") or "")
|
||||
@@ -169,6 +286,8 @@ class ToolExecutor:
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
arguments: dict[str, Any],
|
||||
*,
|
||||
template_values: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
server = tool.mcp_server
|
||||
if server is None:
|
||||
@@ -178,14 +297,12 @@ class ToolExecutor:
|
||||
if not remote_tool_name:
|
||||
raise ToolExecutionError("MCP 工具缺少远端工具名称")
|
||||
|
||||
renderer = self._tool_renderer(tool, template_values)
|
||||
resolved_server = server.model_copy(
|
||||
update={
|
||||
"url": self.store.render(server.url),
|
||||
"headers": self.store.render_data(server.headers),
|
||||
"secret_headers": self.store.render_data(
|
||||
server.secret_headers,
|
||||
allow_secrets=True,
|
||||
),
|
||||
"url": renderer.render(server.url),
|
||||
"headers": renderer.render_data(server.headers),
|
||||
"secret_headers": dict(server.secret_headers),
|
||||
}
|
||||
)
|
||||
try:
|
||||
@@ -198,6 +315,33 @@ class ToolExecutor:
|
||||
raise ToolExecutionError(str(exc)) from exc
|
||||
return {"status": "ok", "data": payload}
|
||||
|
||||
def _tool_renderer(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
template_values: dict[str, Any],
|
||||
) -> DynamicVariableStore:
|
||||
values = dict(self.store.values)
|
||||
values.update(
|
||||
{f"input__{name}": value for name, value in template_values.items()}
|
||||
)
|
||||
secrets = {
|
||||
str(name): str(value)
|
||||
for name, value in (
|
||||
(tool.secrets or {}).get("dynamic_variables") or {}
|
||||
).items()
|
||||
}
|
||||
invalid = [name for name in secrets if not name.startswith("secret__")]
|
||||
if invalid:
|
||||
raise DynamicVariableError(
|
||||
"工具密钥变量必须以 secret__ 开头: " + ", ".join(invalid)
|
||||
)
|
||||
return DynamicVariableStore(
|
||||
values,
|
||||
secrets,
|
||||
optional_names=self.store.optional_names,
|
||||
variable_types=self.store.variable_types,
|
||||
)
|
||||
|
||||
async def _execute_client(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
@@ -205,6 +349,9 @@ class ToolExecutor:
|
||||
) -> dict[str, Any]:
|
||||
if self._client_tools is None:
|
||||
raise ToolExecutionError("当前运行模式不支持客户端工具")
|
||||
supports = getattr(self._client_tools, "supports", None)
|
||||
if supports is not None and not supports(tool.function_name):
|
||||
raise ToolExecutionError(f"客户端未注册工具: {tool.function_name}")
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
policy = policy_for_tool(tool)
|
||||
try:
|
||||
@@ -225,14 +372,9 @@ class ToolExecutor:
|
||||
*,
|
||||
result_assignments: dict[str, str] | None,
|
||||
) -> dict[str, Any]:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
assignments = (
|
||||
result_assignments
|
||||
if result_assignments is not None
|
||||
else config.get("dynamic_variable_assignments") or {}
|
||||
)
|
||||
assignments = result_assignments or {}
|
||||
payload = result.get("data")
|
||||
updated: list[str] = []
|
||||
staged: dict[str, Any] = {}
|
||||
for variable_name, path in assignments.items():
|
||||
try:
|
||||
value = value_at_path(payload, str(path))
|
||||
@@ -241,6 +383,6 @@ class ToolExecutor:
|
||||
value = value_at_path({"response": payload}, str(path))
|
||||
except KeyError:
|
||||
continue
|
||||
self.store.assign(str(variable_name), value)
|
||||
updated.append(str(variable_name))
|
||||
staged[str(variable_name)] = value
|
||||
updated = self.store.assign_declared_many(staged) if staged else []
|
||||
return {**result, "updated_variables": updated}
|
||||
|
||||
Reference in New Issue
Block a user