183 lines
5.8 KiB
Python
183 lines
5.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""A2A mode — ``AgentMode`` for A2A agent communication.
|
||
|
||
Exposes ``/a2a`` slash command via ``SlashCommandRegistry``.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import TYPE_CHECKING, Optional
|
||
|
||
from agentscope.message import Msg, TextBlock
|
||
|
||
from pineagents.modes.base import AgentMode
|
||
from pineagents.runtime.slash_command_registry import CommandSpec
|
||
|
||
if TYPE_CHECKING:
|
||
from pineagents.runtime.hooks import HookContext
|
||
|
||
logger = logging.getLogger("qwenpaw").getChild(
|
||
__name__.replace("plugin_cloudpaw.", ""),
|
||
)
|
||
|
||
_A2A_CONFIG_FILENAME = "a2a_config.json"
|
||
|
||
|
||
def _load_a2a_agents(workspace_dir: Path) -> dict[str, dict]:
|
||
"""Load per-agent A2A config from workspace."""
|
||
path = workspace_dir / _A2A_CONFIG_FILENAME
|
||
if not path.exists():
|
||
return {}
|
||
try:
|
||
data = json.loads(path.read_text(encoding="utf-8"))
|
||
return data.get("agents", {})
|
||
except (json.JSONDecodeError, OSError) as exc:
|
||
logger.warning("Failed to load %s: %s", path, exc)
|
||
return {}
|
||
|
||
|
||
def _info_msg(text: str) -> Msg:
|
||
"""Wrap text into a system Msg for display."""
|
||
return Msg(
|
||
name="system",
|
||
content=[TextBlock(type="text", text=text)],
|
||
role="system",
|
||
)
|
||
|
||
|
||
def _rewrite_user_msg(ctx: "HookContext", text: str) -> None:
|
||
"""Replace the last user message with *text*."""
|
||
msgs = getattr(ctx, "input_msgs", None)
|
||
if not msgs:
|
||
return
|
||
last = msgs[-1]
|
||
if not isinstance(last, Msg):
|
||
return
|
||
last.content = [TextBlock(type="text", text=text)]
|
||
|
||
|
||
class A2AMode(AgentMode):
|
||
"""Bundle for A2A mode behaviour."""
|
||
|
||
name = "a2a"
|
||
|
||
def commands(self) -> list[CommandSpec]:
|
||
"""Register ``/a2a`` as a standard command."""
|
||
return [
|
||
CommandSpec(
|
||
name="a2a",
|
||
handler=self._a2a_handler,
|
||
category="plugin",
|
||
help_text="List or call remote A2A agents",
|
||
metadata={"plugin": "cloudpaw"},
|
||
),
|
||
]
|
||
|
||
async def _a2a_handler(
|
||
self,
|
||
ctx: "HookContext",
|
||
args: str,
|
||
) -> Optional[Msg]:
|
||
"""Handle ``/a2a [args]``.
|
||
|
||
* ``/a2a`` (no args) — list registered A2A agents
|
||
* ``/a2a list`` — list registered A2A agents
|
||
* ``/a2a <alias> <message>`` — rewrite for a2a_call
|
||
"""
|
||
workspace_dir = getattr(ctx, "workspace_dir", None)
|
||
if not workspace_dir:
|
||
return _info_msg("Error: workspace_dir not available")
|
||
|
||
agents_cfg = _load_a2a_agents(Path(workspace_dir))
|
||
|
||
# No args or "list": list agents
|
||
if not args or not args.strip() or args.strip().lower() == "list":
|
||
return await self._handle_list(agents_cfg)
|
||
|
||
# Parse args: <alias> <message>
|
||
parts = args.strip().split(None, 1)
|
||
if len(parts) < 2:
|
||
return _info_msg(
|
||
"用法:`/a2a <agent_name> <message>`\n\n"
|
||
"使用 `/a2a` 或 `/a2a list` 查看可用的 agent 列表。",
|
||
)
|
||
|
||
agent_name, message = parts[0].strip(), parts[1].strip()
|
||
|
||
# Check if agent exists
|
||
if agent_name not in agents_cfg:
|
||
available = ", ".join(agents_cfg.keys()) if agents_cfg else "无"
|
||
return _info_msg(
|
||
f"未找到别名为 '{agent_name}' 的已注册 A2A Agent。\n\n"
|
||
f"可用别名:{available}",
|
||
)
|
||
|
||
# Rewrite message for agent to call a2a_call
|
||
prompt = (
|
||
f"请使用 a2a_call 工具调用远程 A2A Agent。\n"
|
||
f'调用参数:agent_alias="{agent_name}",'
|
||
f'message="{message}"\n\n'
|
||
f"请直接调用 a2a_call 工具完成此任务,不需要做其他额外操作。"
|
||
)
|
||
|
||
_rewrite_user_msg(ctx, prompt)
|
||
logger.info("[CloudPaw] /a2a rewritten for agent processing")
|
||
return None # Let agent process the rewritten message
|
||
|
||
async def _handle_list(self, agents_cfg: dict[str, dict]) -> Msg:
|
||
"""List registered A2A agents."""
|
||
if not agents_cfg:
|
||
return _info_msg(
|
||
"暂无已注册的远程 A2A Agent。\n\n"
|
||
"使用 POST /api/a2a/agents 注册新的 Agent,"
|
||
"或在 A2A 管理页面添加。",
|
||
)
|
||
|
||
try:
|
||
from modules.a2a.client_manager import get_a2a_manager
|
||
except ImportError:
|
||
# Fallback: just list aliases
|
||
lines = ["**已注册的远程 A2A Agent:\n"]
|
||
for alias in agents_cfg.keys():
|
||
lines.append(f"- {alias}")
|
||
footer = "\n---\n*Use `/a2a <alias> <message>` to send a message.*"
|
||
lines.append(footer)
|
||
return _info_msg("\n".join(lines))
|
||
manager = get_a2a_manager()
|
||
|
||
lines = ["**已注册的远程 A2A Agent:**\n"]
|
||
for alias, reg in agents_cfg.items():
|
||
card_info = await manager.get_card_info(reg["url"])
|
||
status = (
|
||
card_info.get("status", "disconnected")
|
||
if card_info
|
||
else "disconnected"
|
||
)
|
||
name = card_info.get("name", "") if card_info else ""
|
||
desc = card_info.get("description", "") if card_info else ""
|
||
status_icon = "🟢" if status == "connected" else "⚪"
|
||
|
||
line = f"\n{status_icon} **{alias}**"
|
||
if name:
|
||
line += f" — {name}"
|
||
if desc:
|
||
line += f"\n {desc[:80]}"
|
||
if status != "connected":
|
||
line += f"\n 状态: {status}"
|
||
lines.append(line)
|
||
|
||
footer = (
|
||
"\n---\n"
|
||
"*Use `/a2a <alias> <message>` to send a message "
|
||
"to a remote Agent.*"
|
||
)
|
||
lines.append(footer)
|
||
|
||
return _info_msg("\n".join(lines))
|
||
|
||
|
||
__all__ = ["A2AMode"]
|