feat(agents): add channel tool middleware support
新增渠道工具中间件,支持从对话渠道加载并执行对应Agent工具: 1. 新增ChannelToolMiddleware中间件实现工具注入逻辑 2. 扩展BaseContext增加channel_type和channel_tools字段 3. 在内置聊天机器人和深度代理中注册渠道工具中间件 4. 导出中间件到包公共接口
This commit is contained in:
parent
fed559dbfc
commit
90b16ce9b1
@ -11,6 +11,7 @@ from yuxi.agents.middlewares import (
|
||||
SummaryOffloadMiddleware,
|
||||
save_attachments_to_fs,
|
||||
)
|
||||
from yuxi.agents.middlewares.channel_tool_middleware import ChannelToolMiddleware
|
||||
from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
|
||||
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware
|
||||
from yuxi.services.mcp_service import get_tools_from_all_servers
|
||||
@ -51,6 +52,7 @@ async def _build_middlewares(context):
|
||||
save_attachments_to_fs, # 附件注入提示词
|
||||
KnowledgeBaseMiddleware(), # 知识库工具
|
||||
RuntimeConfigMiddleware(extra_tools=all_mcp_tools), # 运行时配置应用(模型/工具/MCP/提示词)
|
||||
ChannelToolMiddleware(), # 渠道工具注入
|
||||
SkillsMiddleware(), # Skills 中间件(提示词注入、依赖展开、动态激活)
|
||||
subagents_middleware,
|
||||
summary_middleware,
|
||||
|
||||
@ -14,6 +14,7 @@ from yuxi.agents.middlewares import (
|
||||
SummaryOffloadMiddleware,
|
||||
save_attachments_to_fs,
|
||||
)
|
||||
from yuxi.agents.middlewares.channel_tool_middleware import ChannelToolMiddleware
|
||||
from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
|
||||
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware
|
||||
from yuxi.agents.toolkits.buildin.tools import _create_tavily_search
|
||||
@ -97,6 +98,7 @@ class DeepAgent(BaseAgent):
|
||||
middleware=[
|
||||
FilesystemMiddleware(backend=create_agent_composite_backend), # 文件系统后端
|
||||
RuntimeConfigMiddleware(extra_tools=all_mcp_tools),
|
||||
ChannelToolMiddleware(), # 渠道工具注入
|
||||
SkillsMiddleware(), # Skills 中间件(提示词注入、依赖展开、动态激活)
|
||||
save_attachments_to_fs, # 附件注入提示词
|
||||
TodoListMiddleware(system_prompt="任务结束前,应该检查维护的待办事项列表是否结束。"),
|
||||
|
||||
@ -114,6 +114,24 @@ class BaseContext:
|
||||
},
|
||||
)
|
||||
|
||||
channel_type: str = field(
|
||||
default="",
|
||||
metadata={
|
||||
"name": "渠道类型",
|
||||
"configurable": False,
|
||||
"description": "当前对话的渠道类型(feishu/dingtalk 等),用于渠道工具的运行时执行。",
|
||||
},
|
||||
)
|
||||
|
||||
channel_tools: list[dict] = field(
|
||||
default_factory=list,
|
||||
metadata={
|
||||
"name": "渠道工具",
|
||||
"configurable": False,
|
||||
"description": "渠道注册的 Agent 工具列表(OpenAI function schema 格式),由 ChannelToolMiddleware 转换为可执行工具。",
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_configurable_items(cls):
|
||||
"""实现一个可配置的参数列表,在 UI 上配置时使用"""
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from .attachment_middleware import inject_attachment_context, save_attachments_to_fs
|
||||
from .channel_tool_middleware import ChannelToolMiddleware
|
||||
from .context_middlewares import context_aware_prompt, context_based_model
|
||||
from .dynamic_tool_middleware import DynamicToolMiddleware
|
||||
from .runtime_config_middleware import RuntimeConfigMiddleware
|
||||
from .summary_middleware import SummaryOffloadMiddleware, create_summary_offload_middleware
|
||||
|
||||
__all__ = [
|
||||
"ChannelToolMiddleware",
|
||||
"DynamicToolMiddleware",
|
||||
"RuntimeConfigMiddleware",
|
||||
"SummaryOffloadMiddleware",
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal
|
||||
|
||||
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
|
||||
from langchain_core.tools import StructuredTool
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from yuxi.channel.protocols import AgentToolProtocol
|
||||
from yuxi.channel.plugins.registry import ChannelPluginRegistry
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
_TYPE_MAP: dict[str, type] = {
|
||||
"string": str,
|
||||
"integer": int,
|
||||
"number": float,
|
||||
"boolean": bool,
|
||||
"array": list,
|
||||
"object": dict,
|
||||
}
|
||||
|
||||
|
||||
def _schema_to_pydantic_model(tool_name: str, parameters: dict) -> type[BaseModel]:
|
||||
properties = parameters.get("properties", {})
|
||||
required = set(parameters.get("required", []))
|
||||
|
||||
fields_def: dict[str, Any] = {}
|
||||
for param_name, param_info in properties.items():
|
||||
json_type = param_info.get("type", "string")
|
||||
enum_values = param_info.get("enum")
|
||||
|
||||
if enum_values:
|
||||
param_type = Literal[tuple(enum_values)] # type: ignore[valid-type]
|
||||
else:
|
||||
param_type = _TYPE_MAP.get(json_type, str)
|
||||
|
||||
field_kwargs: dict[str, Any] = {"description": param_info.get("description", "")}
|
||||
if param_name not in required:
|
||||
field_kwargs["default"] = None
|
||||
|
||||
fields_def[param_name] = (param_type, Field(**field_kwargs))
|
||||
|
||||
model_name = f"{tool_name}_args"
|
||||
return create_model(model_name, **fields_def)
|
||||
|
||||
|
||||
def _make_channel_tool_executor(channel_type: str, tool_name: str):
|
||||
async def _execute(**kwargs):
|
||||
plugin = ChannelPluginRegistry.get(channel_type)
|
||||
if plugin is None:
|
||||
return f"Error: 渠道 '{channel_type}' 未注册"
|
||||
if not isinstance(plugin, AgentToolProtocol):
|
||||
return f"Error: 渠道 '{channel_type}' 不支持 Agent 工具协议"
|
||||
try:
|
||||
result = await plugin.execute_agent_tool(tool_name, kwargs, {})
|
||||
except Exception:
|
||||
logger.exception("Channel tool execution failed: %s.%s", channel_type, tool_name)
|
||||
return f"Error: 工具 '{tool_name}' 执行异常"
|
||||
if result.get("success"):
|
||||
return json.dumps(result.get("result", ""), ensure_ascii=False)
|
||||
return f"Error: {result.get('error', 'Unknown error')}"
|
||||
|
||||
return _execute
|
||||
|
||||
|
||||
class ChannelToolMiddleware(AgentMiddleware):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._cache: dict[tuple[str, int], list[StructuredTool]] = {}
|
||||
|
||||
def _build_tools(self, channel_type: str, tool_schemas: list[dict]) -> list[StructuredTool]:
|
||||
schema_hash = hash(json.dumps(tool_schemas, sort_keys=True, ensure_ascii=False))
|
||||
cache_key = (channel_type, schema_hash)
|
||||
if cache_key in self._cache:
|
||||
return self._cache[cache_key]
|
||||
|
||||
tools: list[StructuredTool] = []
|
||||
for schema in tool_schemas:
|
||||
func_def = schema.get("function", {})
|
||||
tool_name = func_def.get("name", "")
|
||||
if not tool_name:
|
||||
continue
|
||||
description = func_def.get("description", "")
|
||||
parameters = func_def.get("parameters", {})
|
||||
|
||||
try:
|
||||
args_model = _schema_to_pydantic_model(tool_name, parameters)
|
||||
executor = _make_channel_tool_executor(channel_type, tool_name)
|
||||
tool = StructuredTool.from_function(
|
||||
name=tool_name,
|
||||
description=description,
|
||||
args_schema=args_model,
|
||||
coroutine=executor,
|
||||
)
|
||||
tools.append(tool)
|
||||
except Exception:
|
||||
logger.exception("Failed to create channel tool: %s.%s", channel_type, tool_name)
|
||||
|
||||
self._cache[cache_key] = tools
|
||||
logger.debug("ChannelToolMiddleware built %d tools for channel %s", len(tools), channel_type)
|
||||
return tools
|
||||
|
||||
async def awrap_model_call(
|
||||
self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
||||
) -> ModelResponse:
|
||||
runtime_context = request.runtime.context
|
||||
channel_type = getattr(runtime_context, "channel_type", None) or ""
|
||||
channel_tools = getattr(runtime_context, "channel_tools", None) or []
|
||||
|
||||
if channel_type and channel_tools:
|
||||
langchain_tools = self._build_tools(channel_type, channel_tools)
|
||||
if langchain_tools:
|
||||
tools_by_name = {t.name: t for t in request.tools or []}
|
||||
for t in langchain_tools:
|
||||
tools_by_name[t.name] = t
|
||||
request = request.override(tools=list(tools_by_name.values()))
|
||||
|
||||
return await handler(request)
|
||||
Loading…
Reference in New Issue
Block a user