Phase 4.2+4.3: YAML workflow spec + ACA protocol abstraction (v2026.07.19.10)
CI / gate (push) Has been cancelled

This commit is contained in:
QihuAI Design committed 2026-07-19 20:17:49 +08:00
1 parent c716dab851
commit 79c27795a8
9 files changed
+936 -1

No files matched your search

+293
View File
@@ -0,0 +1,293 @@
"""Phase 4.3: ACA (Agent Communication Abstraction) 协议抽象层.
设计目标:
- 定义统一对外工具协议接口,隔离 MCP / A2A / 自定义协议的具体实现。
- 防止协议绑架:未来 MCP 升级或 A2A 替代 MCP 时,只需增删适配器,不改业务代码。
- 每个协议适配器被注册为 "protocol adapter",按名称发现。
接口:
ACAProtocol
name: str
version: str
capabilities: list[str]
async list_tools() -> list[dict] # [{name, description, inputSchema}]
async call_tool(tool_name, arguments) -> str
async health_check() -> bool
ProtocolRegistry
register(adapter: ACAProtocol)
get(name: str) -> ACAProtocol
list_all() -> list[ACAProtocol]
async list_all_tools() -> list[dict] # 聚合所有协议的 tools,带 protocol tag
async call_tool(protocol_name, tool_name, args) -> str
使用示例:
from app.core.aca import registry
tools = await registry.list_all_tools()
# tools: [{"name":"search","protocol":"mcp","description":"..."}, ...]
result = await registry.call_tool("mcp", "search", {"query":"..."})
"""
from __future__ import annotations
import abc
import asyncio
import json
import logging
from typing import Any
import httpx
logger = logging.getLogger("aiflow.aca")
# ── 协议接口 ─────────────────────────────────────────────────────────────────
class ACAProtocol(abc.ABC):
name: str = ""
version: str = "1.0"
capabilities: list[str] = []
@abc.abstractmethod
async def list_tools(self) -> list[dict]:
"""返回 [{name, description, inputSchema}]。"""
@abc.abstractmethod
async def call_tool(self, tool_name: str, arguments: dict) -> str:
"""调用工具并返回文本结果。"""
async def health_check(self) -> bool:
"""健康检查,默认 True。"""
return True
def __repr__(self):
return f"<ACA {self.name} v{self.version}>"
# ── 注册表 ───────────────────────────────────────────────────────────────────
class ProtocolRegistry:
"""全局协议注册表,单例。"""
def __init__(self):
self._adapters: dict[str, ACAProtocol] = {}
def register(self, adapter: ACAProtocol):
if not adapter.name:
raise ValueError("protocol adapter must have a name")
self._adapters[adapter.name] = adapter
logger.info("ACA registry: registered %s", adapter)
def get(self, name: str) -> ACAProtocol | None:
return self._adapters.get(name)
def list_all(self) -> list[ACAProtocol]:
return list(self._adapters.values())
async def list_all_tools(self) -> list[dict]:
"""聚合所有已注册协议的 tools,每个 tool 标记 protocol。"""
tasks = [self._list_with_name(a) for a in self._adapters.values()]
results = await asyncio.gather(*tasks, return_exceptions=True)
flat: list[dict] = []
for rlist in results:
if isinstance(rlist, list):
flat.extend(rlist)
return flat
async def _list_with_name(self, adapter: ACAProtocol) -> list[dict]:
try:
tools = await adapter.list_tools()
for t in tools:
t["protocol"] = adapter.name
return tools
except Exception as e:
logger.warning("ACA: list_tools failed for %s: %s", adapter.name, e)
return []
async def call_tool(self, protocol_name: str, tool_name: str, arguments: dict) -> str:
adapter = self._adapters.get(protocol_name)
if not adapter:
raise ValueError(f"协议 {protocol_name} 未注册")
return await adapter.call_tool(tool_name, arguments)
# 全局单例
registry = ProtocolRegistry()
# ── MCP 适配器 ───────────────────────────────────────────────────────────────
class McpProtocolAdapter(ACAProtocol):
"""包装平台现有的 MCP 客户端 (app.mcp_client),提供统一协议接口。"""
def __init__(self, server_configs: list[dict] = None):
self.name = "mcp"
self.version = "1.0"
self.capabilities = ["tools", "resources", "prompts"]
self._server_configs = server_configs or []
self._tools_cache: list[dict] = []
self._cache_ts: float = 0
async def list_tools(self) -> list[dict]:
# Use cached tools if available (TTL 30s)
import time
if self._tools_cache and (time.time() - self._cache_ts) < 30:
return [dict(t) for t in self._tools_cache]
from app.mcp_client import list_mcp_tools
all_tools: list[dict] = []
for server in self._server_configs:
try:
tools = await list_mcp_tools(server)
for t in tools:
t["_mcp_server_id"] = server.get("id", "")
t["_mcp_server_name"] = server.get("name", "")
all_tools.extend(tools)
except Exception as e:
logger.warning("MCP list_tools failed for %s: %s", server.get("name"), e)
self._tools_cache = [dict(t) for t in all_tools]
self._cache_ts = time.time()
return list(self._tools_cache)
async def call_tool(self, tool_name: str, arguments: dict) -> str:
from app.mcp_client import call_mcp_tool
# Find the server that has this tool (check cache first)
for t in self._tools_cache:
if t.get("name") == tool_name:
sid = t.get("_mcp_server_id", "")
server = next((s for s in self._server_configs if s.get("id") == sid), None)
if server:
return await call_mcp_tool(server, tool_name, arguments)
# Fallback: try first available server
if self._server_configs:
return await call_mcp_tool(self._server_configs[0], tool_name, arguments)
return json.dumps({"error": "No MCP server configured for tool: " + tool_name})
async def health_check(self) -> bool:
# Check if any MCP server is connected
try:
tools = await self.list_tools()
return len(tools) > 0
except Exception:
return False
# ── A2A 适配器 ───────────────────────────────────────────────────────────────
class A2aProtocolAdapter(ACAProtocol):
"""A2A (Agent-to-Agent) 协议适配器。
通过标准 JSON-RPC 与远端 Agent 通信。端点需实现:
- POST /a2a/tools → 返回工具列表
- POST /a2a/call → 调用工具
A2A 规范参考: agentcommunicationprotocol.dev
"""
def __init__(self, endpoint: str, api_key: str = None, timeout: float = 30):
self.name = "a2a"
self.version = "0.9"
self.capabilities = ["tools"]
self.endpoint = endpoint.rstrip("/")
self.api_key = api_key
self.timeout = timeout
async def list_tools(self) -> list[dict]:
async with httpx.AsyncClient(timeout=self.timeout, trust_env=False) as client:
headers = self._headers()
resp = await client.post(f"{self.endpoint}/a2a/tools", headers=headers, json={})
if resp.status_code != 200:
logger.warning("A2A list_tools %s HTTP %s: %s", self.endpoint, resp.status_code, resp.text[:200])
return []
data = resp.json()
return data.get("tools") or data.get("data", {}).get("tools") or []
async def call_tool(self, tool_name: str, arguments: dict) -> str:
async with httpx.AsyncClient(timeout=self.timeout, trust_env=False) as client:
headers = self._headers()
body = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {"name": tool_name, "arguments": arguments},
"id": 1,
}
resp = await client.post(f"{self.endpoint}/a2a/call", headers=headers, json=body)
if resp.status_code != 200:
return json.dumps({"error": f"A2A HTTP {resp.status_code}: {resp.text[:300]}"})
data = resp.json()
if "error" in data:
return json.dumps({"error": data["error"]})
result = data.get("result")
if isinstance(result, dict):
return result.get("content", json.dumps(result))
return str(result) if result is not None else ""
async def health_check(self) -> bool:
try:
async with httpx.AsyncClient(timeout=10, trust_env=False) as client:
headers = self._headers()
resp = await client.get(f"{self.endpoint}/a2a/health", headers=headers)
return resp.status_code == 200
except Exception:
return False
def _headers(self) -> dict:
h = {"Content-Type": "application/json"}
if self.api_key:
h["Authorization"] = f"Bearer {self.api_key}"
return h
# ── 启动时初始化 ─────────────────────────────────────────────────────────────
def init_aca_registry():
"""在应用启动时调用,注册所有协议适配器到全局 registry。"""
from app.database import SessionLocal
from app.models import MCPServer
# 注册 MCP 适配器(从 DB 读取所有 enabled MCP server 配置)
try:
db = SessionLocal()
servers = db.query(MCPServer).filter(MCPServer.is_enabled == True).all()
server_configs = []
for s in servers:
server_configs.append({
"id": s.id,
"name": s.name,
"transport_type": s.transport_type,
"command": s.command,
"args": json.loads(s.args) if isinstance(s.args, str) else s.args or [],
"env_vars": json.loads(s.env_vars) if isinstance(s.env_vars, str) else s.env_vars or {},
"url": s.url,
"headers": json.loads(s.headers) if isinstance(s.headers, str) else s.headers or {},
})
db.close()
if server_configs:
registry.register(McpProtocolAdapter(server_configs))
logger.info("ACA: MCP adapter registered with %d servers", len(server_configs))
except Exception as e:
logger.warning("ACA: failed to init MCP adapter: %s", e)
# 注册 A2A 适配器(从配置或 DB 读取 A2A endpoint)
# A2A endpoint 可以从 site_settings 读取,暂无则留空(以后台配置驱动)
a2a_endpoint = _get_a2a_config()
if a2a_endpoint:
registry.register(A2aProtocolAdapter(
endpoint=a2a_endpoint.get("endpoint", ""),
api_key=a2a_endpoint.get("api_key"),
))
logger.info("ACA: A2A adapter registered at %s", a2a_endpoint.get("endpoint"))
def _get_a2a_config() -> dict | None:
"""从 site_settings 读取 A2A 全局配置。"""
try:
from app.database import SessionLocal
from app.models import SiteSetting
db = SessionLocal()
setting = db.query(SiteSetting).filter(SiteSetting.key == "aca_a2a_endpoint").first()
db.close()
if setting and setting.value:
val = json.loads(setting.value) if isinstance(setting.value, str) else setting.value
if isinstance(val, dict) and val.get("endpoint"):
return val
except Exception:
pass
return None