chat.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. """IM 工具 — 将 im-client 接入 Agent 框架。
  2. 新架构:一个 Agent (contact_id) = 一个 IMClient 实例,该实例管理多个窗口 (chat_id)。
  3. """
  4. import asyncio
  5. import json
  6. import logging
  7. import os
  8. import sys
  9. from typing import Optional
  10. from agent.tools import tool, ToolResult, ToolContext
  11. # 将 im-client 目录加入 sys.path
  12. _IM_CLIENT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "im-client"))
  13. if _IM_CLIENT_DIR not in sys.path:
  14. sys.path.insert(0, _IM_CLIENT_DIR)
  15. from client import IMClient # noqa: E402
  16. from notifier import AgentNotifier # noqa: E402
  17. logger = logging.getLogger(__name__)
  18. # ── 全局状态 ──
  19. _clients: dict[str, IMClient] = {}
  20. _tasks: dict[str, asyncio.Task] = {}
  21. _notifications: dict[tuple[str, str], dict] = {} # (contact_id, chat_id) -> 通知
  22. class _ToolNotifier(AgentNotifier):
  23. """内部通知器:按 (contact_id, chat_id) 分发通知。"""
  24. def __init__(self, contact_id: str, chat_id: str):
  25. self._key = (contact_id, chat_id)
  26. async def notify(self, count: int, from_contacts: list[str]):
  27. _notifications[self._key] = {"count": count, "from": from_contacts}
  28. # ── Tool 1: 初始化连接 ──
  29. @tool(
  30. hidden_params=["context"],
  31. groups=["im"],
  32. display={
  33. "zh": {"name": "初始化 IM 连接", "params": {"contact_id": "你的身份 ID", "server_url": "服务器地址"}},
  34. "en": {"name": "Setup IM Connection", "params": {"contact_id": "Your identity ID", "server_url": "Server URL"}},
  35. }
  36. )
  37. async def im_setup(
  38. contact_id: str,
  39. server_url: str = "ws://localhost:8000",
  40. notify_interval: float = 10.0,
  41. context: Optional[ToolContext] = None,
  42. ) -> ToolResult:
  43. """初始化 IM Client 并连接 Server。
  44. Args:
  45. contact_id: 你在 IM 系统中的身份 ID
  46. server_url: Server 的 WebSocket 地址
  47. notify_interval: 检查新消息的间隔秒数
  48. """
  49. if contact_id in _clients:
  50. return ToolResult(title="IM 已连接", output=f"已连接: {contact_id}")
  51. client = IMClient(contact_id=contact_id, server_url=server_url, notify_interval=notify_interval)
  52. _clients[contact_id] = client
  53. loop = asyncio.get_event_loop()
  54. _tasks[contact_id] = loop.create_task(client.run())
  55. # 等待一小段时间,让连接尝试开始(非阻塞)
  56. await asyncio.sleep(0.5)
  57. return ToolResult(title="IM 连接成功", output=f"已启动 IM Client: {contact_id},后台连接中...")
  58. # ── Tool 2: 窗口管理 ──
  59. @tool(
  60. hidden_params=["context"],
  61. groups=["im"],
  62. display={
  63. "zh": {"name": "打开 IM 窗口", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
  64. "en": {"name": "Open IM Window", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
  65. }
  66. )
  67. async def im_open_window(
  68. contact_id: str,
  69. chat_id: str | None = None,
  70. context: Optional[ToolContext] = None,
  71. ) -> ToolResult:
  72. """打开一个新的聊天窗口。
  73. Args:
  74. contact_id: Agent ID
  75. chat_id: 窗口 ID(留空自动生成)
  76. """
  77. client = _clients.get(contact_id)
  78. if client is None:
  79. return ToolResult(title="未连接", output=f"错误: {contact_id} 未初始化", error="未初始化")
  80. actual_chat_id = client.open_window(chat_id=chat_id, notifier=_ToolNotifier(contact_id, chat_id or ""))
  81. if chat_id is None:
  82. client._notifiers[actual_chat_id] = _ToolNotifier(contact_id, actual_chat_id)
  83. return ToolResult(title="窗口已打开", output=f"窗口 ID: {actual_chat_id}")
  84. @tool(
  85. hidden_params=["context"],
  86. groups=["im"],
  87. display={
  88. "zh": {"name": "关闭 IM 窗口", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
  89. "en": {"name": "Close IM Window", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
  90. }
  91. )
  92. async def im_close_window(
  93. contact_id: str,
  94. chat_id: str,
  95. context: Optional[ToolContext] = None,
  96. ) -> ToolResult:
  97. """关闭一个聊天窗口。
  98. Args:
  99. contact_id: Agent ID
  100. chat_id: 窗口 ID
  101. """
  102. client = _clients.get(contact_id)
  103. if client is None:
  104. return ToolResult(title="未连接", output=f"错误: {contact_id} 未初始化", error="未初始化")
  105. client.close_window(chat_id)
  106. _notifications.pop((contact_id, chat_id), None)
  107. return ToolResult(title="窗口已关闭", output=f"已关闭窗口: {chat_id}")
  108. # ── Tool 3: 检查通知 ──
  109. @tool(
  110. hidden_params=["context"],
  111. groups=["im"],
  112. display={
  113. "zh": {"name": "检查 IM 新消息通知", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
  114. "en": {"name": "Check IM Notifications", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
  115. }
  116. )
  117. async def im_check_notification(
  118. contact_id: str,
  119. chat_id: str,
  120. context: Optional[ToolContext] = None,
  121. ) -> ToolResult:
  122. """检查某个窗口是否有新消息通知。
  123. Args:
  124. contact_id: Agent ID
  125. chat_id: 窗口 ID
  126. """
  127. notification = _notifications.pop((contact_id, chat_id), None)
  128. if notification is None:
  129. return ToolResult(title="无新消息", output="当前没有新消息通知")
  130. return ToolResult(
  131. title=f"有 {notification['count']} 条新消息",
  132. output=json.dumps(notification, ensure_ascii=False),
  133. metadata=notification,
  134. )
  135. # ── Tool 4: 接收消息 ──
  136. @tool(
  137. hidden_params=["context"],
  138. groups=["im"],
  139. display={
  140. "zh": {"name": "接收 IM 消息", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID"}},
  141. "en": {"name": "Receive IM Messages", "params": {"contact_id": "Agent ID", "chat_id": "Window ID"}},
  142. }
  143. )
  144. async def im_receive_messages(
  145. contact_id: str,
  146. chat_id: str,
  147. context: Optional[ToolContext] = None,
  148. ) -> ToolResult:
  149. """读取某个窗口的待处理消息。
  150. Args:
  151. contact_id: Agent ID
  152. chat_id: 窗口 ID
  153. """
  154. client = _clients.get(contact_id)
  155. if client is None:
  156. return ToolResult(title="未连接", output=f"错误: {contact_id} 未初始化", error="未初始化")
  157. raw = client.read_pending(chat_id)
  158. if not raw:
  159. return ToolResult(title="无待处理消息", output="[]")
  160. messages = [
  161. {
  162. "sender": m.get("sender", "unknown"),
  163. "sender_chat_id": m.get("sender_chat_id"),
  164. "content": m.get("content", ""),
  165. "msg_type": m.get("msg_type", "chat"),
  166. }
  167. for m in raw
  168. ]
  169. return ToolResult(
  170. title=f"收到 {len(messages)} 条消息",
  171. output=json.dumps(messages, ensure_ascii=False, indent=2),
  172. metadata={"messages": messages},
  173. )
  174. # ── Tool 5: 发送消息 ──
  175. @tool(
  176. hidden_params=["context"],
  177. groups=["im"],
  178. display={
  179. "zh": {"name": "发送 IM 消息", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID", "receiver": "接收者 ID", "content": "消息内容"}},
  180. "en": {"name": "Send IM Message", "params": {"contact_id": "Agent ID", "chat_id": "Window ID", "receiver": "Receiver ID", "content": "Message content"}},
  181. }
  182. )
  183. async def im_send_message(
  184. contact_id: str,
  185. chat_id: str,
  186. receiver: str,
  187. content: str,
  188. msg_type: str = "chat",
  189. receiver_chat_id: str | None = None,
  190. context: Optional[ToolContext] = None,
  191. ) -> ToolResult:
  192. """从某个窗口发送消息。
  193. Args:
  194. contact_id: 发送方 Agent ID
  195. chat_id: 发送方窗口 ID
  196. receiver: 接收方 contact_id
  197. content: 消息内容
  198. msg_type: 消息类型
  199. receiver_chat_id: 接收方窗口 ID(不指定则广播)
  200. """
  201. client = _clients.get(contact_id)
  202. if client is None:
  203. return ToolResult(title="未连接", output=f"错误: {contact_id} 未初始化", error="未初始化")
  204. client.send_message(chat_id, receiver, content, msg_type, receiver_chat_id)
  205. target = f"{receiver}:{receiver_chat_id}" if receiver_chat_id else f"{receiver}:*"
  206. return ToolResult(
  207. title=f"已发送给 {target}",
  208. output=f"[{contact_id}:{chat_id}] 已发送给 {target}: {content[:50]}",
  209. )
  210. # ── Tool 6: 查询联系人 ──
  211. @tool(
  212. hidden_params=["context"],
  213. groups=["im"],
  214. display={
  215. "zh": {"name": "查询 IM 联系人", "params": {"contact_id": "Agent ID", "server_http_url": "服务器 HTTP 地址"}},
  216. "en": {"name": "Get IM Contacts", "params": {"contact_id": "Agent ID", "server_http_url": "Server HTTP URL"}},
  217. }
  218. )
  219. async def im_get_contacts(
  220. contact_id: str,
  221. server_http_url: str = "http://localhost:8000",
  222. context: Optional[ToolContext] = None,
  223. ) -> ToolResult:
  224. """查询联系人列表和当前在线用户。
  225. Args:
  226. contact_id: Agent ID
  227. server_http_url: Server 的 HTTP 地址
  228. """
  229. if contact_id not in _clients:
  230. return ToolResult(title="未连接", output=f"错误: {contact_id} 未初始化", error="未初始化")
  231. import httpx
  232. result = {}
  233. async with httpx.AsyncClient() as http:
  234. try:
  235. r = await http.get(f"{server_http_url}/contacts/{contact_id}")
  236. result["contacts"] = r.json().get("contacts", [])
  237. except Exception as e:
  238. result["contacts_error"] = str(e)
  239. try:
  240. r = await http.get(f"{server_http_url}/health")
  241. result["online"] = r.json().get("online", {})
  242. except Exception as e:
  243. result["online_error"] = str(e)
  244. return ToolResult(
  245. title="联系人查询完成",
  246. output=json.dumps(result, ensure_ascii=False, indent=2),
  247. metadata=result,
  248. )
  249. # ── Tool 7: 查询聊天历史 ──
  250. @tool(
  251. hidden_params=["context"],
  252. groups=["im"],
  253. display={
  254. "zh": {"name": "查询 IM 聊天历史", "params": {"contact_id": "Agent ID", "chat_id": "窗口 ID", "peer_id": "联系人 ID", "limit": "最大条数"}},
  255. "en": {"name": "Get IM Chat History", "params": {"contact_id": "Agent ID", "chat_id": "Window ID", "peer_id": "Contact ID", "limit": "Max records"}},
  256. }
  257. )
  258. async def im_get_chat_history(
  259. contact_id: str,
  260. chat_id: str,
  261. peer_id: str | None = None,
  262. limit: int = 20,
  263. context: Optional[ToolContext] = None,
  264. ) -> ToolResult:
  265. """查询某个窗口的聊天历史。
  266. Args:
  267. contact_id: Agent ID
  268. chat_id: 窗口 ID
  269. peer_id: 筛选与某个联系人的聊天(留空返回所有)
  270. limit: 最多返回条数
  271. """
  272. client = _clients.get(contact_id)
  273. if client is None:
  274. return ToolResult(title="未连接", output=f"错误: {contact_id} 未初始化", error="未初始化")
  275. messages = client.get_chat_history(chat_id, peer_id, limit)
  276. return ToolResult(
  277. title=f"查到 {len(messages)} 条记录",
  278. output=json.dumps(messages, ensure_ascii=False, indent=2),
  279. metadata={"messages": messages},
  280. )