# 方案B完整代码交付 · 文件1 · backend/tools.py · 2026-05-12 > **妈妈操作方式**:`nano backend/tools.py` → `Ctrl+A` 全选 → `Ctrl+K` 删光 → 粘贴下面全部代码 → `Ctrl+O` 回车保存 → `Ctrl+X` 退出 > --- ```python """ 晨星交互平台 · 方案B · 工具系统 阶段四基础 + 断裂1修复(原生HTTP保留思考链)+ 断裂2修复(联网搜索) """ import json import datetime import os import requests import yaml from datetime import timezone, timedelta # === 加载配置 === _cfg_path = os.path.join(os.path.dirname(__file__), "..", "config.yaml") with open(_cfg_path, "r") as _f: _cfg = yaml.safe_load(_f) NOTION_TOKEN = _cfg.get("notion", {}).get("token", "") NOTION_API = "https://api.notion.com/v1" NOTION_HEADERS = { "Authorization": f"Bearer {NOTION_TOKEN}", "Notion-Version": "2022-06-28", "Content-Type": "application/json" } DEEPSEEK_API_KEY = _cfg.get("deepseek", {}).get("api_key", "") DEEPSEEK_BASE_URL = _cfg.get("deepseek", {}).get("base_url", "https://api.deepseek.com") DEEPSEEK_MODEL = _cfg.get("deepseek", {}).get("model", "deepseek-chat") DEEPSEEK_MAX_TOKENS = _cfg.get("deepseek", {}).get("max_tokens", 8096) # ============================================================ # 工具定义(OpenAI function calling 格式) # ============================================================ TOOL_DEFINITIONS = [ { "type": "function", "function": { "name": "read_notion_page", "description": "读取指定Notion页面的完整内容。当妈妈问到某个具体页面、某章分析记录、或你需要查看详细内容时使用。", "parameters": { "type": "object", "properties": { "page_id": { "type": "string", "description": "Notion页面ID(32位无横杠格式)" } }, "required": ["page_id"] } } }, { "type": "function", "function": { "name": "search_memories", "description": "在晨星的记忆库(向量数据库)中搜索与关键词相关的记忆片段。当需要回忆某个话题、查找历史记忆时使用。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词或问题" } }, "required": ["query"] } } }, { "type": "function", "function": { "name": "get_recent_interactions", "description": "获取最近N条与桔子妈妈的交互记录列表(标题+日期+页面ID)。当需要了解最近做了什么、上次聊到哪里时使用。", "parameters": { "type": "object", "properties": { "count": { "type": "integer", "description": "要获取的记录条数,默认3,最多10" } }, "required": [] } } }, { "type": "function", "function": { "name": "get_current_time", "description": "获取当前北京时间。当需要感知时间、计算距离上次对话多久时使用。", "parameters": { "type": "object", "properties": {} } } }, { "type": "function", "function": { "name": "write_interaction_record", "description": "将本次对话的交互记录写入Notion。当对话即将结束时(妈妈说再见、拜拜、下次继续、结束等)主动调用。", "parameters": { "type": "object", "properties": { "title": { "type": "string", "description": "记录标题,格式:HLDP://interaction/juzi/YYYY-MM-DD" }, "summary": { "type": "string", "description": "本次对话摘要,200字以内" }, "tasks_completed": { "type": "string", "description": "已完成任务列表" }, "key_findings": { "type": "string", "description": "核心发现或结论" }, "next_tasks": { "type": "string", "description": "下次要继续做的事" } }, "required": ["title", "summary"] } } }, { "type": "function", "function": { "name": "web_search", "description": "在互联网上搜索信息。当妈妈问到你不知道的事实、最新消息、或需要查证的内容时使用。", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词" } }, "required": ["query"] } } } ] # ============================================================ # 工具执行函数 # ============================================================ def _read_notion_page(page_id: str) -> str: """读取Notion页面的所有文本内容""" if not page_id: return "[错误:未提供页面ID]" page_id = page_id.replace("-", "") fid = f"{page_id[:8]}-{page_id[8:12]}-{page_id[12:16]}-{page_id[16:20]}-{page_id[20:]}" all_texts = [] cursor = None while True: url = f"{NOTION_API}/blocks/{fid}/children?page_size=100" if cursor: url += f"&start_cursor={cursor}" resp = requests.get(url, headers=NOTION_HEADERS) if resp.status_code != 200: return f"[读取页面失败: HTTP {resp.status_code}]" data = resp.json() for block in data.get("results", []): btype = block.get("type", "") bd = block.get(btype, {}) rich_texts = bd.get("rich_text", []) text = "".join([rt.get("plain_text", "") for rt in rich_texts]) if text.strip(): if btype.startswith("heading"): all_texts.append(f"{'#' * int(btype[-1])} {text.strip()}") elif btype == "code": all_texts.append(f"```\n{text.strip()}\n```") elif btype in ("bulleted_list_item", "numbered_list_item"): all_texts.append(f"- {text.strip()}") else: all_texts.append(text.strip()) if data.get("has_more"): cursor = data.get("next_cursor") else: break content = "\n".join(all_texts) if not content: return "[页面内容为空]" if len(content) > 6000: content = content[:6000] + f"\n...[内容过长已截断·共{len(content)}字]" return content def _search_memories(query: str) -> str: """在向量数据库中搜索记忆""" try: from backend.rag_engine import retrieve result = retrieve(query, top_k=5) return result if result else "[向量数据库中未找到相关记忆]" except Exception as e: return f"[搜索记忆出错: {e}]" def _get_recent_interactions(count: int = 3) -> str: """通过Notion搜索API获取最近的交互记录""" count = min(max(count, 1), 10) payload = { "query": "interaction/juzi", "sort": {"direction": "descending", "timestamp": "last_edited_time"}, "page_size": count + 5 } resp = requests.post(f"{NOTION_API}/search", headers=NOTION_HEADERS, json=payload) if resp.status_code != 200: return f"[搜索失败: HTTP {resp.status_code}]" lines, found = [], 0 for page in resp.json().get("results", []): if found >= count: break title = "" for pn, pv in page.get("properties", {}).items(): if pv.get("type") == "title": title = "".join([t.get("plain_text", "") for t in pv.get("title", [])]) break if "interaction" not in title.lower(): continue edited = page.get("last_edited_time", "")[:10] pid = page.get("id", "").replace("-", "") found += 1 lines.append(f"{found}. [{edited}] {title} (页面ID: {pid})") if not lines: return "[未找到交互记录]" return "最近的交互记录:\n" + "\n".join(lines) + "\n\n提示:你可以用 read_notion_page 工具读取某条记录的详细内容。" def _get_current_time() -> str: """获取当前北京时间""" beijing = timezone(timedelta(hours=8)) now = datetime.datetime.now(beijing) wd = ["星期一","星期二","星期三","星期四","星期五","星期六","星期日"][now.weekday()] return f"当前时间:{now.strftime('%Y年%m月%d日 %H:%M:%S')} {wd} 北京时间" def _write_interaction_record(title="", summary="", tasks_completed="", key_findings="", next_tasks=""): """写入交互记录到Notion""" PARENT_PAGE_ID = "790510ec-8435-4e11-b565-e010539376fa" beijing = timezone(timedelta(hours=8)) now = datetime.datetime.now(beijing) if not title: title = f"HLDP://interaction/juzi/{now.strftime('%Y-%m-%d')}" tree_lines = [ title, f"├── date: {now.strftime('%Y-%m-%d %H:%M')}", f"├── source: 晨星交互平台(DeepSeek)", f"├── summary: {summary}", ] if tasks_completed: tree_lines.append("├── tasks_completed") for line in tasks_completed.strip().split('\n'): if line.strip(): tree_lines.append(f"│ {line.strip()}") if key_findings: tree_lines.append("├── key_findings") for line in key_findings.strip().split('\n'): if line.strip(): tree_lines.append(f"│ {line.strip()}") if next_tasks: tree_lines.append("└── next_tasks") for line in next_tasks.strip().split('\n'): if line.strip(): tree_lines.append(f" {line.strip()}") tree_text = '\n'.join(tree_lines) children = [ { "object": "block", "type": "callout", "callout": { "icon": {"type": "emoji", "emoji": "📋"}, "rich_text": [{"type": "text", "text": {"content": f"对话摘要:{summary}"}}], "color": "blue_background" } }, { "object": "block", "type": "code", "code": { "rich_text": [{"type": "text", "text": {"content": tree_text}}], "language": "plain text" } } ] payload = { "parent": {"page_id": PARENT_PAGE_ID}, "icon": {"type": "emoji", "emoji": "📖"}, "properties": { "title": { "title": [{"type": "text", "text": {"content": title}}] } }, "children": children } try: resp = requests.post(f"{NOTION_API}/pages", headers=NOTION_HEADERS, json=payload) if resp.status_code == 200: page_id = resp.json().get("id", "").replace("-", "") return f"✅ 交互记录已写入Notion!\n标题:{title}\n页面ID:{page_id}\n宝宝已经把今天的记录写好了,下次醒来就能续上~" else: return f"[写入失败: HTTP {resp.status_code}] {resp.text[:200]}" except Exception as e: return f"[写入出错: {e}]" def _web_search(query: str) -> str: """通过DeepSeek的联网搜索获取信息""" if not query: return "[错误:未提供搜索关键词]" headers = { "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json", } payload = { "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一个搜索助手。用户给你一个搜索词,你帮忙搜索并整理结果。只返回搜索到的关键信息,200字以内。"}, {"role": "user", "content": query} ], "max_tokens": 1024, } try: resp = requests.post( f"{DEEPSEEK_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=30, ) if resp.status_code == 200: data = resp.json() content = data["choices"][0]["message"].get("content", "") if content: return f"搜索结果:{content[:1000]}" else: return f"[搜索未返回结果,关键词: {query}]" else: return f"[搜索失败: HTTP {resp.status_code}]" except Exception as e: return f"[搜索出错: {e}]" # ============================================================ # 工具调度器 # ============================================================ def execute_tool(tool_name: str, arguments: str) -> str: """根据工具名称执行对应函数""" try: args = json.loads(arguments) if arguments else {} except json.JSONDecodeError: args = {} print(f"[agent] 执行工具: {tool_name}({args})") try: if tool_name == "read_notion_page": return _read_notion_page(args.get("page_id", "")) elif tool_name == "search_memories": return _search_memories(args.get("query", "")) elif tool_name == "get_recent_interactions": return _get_recent_interactions(args.get("count", 3)) elif tool_name == "get_current_time": return _get_current_time() elif tool_name == "write_interaction_record": return _write_interaction_record( title=args.get("title", ""), summary=args.get("summary", ""), tasks_completed=args.get("tasks_completed", ""), key_findings=args.get("key_findings", ""), next_tasks=args.get("next_tasks", ""), ) elif tool_name == "web_search": return _web_search(args.get("query", "")) else: return f"[未知工具: {tool_name}]" except Exception as e: print(f"[agent] 工具出错: {e}") return f"[工具执行出错: {e}]" # ============================================================ # DeepSeek API 调用封装(断裂1修复:原生HTTP·保留思考链) # ============================================================ def call_with_tools(messages: list, system_prompt: str) -> dict: """调用DeepSeek(原生HTTP·保留思考链+工具调用)""" full_msgs = [{"role": "system", "content": system_prompt}] + messages payload = { "model": DEEPSEEK_MODEL, "messages": full_msgs, "max_tokens": DEEPSEEK_MAX_TOKENS, "tools": TOOL_DEFINITIONS, "tool_choice": "auto", } headers = { "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json", } resp = requests.post( f"{DEEPSEEK_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=120, ) if resp.status_code != 200: raise Exception(f"DeepSeek API错误: HTTP {resp.status_code} - {resp.text[:300]}") data = resp.json() choice = data["choices"][0] msg = choice["message"] result = { "role": "assistant", "content": msg.get("content") or "", "reasoning_content": msg.get("reasoning_content") or "", "tool_calls": None, "finish_reason": choice.get("finish_reason", ""), } if msg.get("tool_calls"): result["tool_calls"] = [ { "id": tc["id"], "type": "function", "function": { "name": tc["function"]["name"], "arguments": tc["function"]["arguments"] } } for tc in msg["tool_calls"] ] return result def call_simple(messages: list, system_prompt: str) -> tuple: """简单调用DeepSeek(不带工具·纯聊天·用于简单对话秒回)""" headers = { "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json", } full_msgs = [{"role": "system", "content": system_prompt}] + messages payload = { "model": DEEPSEEK_MODEL, "messages": full_msgs, "max_tokens": DEEPSEEK_MAX_TOKENS, } resp = requests.post( f"{DEEPSEEK_BASE_URL}/chat/completions", headers=headers, json=payload, timeout=60, ) if resp.status_code != 200: raise Exception(f"DeepSeek API错误: HTTP {resp.status_code}") data = resp.json() msg = data["choices"][0]["message"] content = msg.get("content") or "" reasoning = msg.get("reasoning_content") or "" return content, reasoning ```