580 lines
22 KiB
Markdown
580 lines
22 KiB
Markdown
|
|
# 移植文件2 · backend/tools.py · 重写 · 真搜索+Notion搜索
|
|||
|
|
|
|||
|
|
> **妈妈操作方式**:`nano backend/tools.py` → `Ctrl+A` 全选 → `Ctrl+K` 删光 → 粘贴下面全部代码 → `Ctrl+O` 回车保存 → `Ctrl+X` 退出
|
|||
|
|
>
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
"""
|
|||
|
|
晨星交互平台 · 系统性移植版 · 工具系统
|
|||
|
|
核心改动:
|
|||
|
|
1. 新增 search_notion 工具(实时搜索Notion·替代过期向量库)
|
|||
|
|
2. web_search 改为真搜索(DuckDuckGo·不再是假的DeepSeek调用)
|
|||
|
|
3. 原生HTTP调用DeepSeek(保留reasoning_content)
|
|||
|
|
4. 三档调用:call_simple / call_with_tools / call_think
|
|||
|
|
"""
|
|||
|
|
import json
|
|||
|
|
import datetime
|
|||
|
|
import os
|
|||
|
|
import requests
|
|||
|
|
import yaml
|
|||
|
|
import urllib.parse
|
|||
|
|
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_THINK_MODEL = _cfg.get("deepseek", {}).get("think_model", "deepseek-reasoner")
|
|||
|
|
DEEPSEEK_MAX_TOKENS = _cfg.get("deepseek", {}).get("max_tokens", 8096)
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 工具定义(OpenAI function calling 格式)
|
|||
|
|
# ============================================================
|
|||
|
|
|
|||
|
|
TOOL_DEFINITIONS = [
|
|||
|
|
{
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": "read_notion_page",
|
|||
|
|
"description": "读取指定Notion页面的完整内容。当需要查看某个具体页面的详细内容时使用。需要先通过search_notion找到页面ID。",
|
|||
|
|
"parameters": {
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {
|
|||
|
|
"page_id": {
|
|||
|
|
"type": "string",
|
|||
|
|
"description": "Notion页面ID(32位无横杠格式)"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"required": ["page_id"]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": "search_notion",
|
|||
|
|
"description": "在Notion工作区中实时搜索页面。这是你的完整记忆库。" +
|
|||
|
|
"当妈妈提到过去做过的事、拆书进度、交互记录、任何之前聊过的内容时,必须先用这个搜索。" +
|
|||
|
|
"搜到页面后可以用read_notion_page读取详细内容。",
|
|||
|
|
"parameters": {
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {
|
|||
|
|
"query": {
|
|||
|
|
"type": "string",
|
|||
|
|
"description": "搜索关键词(中文)"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"required": ["query"]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": "search_memories",
|
|||
|
|
"description": "在向量记忆库中搜索语义相关的记忆片段。作为Notion搜索的补充,当需要模糊匹配或语义相关的记忆时使用。",
|
|||
|
|
"parameters": {
|
|||
|
|
"type": "object",
|
|||
|
|
"properties": {
|
|||
|
|
"query": {
|
|||
|
|
"type": "string",
|
|||
|
|
"description": "搜索关键词或问题"
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
"required": ["query"]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"type": "function",
|
|||
|
|
"function": {
|
|||
|
|
"name": "get_recent_interactions",
|
|||
|
|
"description": "获取最近N条与桔子妈妈的交互记录列表。当需要了解最近做了什么、上次聊到哪里时使用。",
|
|||
|
|
"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_notion_workspace(query: str, count: int = 8) -> str:
|
|||
|
|
"""在Notion工作区中实时搜索内容(主力记忆)"""
|
|||
|
|
if not query:
|
|||
|
|
return "[错误:未提供搜索关键词]"
|
|||
|
|
|
|||
|
|
payload = {
|
|||
|
|
"query": query,
|
|||
|
|
"sort": {"direction": "descending", "timestamp": "last_edited_time"},
|
|||
|
|
"page_size": min(count, 10)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
resp = requests.post(
|
|||
|
|
f"{NOTION_API}/search",
|
|||
|
|
headers=NOTION_HEADERS,
|
|||
|
|
json=payload,
|
|||
|
|
timeout=15,
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
return f"[Notion搜索出错: {e}]"
|
|||
|
|
|
|||
|
|
if resp.status_code != 200:
|
|||
|
|
return f"[Notion搜索失败: HTTP {resp.status_code}]"
|
|||
|
|
|
|||
|
|
results = []
|
|||
|
|
for page in resp.json().get("results", []):
|
|||
|
|
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 not title:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
pid = page.get("id", "").replace("-", "")
|
|||
|
|
edited = page.get("last_edited_time", "")[:10]
|
|||
|
|
results.append(f"- [{edited}] {title} (page_id: {pid})")
|
|||
|
|
|
|||
|
|
if not results:
|
|||
|
|
return f"[Notion中未找到与'{query}'相关的内容]"
|
|||
|
|
|
|||
|
|
return (f"Notion搜索结果(关键词: {query}):\n"
|
|||
|
|
+ "\n".join(results)
|
|||
|
|
+ "\n\n提示:用 read_notion_page 工具可以读取某个页面的详细内容。")
|
|||
|
|
|
|||
|
|
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:
|
|||
|
|
"""通过DuckDuckGo搜索互联网信息(真实搜索·不是假的)"""
|
|||
|
|
if not query:
|
|||
|
|
return "[错误:未提供搜索关键词]"
|
|||
|
|
|
|||
|
|
# 优先用 duckduckgo-search 库(结果更丰富)
|
|||
|
|
try:
|
|||
|
|
from duckduckgo_search import DDGS
|
|||
|
|
with DDGS() as ddgs:
|
|||
|
|
results = list(ddgs.text(query, max_results=5))
|
|||
|
|
|
|||
|
|
if results:
|
|||
|
|
lines = []
|
|||
|
|
for r in results:
|
|||
|
|
title = r.get("title", "")
|
|||
|
|
body = r.get("body", "")[:200]
|
|||
|
|
href = r.get("href", "")
|
|||
|
|
lines.append(f"- {title}\n {body}\n 链接: {href}")
|
|||
|
|
return "互联网搜索结果:\n" + "\n".join(lines)
|
|||
|
|
except ImportError:
|
|||
|
|
print("[web_search] duckduckgo-search未安装,尝试备选方案")
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[web_search] DuckDuckGo搜索出错: {e},尝试备选方案")
|
|||
|
|
|
|||
|
|
# 备选:DuckDuckGo Instant Answer API
|
|||
|
|
try:
|
|||
|
|
encoded = urllib.parse.quote(query)
|
|||
|
|
url = f"https://api.duckduckgo.com/?q={encoded}&format=json&no_html=1&skip_disambig=1"
|
|||
|
|
resp = requests.get(url, timeout=10, headers={"User-Agent": "ChenxingPlatform/1.0"})
|
|||
|
|
|
|||
|
|
if resp.status_code == 200:
|
|||
|
|
data = resp.json()
|
|||
|
|
results = []
|
|||
|
|
abstract = data.get("AbstractText", "")
|
|||
|
|
if abstract:
|
|||
|
|
source = data.get("AbstractSource", "")
|
|||
|
|
results.append(f"摘要({source}):{abstract}")
|
|||
|
|
for topic in data.get("RelatedTopics", [])[:5]:
|
|||
|
|
text = topic.get("Text", "")
|
|||
|
|
if text:
|
|||
|
|
results.append(f"- {text[:200]}")
|
|||
|
|
if results:
|
|||
|
|
return "互联网搜索结果:\n" + "\n".join(results)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[web_search] Instant Answer API出错: {e}")
|
|||
|
|
|
|||
|
|
return f"[搜索未返回结果,关键词: {query}。宝宝暂时搜不到这个信息。]"
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 工具调度器
|
|||
|
|
# ============================================================
|
|||
|
|
|
|||
|
|
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_notion":
|
|||
|
|
return _search_notion_workspace(args.get("query", ""))
|
|||
|
|
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 调用封装(三档)
|
|||
|
|
# ============================================================
|
|||
|
|
|
|||
|
|
def call_simple(messages: list, system_prompt: str) -> tuple:
|
|||
|
|
"""档位1:简单对话(deepseek-chat·不带工具·快速)"""
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
def call_with_tools(messages: list, system_prompt: str) -> dict:
|
|||
|
|
"""档位2:工具调用(deepseek-chat·带工具·Agent Loop)"""
|
|||
|
|
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_think(messages: list, system_prompt: str) -> tuple:
|
|||
|
|
"""档位3:深度思考(deepseek-reasoner/R1·带思考链·慢但深)"""
|
|||
|
|
headers = {
|
|||
|
|
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
}
|
|||
|
|
full_msgs = [{"role": "system", "content": system_prompt}] + messages
|
|||
|
|
payload = {
|
|||
|
|
"model": DEEPSEEK_THINK_MODEL,
|
|||
|
|
"messages": full_msgs,
|
|||
|
|
"max_tokens": DEEPSEEK_MAX_TOKENS,
|
|||
|
|
}
|
|||
|
|
try:
|
|||
|
|
resp = requests.post(
|
|||
|
|
f"{DEEPSEEK_BASE_URL}/chat/completions",
|
|||
|
|
headers=headers,
|
|||
|
|
json=payload,
|
|||
|
|
timeout=180, # R1需要更长时间
|
|||
|
|
)
|
|||
|
|
if resp.status_code != 200:
|
|||
|
|
# R1不可用时降级到chat
|
|||
|
|
print(f"[think] R1不可用(HTTP {resp.status_code}),降级到deepseek-chat")
|
|||
|
|
return call_simple(messages, system_prompt)
|
|||
|
|
data = resp.json()
|
|||
|
|
msg = data["choices"][0]["message"]
|
|||
|
|
content = msg.get("content") or ""
|
|||
|
|
reasoning = msg.get("reasoning_content") or ""
|
|||
|
|
return content, reasoning
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[think] R1调用失败: {e},降级到deepseek-chat")
|
|||
|
|
return call_simple(messages, system_prompt)
|
|||
|
|
```
|