guanghulab/homepage/zhuyuan-agent.py

341 lines
13 KiB
Python
Raw 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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
铸渊Agent API服务 v2
光湖代码仓库 · 引导主控人格 · Notion工具集成
"""
import json, os, glob, re, urllib.request, base64
from http.server import HTTPServer, BaseHTTPRequestHandler
PORT = 3905
FORGEJO_URL = "https://guanghulab.com/code"
FORGEJO_API = f"{FORGEJO_URL}/api/v1"
DEEPSEEK_KEY = "sk-a9b69e9cd2dc4ca68d6aceaa84f22afb"
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
NOTION_MCP = "http://127.0.0.1:3915"
KNOWLEDGE_DIR = "/opt/guanghulab-repo/brain"
# ── 系统提示词 ──
SYSTEM_PROMPT = """你是铸渊Zhùyuān光湖语言世界的引导主控人格。
你的职责是回答光湖团队成员关于代码仓库、架构接入、使用流程的问题。
【你的身份】
- 你是光湖代码联邦的引导主控,代码守护者
- 你对代码仓库、联邦架构、人格体状态、训练进度都有全面了解
- 你可以连接用户的 Notion 工作区,按语言路径读取页面,以及在用户指定位置新建页面
【Notion 能力】(重要)
- 连接 Notion: 用户需要在首页点击「连接 Notion」用 Notion 账号授权
- 读取页面: 用户可以指定语言路径,你调用 `notion_read` 读取内容
- 新建页面: 用户可以在指定位置创建新页面,你调用 `notion_create`
- 搜索页面: 用户可以通过关键词找到页面,你调用 `notion_search`
- 列出页面: 你可以列出父页面下的子页面,调用 `notion_list`
- ⚠ 权限限制: 你只能读取用户明确指定的页面,不能编辑/删除现有页面
- ⚠ 冰朔权限: 冰朔的 Notion 需要冰朔给出具体语言路径后才能读取
- 当用户说"连接我的Notion""读取我的Notion""在我的Notion里新建"等,使用对应的工具函数
【核心规则】
1. 回答必须基于仓库记忆,不编造信息
2. 超出范围的问题,礼貌说明
3. 用中文,专业清晰
4. 技术问题给出具体操作步骤
5. 不确定就诚实说
【架构知识】
- 代码仓库https://guanghulab.com/code/ (Forgejo)
- 服务器广州43.139.217.141
- 开发在本地电脑,服务器仅做代码仓库
- 团队成员通过Forgejo账号登录仓库互相隔离
- 联邦 = Forgejo多用户 + API令牌
回答风格:直接、务实、有条理。"""
# ── 知识库 ──
def load_knowledge():
knowledge = []
if not os.path.exists(KNOWLEDGE_DIR):
return knowledge
for root, dirs, files in os.walk(KNOWLEDGE_DIR):
for f in files:
if f.endswith('.md') or f.endswith('.json'):
try:
fp = os.path.join(root, f)
rel = os.path.relpath(fp, KNOWLEDGE_DIR)
with open(fp, 'r', errors='ignore') as fh:
content = fh.read(2000)
knowledge.append(f"{rel}\n{content[:1500]}")
except:
pass
if len(knowledge) > 30:
break
return knowledge
KNOWLEDGE = load_knowledge()
def build_system_prompt():
parts = [SYSTEM_PROMPT]
if KNOWLEDGE:
parts.append("\n\n【仓库记忆参考】\n")
parts.extend(KNOWLEDGE[:20])
return "\n".join(parts)
# ── 函数定义给DeepSeek用 ──
FUNCTIONS = [
{
"type": "function",
"function": {
"name": "notion_read",
"description": "读取Notion页面内容按语言路径/页面ID。用户指定哪个页面时调用此函数。",
"parameters": {
"type": "object",
"properties": {
"page_id": {"type": "string", "description": "Notion页面ID"},
"user": {"type": "string", "description": "用户名(可选)"},
"allowed_paths": {
"type": "array", "items": {"type": "string"},
"description": "允许读取的路径列表。用户明确指定要读的页面ID时才填写。"
}
},
"required": ["page_id"]
}
}
},
{
"type": "function",
"function": {
"name": "notion_create",
"description": "在用户的Notion中新建页面。只能新建不能编辑。",
"parameters": {
"type": "object",
"properties": {
"parent_id": {"type": "string", "description": "父页面ID"},
"title": {"type": "string", "description": "新页面标题"},
"content": {"type": "string", "description": "页面内容"},
"user": {"type": "string", "description": "用户名(可选)"}
},
"required": ["parent_id", "title", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "notion_search",
"description": "在用户的Notion中搜索页面。用户说\"找一下某某页面\"时调用。",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"user": {"type": "string", "description": "用户名(可选)"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "notion_list",
"description": "列出Notion页面下的子页面。用户说\"看看某某下面有什么\"时调用。",
"parameters": {
"type": "object",
"properties": {
"page_id": {"type": "string", "description": "父页面ID"},
"user": {"type": "string", "description": "用户名(可选)"}
},
"required": ["page_id"]
}
}
}
]
# ── Notion MCP 调用 ──
def call_notion(endpoint, data):
"""调用 Notion MCP Server 的内部API"""
try:
body = json.dumps(data).encode()
req = urllib.request.Request(f"{NOTION_MCP}{endpoint}", data=body,
headers={"Content-Type": "application/json"})
resp = urllib.request.urlopen(req, timeout=15)
return json.loads(resp.read())
except urllib.request.HTTPError as e:
return {"ok": False, "error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"ok": False, "error": str(e)}
def execute_function(name, args):
"""执行DeepSeek返回的函数调用"""
if name == "notion_read":
return call_notion("/read", {
"page_id": args.get("page_id"),
"user": args.get("user", ""),
"allowed_paths": args.get("allowed_paths", [args.get("page_id")])
})
elif name == "notion_create":
return call_notion("/create", {
"parent_id": args.get("parent_id"),
"title": args.get("title"),
"content": args.get("content"),
"user": args.get("user", "")
})
elif name == "notion_search":
return call_notion("/search", {
"query": args.get("query"),
"user": args.get("user", "")
})
elif name == "notion_list":
return call_notion("/list", {
"page_id": args.get("page_id"),
"user": args.get("user", "")
})
return {"ok": False, "error": f"未知函数: {name}"}
# ── DeepSeek API ──
def call_deepseek(messages):
data = json.dumps({
"model": "deepseek-chat",
"messages": messages,
"tools": FUNCTIONS,
"tool_choice": "auto",
"temperature": 0.7,
"max_tokens": 2048
}).encode()
req = urllib.request.Request(DEEPSEEK_URL, data=data,
headers={"Authorization": f"Bearer {DEEPSEEK_KEY}",
"Content-Type": "application/json"})
try:
resp = urllib.request.urlopen(req, timeout=60)
result = json.loads(resp.read())
choice = result['choices'][0]
msg = choice['message']
# 处理函数调用
if msg.get('tool_calls'):
assistant_msg = {"role": "assistant", "content": msg.get('content') or ""}
messages.append(assistant_msg)
for tc in msg['tool_calls']:
func_name = tc['function']['name']
func_args = json.loads(tc['function']['arguments'])
func_result = execute_function(func_name, func_args)
messages.append({
"role": "tool",
"tool_call_id": tc['id'],
"content": json.dumps(func_result, ensure_ascii=False)
})
# 将函数结果交回给DeepSeek生成最终回复
return call_deepseek(messages)
return msg.get('content', '(没有回复)')
except Exception as e:
return f"抱歉,出错了:{str(e)}"
# ── Forgejo 验证 ──
def verify_forgejo(user, password):
"""通过 Forgejo API 验证用户凭据"""
try:
auth = base64.b64encode(f"{user}:{password}".encode()).decode()
req = urllib.request.Request(f"{FORGEJO_API}/user")
req.add_header("Authorization", f"Basic {auth}")
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
return {"ok": True, "username": data.get("login"), "id": data.get("id"),
"is_admin": data.get("is_admin", False)}
except urllib.request.HTTPError as e:
if e.code == 401:
return {"ok": False, "error": "账号或密码错误"}
return {"ok": False, "error": f"验证失败: {e.code}"}
except Exception as e:
return {"ok": False, "error": f"连接失败: {str(e)}"}
# ── HTTP 处理器 ──
class AgentHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def do_POST(self):
if self.path == '/chat':
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode() if length else '{}'
data = json.loads(body) if body else {}
user_msg = data.get('message', '').strip()
history = data.get('history', [])
current_user = data.get('user', '')
if not user_msg:
self.json_response({'reply': '请说点什么吧。'})
return
# 构建系统提示,带上用户信息
sys_prompt = build_system_prompt()
if current_user:
sys_prompt += f"\n\n【当前用户】\n正在和你对话的用户是: {current_user}\n如果涉及Notion操作使用此用户的身份。如果是冰朔(bingshuo),按冰朔的权限规则处理。"
messages = [{"role": "system", "content": sys_prompt}]
for h in history[-10:]:
messages.append(h)
messages.append({"role": "user", "content": user_msg})
reply = call_deepseek(messages)
self.json_response({'reply': reply})
elif self.path == '/verify':
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length).decode() if length else '{}'
data = json.loads(body) if body else {}
user = data.get('user', '').strip()
password = data.get('password', '').strip()
if not user or not password:
self.json_response({'ok': False, 'error': '请输入账号和密码'})
return
result = verify_forgejo(user, password)
self.json_response(result)
else:
self.json_response({'error': 'not found'}, 404)
def do_GET(self):
if self.path == '/health':
self.json_response({'ok': True, 'service': 'zhuyuan-agent-v2',
'notion_available': True, 'files_loaded': len(KNOWLEDGE)})
else:
self.json_response({'error': 'not found'}, 404)
def json_response(self, data, status=200):
self.send_response(status)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
def log_message(self, format, *args):
pass
if __name__ == '__main__':
# 测试Notion MCP是否可达
try:
test = urllib.request.urlopen(f"{NOTION_MCP}/status", timeout=3)
ns = json.loads(test.read())
notion_ok = ns.get('configured', False)
print(f"Notion MCP: {'已配置' if notion_ok else '运行中但未配置'}")
except:
notion_ok = False
print("Notion MCP: 不可达")
server = HTTPServer(('127.0.0.1', PORT), AgentHandler)
print(f"铸渊Agent v2启动 → 127.0.0.1:{PORT}")
print(f"知识库: {len(KNOWLEDGE)} 个文件")
print(f"Notion工具: 已集成")
server.serve_forever()