451 lines
20 KiB
Python
451 lines
20 KiB
Python
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 _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)
|