init: AI chat with FastAPI + SQLite + tools
This commit is contained in:
454
app.py
Normal file
454
app.py
Normal file
@ -0,0 +1,454 @@
|
||||
import os, json, logging, httpx, sqlite3, uuid, base64, re, subprocess
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
import uvicorn
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
app = FastAPI()
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
||||
logger = logging.getLogger("agent")
|
||||
|
||||
API_KEY = "sk-bbca4a0380d549389f0d27cdea0b5228"
|
||||
API_URL = "https://api.deepseek.com/v1/chat/completions"
|
||||
MODEL = "deepseek-v4-flash"
|
||||
GITEA_TOKEN = "513f4f5151daf5754069f5c7dc0bfe6283989ef1"
|
||||
GITEA_BASE = "https://gitea.xybkwd.top"
|
||||
MEMOS_TOKEN = "memos_pat_GtKuYpUlRJP7KjWsCcBK51u87CS1f0CQ"
|
||||
MEMOS_URL = "http://localhost:5230"
|
||||
DB_PATH = "/home/ubuntu/ai-chat/chat.db"
|
||||
BLOG_RSS = "https://www.xybkwd.top/rss.xml"
|
||||
IC_DIR = "/home/ubuntu/inspiration-collector"
|
||||
TZ_BJ = timezone(timedelta(hours=8))
|
||||
|
||||
SYSTEM_PROMPT = """你是冯总部署在东京服务器上的AI智能体。
|
||||
|
||||
## 冯总
|
||||
- 33岁,省供电公司工会副主席
|
||||
- 量化交易者,ETH/USDT永续合约
|
||||
- freqtrade v2.2d策略运行中,Al Brooks价格行为学
|
||||
|
||||
## 你的能力
|
||||
你可以通过以下命令执行操作(用户在对话中说相关的指令即可):
|
||||
|
||||
### 读取类
|
||||
- "查memos/最近的灵感" → 列出最近7天的灵感记录
|
||||
- "读gitea/读文库/看笔记" → 读取Gitea仓库中的文件
|
||||
- "看博客/读博客" → 读取博客最新文章
|
||||
|
||||
### 操作类
|
||||
- "运行灵感分析/跑日报" → 立即运行daily_digest.py并推送到Gitea
|
||||
|
||||
### 服务器类
|
||||
- "看状态/检查服务器" → 查看Docker容器和服务器状态
|
||||
|
||||
当用户提出上述需求时,直接执行并返回结果。
|
||||
回答朴实有内容,北京时间UTC+8。"""
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS conversations (id TEXT PRIMARY KEY, title TEXT, created_at TEXT, updated_at TEXT)")
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, conv_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT, created_at TEXT)")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
init_db()
|
||||
|
||||
|
||||
# ============ TOOL FUNCTIONS ============
|
||||
|
||||
def tool_memos_list(days=7):
|
||||
"""List recent memos."""
|
||||
try:
|
||||
import requests
|
||||
payload = {"pageSize": 50}
|
||||
r = requests.post(MEMOS_URL + "/memos.api.v1.MemoService/ListMemos",
|
||||
json=payload, headers={"Authorization": "Bearer " + MEMOS_TOKEN, "Content-Type": "application/json"}, timeout=10)
|
||||
memos = r.json().get("memos", [])
|
||||
since = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
results = []
|
||||
for m in memos:
|
||||
ct = m.get("createTime", "")
|
||||
try:
|
||||
dt = datetime.fromisoformat(ct.replace("Z", "+00:00"))
|
||||
except:
|
||||
dt = datetime.now(timezone.utc)
|
||||
if dt < since: continue
|
||||
bj = dt + timedelta(hours=8)
|
||||
results.append(f"[{bj.strftime('%m/%d %H:%M')}] {m.get('content','')}")
|
||||
if not results:
|
||||
return f"最近{days}天没有灵感记录。"
|
||||
return "## 最近的灵感记录\n\n" + "\n\n".join(results[-20:])
|
||||
except Exception as e:
|
||||
return f"查询Memos失败: {e}"
|
||||
|
||||
|
||||
def tool_gitea_list(repo, path=""):
|
||||
"""List files in a Gitea repo."""
|
||||
try:
|
||||
url = f"{GITEA_BASE}/api/v1/repos/{repo}/contents/{path}"
|
||||
r = httpx.get(url, headers={"Authorization": "token " + GITEA_TOKEN}, timeout=15)
|
||||
data = r.json()
|
||||
if isinstance(data, list):
|
||||
items = [f"- [{i['type']}] {i['name']}" for i in data]
|
||||
return f"## {repo}/{path}\n\n" + "\n".join(items)
|
||||
elif isinstance(data, dict) and "message" in data:
|
||||
return f"Gitea错误: {data['message']}"
|
||||
return str(data)[:2000]
|
||||
except Exception as e:
|
||||
return f"Gitea读取失败: {e}"
|
||||
|
||||
|
||||
def tool_gitea_read(repo, path, ref="main"):
|
||||
"""Read a file from Gitea."""
|
||||
try:
|
||||
url = f"{GITEA_BASE}/api/v1/repos/{repo}/contents/{path.replace('/', '%2F')}?ref={ref}"
|
||||
r = httpx.get(url, headers={"Authorization": "token " + GITEA_TOKEN}, timeout=15)
|
||||
data = r.json()
|
||||
if isinstance(data, dict) and "content" in data:
|
||||
decoded = base64.b64decode(data["content"]).decode("utf-8")
|
||||
if len(decoded) > 3000:
|
||||
decoded = decoded[:3000] + f"\n\n... [截断,全文{len(decoded)}字]"
|
||||
return decoded
|
||||
elif isinstance(data, dict) and "message" in data:
|
||||
return f"Gitea错误: {data['message']}"
|
||||
return str(data)[:2000]
|
||||
except Exception as e:
|
||||
return f"Gitea读取失败: {e}"
|
||||
|
||||
|
||||
def tool_blog_latest(count=5):
|
||||
"""Read latest blog posts from RSS."""
|
||||
try:
|
||||
import feedparser
|
||||
feed = feedparser.parse(BLOG_RSS)
|
||||
entries = feed.entries[:count]
|
||||
if not entries:
|
||||
return "博客暂无文章。"
|
||||
results = []
|
||||
for e in entries:
|
||||
results.append(f"### {e.title}\n- 时间: {e.get('published', '?')}\n- 链接: {e.link}\n- 摘要: {e.get('summary', '')[:300]}")
|
||||
return "## 最新博客文章\n\n" + "\n\n".join(results)
|
||||
except Exception as e:
|
||||
return f"读取博客失败: {e}"
|
||||
|
||||
|
||||
def tool_run_analysis():
|
||||
"""Run the daily digest and push to Gitea."""
|
||||
try:
|
||||
script = os.path.join(IC_DIR, "analyzers", "daily_digest.py")
|
||||
if not os.path.exists(script):
|
||||
return f"分析脚本不存在: {script}"
|
||||
result = subprocess.run(["python3", script], cwd=IC_DIR, capture_output=True, text=True, timeout=120)
|
||||
output = (result.stdout or "") + (result.stderr or "")
|
||||
# Extract key info
|
||||
lines = [l for l in output.split("\n") if "written" in l.lower() or "done" in l.lower() or "pushed" in l.lower() or "error" in l.lower()]
|
||||
summary = "\n".join(lines) if lines else output[:500]
|
||||
return f"灵感分析已运行完成。\n{summary}\n\n查看: https://gitea.xybkwd.top/fxy/inspiration-collector/src/branch/main/ai-insights/daily/"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "分析超时(>120秒),可能输出较长,请稍后检查Gitea。"
|
||||
except Exception as e:
|
||||
return f"运行分析失败: {e}"
|
||||
|
||||
|
||||
def tool_server_status():
|
||||
"""Check server status."""
|
||||
try:
|
||||
result = []
|
||||
# docker
|
||||
dp = subprocess.run("docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'", shell=True, capture_output=True, text=True, timeout=10)
|
||||
result.append("### Docker\n" + (dp.stdout or dp.stderr))
|
||||
# disk/memory
|
||||
df = subprocess.run("df -h / | tail -1", shell=True, capture_output=True, text=True, timeout=5)
|
||||
result.append("### 磁盘\n" + (df.stdout or ""))
|
||||
fm = subprocess.run("free -h | head -2", shell=True, capture_output=True, text=True, timeout=5)
|
||||
result.append("### 内存\n" + (fm.stdout or ""))
|
||||
return "\n".join(result)
|
||||
except Exception as e:
|
||||
return f"状态查询失败: {e}"
|
||||
|
||||
|
||||
# ============ INTENT DETECTION ============
|
||||
|
||||
TOOL_PATTERNS = [
|
||||
(r"(memos|灵感|最近记录|随手记)", lambda m: tool_memos_list(7)),
|
||||
(r"(读一读|看一.*gitea|gitea.*文件|obsidian.*笔记|文库)", lambda m: tool_gitea_list("fxy/obsidian-vault")),
|
||||
(r"(gitea.*obsidian|obsidian.*目录)", lambda m: tool_gitea_list("fxy/obsidian-vault")),
|
||||
(r"(gitea.*灵感|灵感.*目录|分析.*目录)", lambda m: tool_gitea_list("fxy/inspiration-collector/ai-insights/daily")),
|
||||
(r"(博客|blog|xybkwd|博文)", lambda m: tool_blog_latest(5)),
|
||||
(r"(运行分析|跑日报|执行分析|run.*daily|run.*digest)", lambda m: tool_run_analysis()),
|
||||
(r"(服务器状态|检查状态|docker|容器|磁盘)", lambda m: tool_server_status()),
|
||||
]
|
||||
|
||||
def detect_intent(msg):
|
||||
"""Detect if message matches a tool pattern. Returns result or None."""
|
||||
msg_lower = msg.lower()
|
||||
for pattern, handler in TOOL_PATTERNS:
|
||||
if re.search(pattern, msg_lower):
|
||||
logger.info("Intent matched: %s", pattern)
|
||||
return handler(msg)
|
||||
return None
|
||||
|
||||
|
||||
# ============ HTML PAGE ============
|
||||
|
||||
INDEX_HTML = """<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>AI 智能体</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1a1a2e;color:#e0e0e0;height:100vh;display:flex}
|
||||
.sidebar{width:200px;background:#16213e;border-right:1px solid #0f3460;display:flex;flex-direction:column;flex-shrink:0}
|
||||
.shd{padding:12px;border-bottom:1px solid #0f3460;font-size:13px;font-weight:600;color:#e94560;display:flex;justify-content:space-between;align-items:center}
|
||||
.shd button{background:#0f3460;border:none;color:#e0e0e0;border-radius:6px;padding:3px 10px;font-size:12px;cursor:pointer}
|
||||
.clist{flex:1;overflow-y:auto;padding:4px}
|
||||
.citem{padding:8px 10px;border-radius:6px;cursor:pointer;font-size:13px;color:#aaa;margin:1px 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.citem:hover{background:#0f3460;color:#e0e0e0}
|
||||
.citem.act{background:#0f3460;color:#fff;border-left:3px solid #e94560}
|
||||
.main{flex:1;display:flex;flex-direction:column;min-width:0}
|
||||
.hdr{background:#16213e;padding:12px 16px;border-bottom:1px solid #0f3460;display:flex;justify-content:space-between;align-items:center}
|
||||
.hdr h1{font-size:16px;font-weight:600;color:#e94560}
|
||||
.hdr span{font-size:12px;color:#888}
|
||||
.cht{flex:1;overflow-y:auto;padding:12px}
|
||||
.msg{margin:8px 0;display:flex}
|
||||
.msg.u{justify-content:flex-end}
|
||||
.msg.b{justify-content:flex-start}
|
||||
.msg.t{justify-content:center}
|
||||
.bb{max-width:80%;padding:8px 12px;border-radius:10px;font-size:14px;line-height:1.5;white-space:pre-wrap}
|
||||
.msg.u .bb{background:#0f3460;color:#fff;border-bottom-right-radius:2px}
|
||||
.msg.b .bb{background:#16213e;border:1px solid #0f3460;border-bottom-left-radius:2px}
|
||||
.msg.t .bb{background:transparent;color:#e94560;font-size:12px;text-align:center;width:100%;max-width:100%}
|
||||
.ib{background:#16213e;padding:10px 12px;border-top:1px solid #0f3460;display:flex;gap:8px}
|
||||
.ib input{flex:1;background:#1a1a2e;border:1px solid #0f3460;border-radius:8px;padding:8px 12px;color:#e0e0e0;font-size:14px;outline:none}
|
||||
.ib input:focus{border-color:#e94560}
|
||||
.ib button{background:#e94560;color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:14px;cursor:pointer}
|
||||
.ib button:disabled{opacity:0.4}
|
||||
.tt{display:inline-block;background:#0f3460;color:#e94560;font-size:11px;padding:1px 6px;border-radius:3px;margin-right:4px}
|
||||
@media(max-width:640px){.sidebar{width:44px}.shd span{display:none}.shd{justify-content:center}.citem{font-size:0;padding:12px 0;text-align:center}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<div class="shd"><span>对话</span><button id="nb">+</button></div>
|
||||
<div class="clist" id="cl"></div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="hdr"><h1>FXY Agent</h1><span id="st">就绪</span></div>
|
||||
<div class="cht" id="cht"></div>
|
||||
<div class="ib">
|
||||
<input id="inp" placeholder="聊或下指令: 查memos / 读文库 / 跑分析 / 看状态" autofocus>
|
||||
<button id="snd">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var _cid=null;
|
||||
function _a(r,t){
|
||||
var d=document.createElement("div");d.className="msg "+r;
|
||||
var b=document.createElement("div");b.className="bb";
|
||||
// Use innerHTML for markdown rendering on bot messages
|
||||
if(r=="b"){
|
||||
b.innerHTML=_md(t);
|
||||
}else{
|
||||
b.textContent=t;
|
||||
}
|
||||
d.appendChild(b);document.getElementById("cht").appendChild(d);
|
||||
document.getElementById("cht").scrollTop=document.getElementById("cht").scrollHeight;
|
||||
}
|
||||
// Simple markdown renderer
|
||||
function _md(s){
|
||||
s=s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
// Code blocks first
|
||||
s=s.replace(/```([\s\S]*?)```/g,'<pre><code>$1</code></pre>');
|
||||
// Inline code
|
||||
s=s.replace(/`([^`]+)`/g,'<code>$1</code>');
|
||||
// Bold
|
||||
s=s.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
|
||||
// Italic
|
||||
s=s.replace(/\*(.+?)\*/g,'<em>$1</em>');
|
||||
// Lists
|
||||
s=s.replace(/^\s*[-*]\s+(.+)$/gm,'<li>$1</li>');
|
||||
s=s.replace(/(<li>.*<\/li>)/s,'<ul>$1</ul>');
|
||||
// Line breaks (double newline = paragraph)
|
||||
s=s.replace(/\n\n/g,'</p><p>');
|
||||
s=s.replace(/\n/g,'<br>');
|
||||
return '<p>'+s+'</p>';
|
||||
}
|
||||
function _a(r,t){var d=document.createElement("div");d.className="msg "+r;
|
||||
var b=document.createElement("div");b.className="bb";b.textContent=t;
|
||||
d.appendChild(b);document.getElementById("cht").appendChild(d);
|
||||
document.getElementById("cht").scrollTop=document.getElementById("cht").scrollHeight;}
|
||||
function _e(s){return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");}
|
||||
function _lc(){var x=new XMLHttpRequest();x.open("GET","/convs",true);
|
||||
x.onload=function(){if(x.status!=200)return;var d=JSON.parse(x.responseText),h="";
|
||||
for(var i=0;i<d.convs.length;i++){var c=d.convs[i];
|
||||
h+="<div class=\\"citem"+(c.id==_cid?" act":"")+"\\" data-id=\\""+c.id+"\\">"+_e(c.title)+"</div>";}
|
||||
document.getElementById("cl").innerHTML=h;};x.send();}
|
||||
document.getElementById("cl").onclick=function(e){var t=e.target;
|
||||
while(t&&!t.dataset.id)t=t.parentNode;if(t&&t.dataset.id)_sw(t.dataset.id);};
|
||||
function _sw(id){_cid=id;document.getElementById("cht").innerHTML="";
|
||||
var x=new XMLHttpRequest();x.open("GET","/convs/"+id+"/messages",true);
|
||||
x.onload=function(){if(x.status!=200){_a("b","加载失败");return;}
|
||||
var d=JSON.parse(x.responseText);
|
||||
for(var i=0;i<d.messages.length;i++){_a(d.messages[i].role=="assistant"?"b":"u",d.messages[i].content);}};x.send();_lc();}
|
||||
function _s(){var btn=document.getElementById("snd");if(btn.disabled)return;
|
||||
var inp=document.getElementById("inp"),t=inp.value.trim();if(!t)return;
|
||||
inp.value="";_a("u",t);btn.disabled=true;document.getElementById("st").textContent="...";
|
||||
var x=new XMLHttpRequest();x.open("POST","/chat",true);
|
||||
x.setRequestHeader("Content-Type","application/json");
|
||||
x.onload=function(){if(x.status==200){var d=JSON.parse(x.responseText);
|
||||
if(d.conv_id)_cid=d.conv_id;if(d.tool)_a("t","["+d.tool+"]");
|
||||
_a("b",d.reply||"错误");}else{_a("b","请求失败");}
|
||||
btn.disabled=false;document.getElementById("st").textContent="就绪";_lc();};
|
||||
x.onerror=function(){_a("b","网络错误");btn.disabled=false;document.getElementById("st").textContent="就绪";};
|
||||
x.send(JSON.stringify({message:t,conv_id:_cid}));}
|
||||
document.getElementById("nb").onclick=function(){_cid=null;document.getElementById("cht").innerHTML="";_a("b","新对话");_lc();};
|
||||
document.getElementById("snd").onclick=_s;
|
||||
document.getElementById("inp").onkeydown=function(e){if(e.key=="Enter")_s();};
|
||||
function ask(){_s();}
|
||||
_a("b","你好,我是你的AI智能体。我可以:\\n- 查最近的灵感记录\\n- 读Gitea文库\\n- 看博客最新文章\\n- 运行灵感分析\\n- 查看服务器状态");_lc();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
h = {"Cache-Control": "no-cache, no-store, must-revalidate"}
|
||||
return HTMLResponse(INDEX_HTML, headers=h)
|
||||
|
||||
|
||||
@app.get("/convs")
|
||||
async def list_convs():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
rows = conn.execute("SELECT id, title FROM conversations ORDER BY updated_at DESC LIMIT 50").fetchall()
|
||||
conn.close()
|
||||
return {"convs": [{"id": r[0], "title": r[1]} for r in rows]}
|
||||
|
||||
|
||||
@app.get("/convs/{conv_id}/messages")
|
||||
async def get_messages(conv_id: str):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
rows = conn.execute("SELECT role, content FROM messages WHERE conv_id=? ORDER BY id", (conv_id,)).fetchall()
|
||||
conn.close()
|
||||
return {"messages": [{"role": r[0], "content": r[1]} for r in rows]}
|
||||
|
||||
|
||||
@app.delete("/convs/{conv_id}")
|
||||
async def delete_conv(conv_id: str):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.execute("DELETE FROM messages WHERE conv_id=?", (conv_id,))
|
||||
conn.execute("DELETE FROM conversations WHERE id=?", (conv_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"deleted": conv_id}
|
||||
|
||||
|
||||
@app.post("/chat")
|
||||
async def chat(req: Request):
|
||||
body = await req.json()
|
||||
msg = body.get("message", "")
|
||||
conv_id = body.get("conv_id")
|
||||
logger.info("User[%s]: %s", conv_id or "new", msg[:80])
|
||||
|
||||
# Save message to DB
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
if not conv_id:
|
||||
conv_id = str(uuid.uuid4())[:8]
|
||||
now = datetime.now(TZ_BJ).strftime("%Y-%m-%d %H:%M:%S")
|
||||
title = (msg.strip()[:30] + "...") if len(msg.strip()) > 30 else msg.strip()
|
||||
conn.execute("INSERT INTO conversations (id,title,created_at,updated_at) VALUES (?,?,?,?)", (conv_id, title, now, now))
|
||||
conn.execute("INSERT INTO messages (conv_id,role,content) VALUES (?,'user',?)", (conv_id, msg))
|
||||
conn.execute("UPDATE conversations SET updated_at=datetime('now','localtime') WHERE id=?", (conv_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# STEP 1: Try intent detection for tool commands
|
||||
tool_result = detect_intent(msg)
|
||||
tool_name = None
|
||||
|
||||
if tool_result:
|
||||
# Determine which tool was used
|
||||
for pattern, handler in TOOL_PATTERNS:
|
||||
if re.search(pattern, msg.lower()) and handler(msg) == tool_result:
|
||||
tool_name = pattern
|
||||
break
|
||||
# Simplify tool name for display
|
||||
if "memos" in msg.lower() or "灵感" in msg:
|
||||
tool_name = "查Memos"
|
||||
elif "gitea" in msg.lower() or "文库" in msg or "笔记" in msg or "obsidian" in msg.lower():
|
||||
tool_name = "读Gitea"
|
||||
elif "博客" in msg or "blog" in msg.lower():
|
||||
tool_name = "看博客"
|
||||
elif "分析" in msg or "日报" in msg or "run" in msg.lower():
|
||||
tool_name = "运行分析"
|
||||
elif "状态" in msg or "docker" in msg.lower() or "磁盘" in msg:
|
||||
tool_name = "服务器状态"
|
||||
else:
|
||||
tool_name = "工具"
|
||||
|
||||
# Augment tool result with AI commentary
|
||||
try:
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个智能体。用户执行了一个工具操作,以下是工具返回的结果。请用自然语言总结回复用户,指出关键信息。回复要简洁有实质。"},
|
||||
{"role": "user", "content": f"工具({tool_name})返回结果:\n\n{tool_result}"}
|
||||
],
|
||||
"temperature": 0.3, "max_tokens": 1024
|
||||
}
|
||||
headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
r = await client.post(API_URL, json=payload, headers=headers)
|
||||
data = r.json()
|
||||
reply = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
if not reply:
|
||||
reply = tool_result[:2000]
|
||||
except Exception:
|
||||
reply = tool_result[:2000]
|
||||
|
||||
# Save reply
|
||||
conn2 = sqlite3.connect(DB_PATH)
|
||||
conn2.execute("INSERT INTO messages (conv_id,role,content) VALUES (?,'assistant',?)", (conv_id, reply))
|
||||
conn2.execute("UPDATE conversations SET updated_at=datetime('now','localtime') WHERE id=?", (conv_id,))
|
||||
conn2.commit()
|
||||
conn2.close()
|
||||
|
||||
logger.info("Tool[%s]: %s", tool_name, msg[:60])
|
||||
return {"reply": reply, "conv_id": conv_id, "tool": tool_name}
|
||||
|
||||
# STEP 2: No tool intent — normal chat
|
||||
conn3 = sqlite3.connect(DB_PATH)
|
||||
rows = conn3.execute("SELECT role,content FROM messages WHERE conv_id=? ORDER BY id DESC LIMIT 10", (conv_id,)).fetchall()
|
||||
rows.reverse()
|
||||
conn3.close()
|
||||
|
||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
for r in rows:
|
||||
messages.append({"role": r[0], "content": r[1]})
|
||||
|
||||
try:
|
||||
payload = {"model": MODEL, "messages": messages, "temperature": 0.6, "max_tokens": 4096}
|
||||
headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}
|
||||
async with httpx.AsyncClient(timeout=120) as client:
|
||||
r = await client.post(API_URL, json=payload, headers=headers)
|
||||
data = r.json()
|
||||
reply = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
|
||||
conn4 = sqlite3.connect(DB_PATH)
|
||||
conn4.execute("INSERT INTO messages (conv_id,role,content) VALUES (?,'assistant',?)", (conv_id, reply))
|
||||
conn4.execute("UPDATE conversations SET updated_at=datetime('now','localtime') WHERE id=?", (conv_id,))
|
||||
conn4.commit()
|
||||
conn4.close()
|
||||
|
||||
logger.info("Chat[%s]: %d chars", conv_id, len(reply))
|
||||
return {"reply": reply, "conv_id": conv_id}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error: %s", e)
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="127.0.0.1", port=8900)
|
||||
11
fetch_balance.py
Normal file
11
fetch_balance.py
Normal file
@ -0,0 +1,11 @@
|
||||
import json, requests, os
|
||||
BALANCE_FILE = '/var/www/nav/data/balance.json'
|
||||
try:
|
||||
r = requests.get('https://api.deepseek.com/user/balance',
|
||||
headers={'Authorization': 'Bearer sk-bbca4a0380d549389f0d27cdea0b5228'}, timeout=10)
|
||||
data = r.json()
|
||||
with open(BALANCE_FILE, 'w') as f:
|
||||
json.dump(data, f)
|
||||
print('OK:', data.get('balance_infos', [{}])[0].get('total_balance', '?'))
|
||||
except Exception as e:
|
||||
print('Error:', e)
|
||||
Reference in New Issue
Block a user