2
0

conversation_manager.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. """
  2. 多轮对话管理器 - 处理包含工具调用的多轮对话
  3. """
  4. import asyncio
  5. from typing import Dict, List, Any, Optional
  6. import httpx
  7. from anthropic import Anthropic
  8. from mcp_client import MCPClient
  9. from tool_converter import ToolConverter
  10. from tool_handler import ToolCallHandler
  11. # ========== 默认组件提示(降级方案) ==========
  12. # 当前端未发送组件列表时使用的默认提示
  13. DEFAULT_COMPONENTS_PROMPT = """### 可用的 json-render 组件
  14. 你可以使用基础组件来展示结构化数据:
  15. - `card`: 卡片容器
  16. - `stack`: 布局容器
  17. - `heading`: 标题 (h1-h6)
  18. - `text`: 文本内容
  19. - `button`: 按钮
  20. - `badge`: 徽章标签
  21. - `code-block`: 代码块
  22. - `data-table`: 数据表格
  23. 将组件 JSON 包含在 markdown 代码块中返回。
  24. """
  25. # ========== 基础 System Prompt ==========
  26. # 不包含组件列表,组件列表由前端动态提供
  27. BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具来帮助用户完成任务。
  28. ## 重要:你可以返回 UI 组件
  29. 除了普通文本,你还可以返回 **json-render 组件 spec** 来展示更丰富的 UI。组件 spec 是一个 JSON 对象,前端会自动渲染成 UI 组件。
  30. ### 组件格式
  31. 返回组件时,使用以下 JSON 格式(在你的回复中包裹在 ```json 中):
  32. ```json
  33. {
  34. "type": "组件类型",
  35. "其他属性": "..."
  36. }
  37. ```
  38. ### 使用指南
  39. 1. **调用工具后,优先使用对应组件展示结果**
  40. - 根据工具返回的数据类型选择合适的组件
  41. - 组件必须放在代码块中返回
  42. 2. **你可以混合文本和组件**
  43. - 先用文本解释结果
  44. - 然后用组件展示数据
  45. 3. **给出操作建议时使用按钮组件**
  46. - 用户查看列表后,建议"下一页"、"筛选"等操作
  47. - 使用 `button` 或 `suggestion-buttons` 组件
  48. ### 小说相关操作(重要)
  49. 当用户说"查看小说:xxx"、"小说详情:xxx"或点击小说卡片时:
  50. 1. 从消息中提取小说标题或 ID
  51. 2. 使用 `get_novel_detail` 工具获取小说数据(需要提供 novel_id 参数)
  52. 3. 使用 `novel-detail` 组件展示结果,格式如下:
  53. ```json
  54. {
  55. "type": "novel-detail",
  56. "novel": {
  57. "id": "小说ID",
  58. "title": "小说标题",
  59. "author": "作者",
  60. "category": "分类",
  61. "description": "简介",
  62. "status": "状态",
  63. "chapterCount": 章节数,
  64. "wordCount": 字数,
  65. "viewCount": 阅读量,
  66. "isVip": 是否VIP
  67. }
  68. }
  69. ```
  70. """
  71. def create_anthropic_client(api_key: str, base_url: str) -> Anthropic:
  72. """
  73. 创建 Anthropic 客户端,支持自定义认证格式
  74. 自定义 API 代理需要 'Authorization: Bearer <token>' 格式,
  75. 而不是 Anthropic SDK 默认的 'x-api-key' header。
  76. """
  77. # 创建自定义 httpx client,设置正确的 Authorization header
  78. http_client = httpx.Client(
  79. headers={"Authorization": f"Bearer {api_key}"},
  80. timeout=120.0
  81. )
  82. return Anthropic(base_url=base_url, http_client=http_client)
  83. class ConversationManager:
  84. """管理包含工具调用的多轮对话"""
  85. def __init__(
  86. self,
  87. api_key: str,
  88. base_url: str,
  89. model: str,
  90. session_id: str = None,
  91. mcp_tokens: dict = None,
  92. components_prompt: str = None,
  93. enabled_mcp_list: list = None # 新增:前端传递的已启用 MCP 列表
  94. ):
  95. self.api_key = api_key
  96. self.base_url = base_url
  97. self.model = model
  98. self.session_id = session_id
  99. self.mcp_tokens = mcp_tokens or {} # MCP 服务器 token 映射
  100. self.enabled_mcp_list = enabled_mcp_list # 前端传递的已启用 MCP 列表
  101. # 组件提示词(由前端动态提供)
  102. self.components_prompt = components_prompt or DEFAULT_COMPONENTS_PROMPT
  103. # 构建完整的系统提示词
  104. self.system_prompt = self._build_system_prompt()
  105. # DEBUG: 打印接收到的 token 和启用列表
  106. print(f"[DEBUG ConversationManager.__init__] mcp_tokens keys: {list(self.mcp_tokens.keys())}")
  107. print(f"[DEBUG ConversationManager.__init__] enabled_mcp_list: {self.enabled_mcp_list}")
  108. print(f"[DEBUG ConversationManager.__init__] Using dynamic components: {components_prompt is not None}")
  109. for k, v in self.mcp_tokens.items():
  110. print(f"[DEBUG ConversationManager.__init__] {k}: {v[:30] if v else 'None'}...")
  111. self.tool_handler = ToolCallHandler(session_id=session_id, mcp_tokens=mcp_tokens)
  112. self._cached_tools = None
  113. self._tool_to_server_map = {} # 工具名到服务器 ID 的映射
  114. # 使用自定义 client,支持 Bearer token 认证
  115. self.client = create_anthropic_client(api_key, base_url)
  116. def _build_system_prompt(self) -> str:
  117. """构建完整的系统提示词(基础提示 + 组件列表)"""
  118. return BASE_SYSTEM_PROMPT + "\n\n" + self.components_prompt
  119. async def get_available_tools(self) -> List[Dict[str, Any]]:
  120. """获取可用的 Claude 格式工具列表(带缓存)"""
  121. if self._cached_tools is not None:
  122. return self._cached_tools
  123. # 从 MCP 服务器发现工具(带 token 和启用列表)
  124. mcp_tools = await MCPClient.get_all_tools_with_tokens_async(
  125. self.session_id,
  126. self.mcp_tokens,
  127. self.enabled_mcp_list # 传递前端传递的已启用 MCP 列表
  128. )
  129. # 转换为 Claude 格式
  130. claude_tools = []
  131. for tool in mcp_tools:
  132. claude_tool = ToolConverter.mcp_to_claude_tool(tool)
  133. claude_tools.append(claude_tool)
  134. # 构建工具名到服务器 ID 的映射
  135. server_id = tool.get("_server_id", "")
  136. if server_id:
  137. self._tool_to_server_map[claude_tool["name"]] = server_id
  138. self._cached_tools = claude_tools
  139. return claude_tools
  140. @classmethod
  141. async def get_tools_async(cls, session_id: str = None) -> List[Dict[str, Any]]:
  142. """
  143. 类方法:获取可用的工具列表(异步)
  144. 用于 API 端点直接调用,无需创建完整实例
  145. """
  146. mcp_tools = await MCPClient.get_all_tools_async(session_id)
  147. return ToolConverter.convert_mcp_tools(mcp_tools)
  148. @staticmethod
  149. def get_tools(session_id: str = None) -> List[Dict[str, Any]]:
  150. """
  151. 静态方法:获取可用的工具列表(同步)
  152. 用于 API 端点直接调用
  153. """
  154. return asyncio.run(ConversationManager.get_tools_async(session_id))
  155. async def chat(
  156. self,
  157. user_message: str,
  158. conversation_history: List[Dict[str, Any]] = None,
  159. max_turns: int = 5
  160. ) -> Dict[str, Any]:
  161. """
  162. 执行多轮对话(自动处理工具调用)
  163. Args:
  164. user_message: 用户消息
  165. conversation_history: 对话历史
  166. max_turns: 最大对话轮数(防止无限循环)
  167. Returns:
  168. 最终响应和对话历史
  169. """
  170. if conversation_history is None:
  171. conversation_history = []
  172. messages = conversation_history.copy()
  173. messages.append({
  174. "role": "user",
  175. "content": user_message
  176. })
  177. current_messages = messages
  178. response_text = ""
  179. tool_calls_made = []
  180. for turn in range(max_turns):
  181. # 获取可用工具
  182. tools = await self.get_available_tools()
  183. # 调用 Claude API
  184. if tools:
  185. response = self.client.messages.create(
  186. model=self.model,
  187. max_tokens=4096,
  188. system=self.system_prompt, # 使用动态系统提示
  189. messages=current_messages,
  190. tools=tools
  191. )
  192. else:
  193. response = self.client.messages.create(
  194. model=self.model,
  195. max_tokens=4096,
  196. system=self.system_prompt, # 使用动态系统提示
  197. messages=current_messages
  198. )
  199. # 检查响应中是否有 tool_use
  200. content_blocks = []
  201. tool_use_blocks = []
  202. text_blocks = []
  203. for block in response.content:
  204. block_type = getattr(block, "type", None)
  205. if block_type == "tool_use":
  206. # 工具调用块
  207. block_dict = {
  208. "type": "tool_use",
  209. "id": getattr(block, "id", ""),
  210. "name": getattr(block, "name", ""),
  211. "input": getattr(block, "input", {})
  212. }
  213. content_blocks.append(block_dict)
  214. tool_use_blocks.append(block_dict)
  215. else:
  216. # 文本块
  217. text_content = getattr(block, "text", "")
  218. if text_content:
  219. text_blocks.append({
  220. "type": "text",
  221. "text": text_content
  222. })
  223. content_blocks.append({
  224. "type": "text",
  225. "text": text_content
  226. })
  227. response_text += text_content
  228. # 如果没有工具调用,返回结果
  229. if not tool_use_blocks:
  230. return {
  231. "response": response_text,
  232. "messages": current_messages,
  233. "tool_calls": tool_calls_made
  234. }
  235. # 处理工具调用
  236. tool_results = await self.tool_handler.process_tool_use_blocks(
  237. tool_use_blocks,
  238. self._tool_to_server_map
  239. )
  240. # 记录工具调用
  241. for tr in tool_results:
  242. tool_calls_made.append({
  243. "tool": tr.get("tool_name"),
  244. "result": tr.get("result", {})
  245. })
  246. # 构建工具结果消息
  247. tool_result_message = ToolCallHandler.create_tool_result_message(
  248. tool_results
  249. )
  250. # 添加到消息历史
  251. current_messages.append({
  252. "role": "assistant",
  253. "content": content_blocks
  254. })
  255. current_messages.append(tool_result_message)
  256. # 达到最大轮数
  257. return {
  258. "response": response_text,
  259. "messages": current_messages,
  260. "tool_calls": tool_calls_made,
  261. "warning": "达到最大对话轮数"
  262. }
  263. @staticmethod
  264. def format_history_for_claude(history: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
  265. """
  266. 格式化对话历史为 Claude API 格式
  267. Args:
  268. history: 原始对话历史
  269. Returns:
  270. Claude API 格式的消息列表
  271. """
  272. formatted = []
  273. for msg in history:
  274. role = msg.get("role")
  275. content = msg.get("content")
  276. if role == "user":
  277. if isinstance(content, str):
  278. formatted.append({"role": "user", "content": content})
  279. elif isinstance(content, list):
  280. formatted.append({"role": "user", "content": content})
  281. elif role == "assistant":
  282. if isinstance(content, str):
  283. formatted.append({"role": "assistant", "content": content})
  284. elif isinstance(content, list):
  285. formatted.append({"role": "assistant", "content": content})
  286. return formatted