chat.py 11 KB

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