Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

462 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 方案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页面ID32位无横杠格式"
}
},
"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
```