Files
2026-07-13 13:04:19 +08:00

126 lines
6.2 KiB
Python
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.
import threading, sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fastapi import FastAPI, Header, HTTPException, Query, Depends; from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from agentmain import GenericAgent as GA
PORT, API_KEY = int(sys.argv[1]), sys.argv[2]
app, agent, lock = FastAPI(), GA(), threading.Lock()
outputs, stopped = [], True
threading.Thread(target=agent.run, daemon=True).start()
class Req(BaseModel): prompt: str = ""
agent.verbose = False
def require_key(key: str = Query(None), x_api_key: str = Header(None, alias="X-API-Key")):
if API_KEY not in (key, x_api_key): raise HTTPException(404)
def run_task(prompt):
global stopped
segs = [] # 本任务按 turn 索引的分段输出
with lock: task_start = len(outputs)
def flush():
with lock: outputs[task_start:] = segs
try:
dq = agent.put_task(prompt, source="http")
while "done" not in (item := dq.get(timeout=2200)):
outs = item.get("outputs")
if not outs: continue
idx = max(0, int(item.get("turn", 0) or 0) - 1) # turn 1-based → 槽位 0-based
while len(segs) <= idx: segs.append("")
segs[idx] = str(outs[-1]) # 当前 turn
if len(outs) >= 2 and idx >= 1: segs[idx - 1] = str(outs[-2]) # 前一 turn 落定值
flush()
segs = [str(s) for s in item.get("outputs", [])] # done 时全量替换
flush()
finally: stopped = True
@app.post("/put_task")
def put_task(req: Req, _=Depends(require_key)):
global stopped
with lock:
if not stopped: return {"ok": False, "error": "should abort first"}
stopped = False
threading.Thread(target=run_task, args=(req.prompt,), daemon=True).start()
return {"ok": True}
@app.post("/abort")
def abort(_=Depends(require_key)): agent.abort(); return {"ok": True}
@app.post("/input")
def input_task(req: Req, _=Depends(require_key)):
global stopped
if not stopped: agent.intervene = req.prompt; return {"ok": True, "mode": "intervene"}
with lock:
if not stopped: agent.intervene = req.prompt; return {"ok": True, "mode": "intervene"}
stopped = False
threading.Thread(target=run_task, args=(req.prompt,), daemon=True).start()
return {"ok": True, "mode": "task"}
@app.get("/output")
def get_output(k: int = Query(5), _=Depends(require_key)):
with lock: r = outputs[-k:]
return {"stopped": stopped, "output": "\n".join(r),
"history": "\n".join(str(h) for h in agent.history)}
@app.get("/llm")
def llm_ep(llm_no: int = Query(None), _=Depends(require_key)):
if llm_no is not None:
agent.next_llm(llm_no)
return {"llm_no": agent.llm_no, "name": agent.get_llm_name(),
"llms": [{"no": i, "name": n, "current": a} for i, n, a in agent.list_llms()]}
@app.get("/sysprompt")
def sysprompt_ep(text: str = Query(None), _=Depends(require_key)):
if text is not None:
agent.extra_sys_prompts = [text] if text else []
return {"extra_sys_prompts": agent.extra_sys_prompts}
HELP = """GA HTTP 操作协议(所有请求带 ?key=API_KEY,或 Header X-API-Key
GET /output?k=N 查看状态:{stopped, output(末N条), history}。stopped=true 表示空闲
POST /input {prompt} 下发指令:空闲时作为新任务,忙时作为中途干预(intervene)
POST /abort 中止当前任务
GET /llm[?llm_no=N] 查/切模型:返回 {llm_no,name,llms:[{no,name,current}]}
GET /sysprompt[?text]查/设附加系统提示(extra_sys_prompts)text 为空则清空
纠偏流程:先 GET /output 读 history 判断状态→需要时 POST /input 注入纠偏指令"""
@app.get("/help")
def help_ep(_=Depends(require_key)): return {"help": HELP}
@app.get("/")
def ui():
return HTMLResponse(f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>GA Monitor</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
*{{margin:0;padding:0;box-sizing:border-box}}
body{{font:14px/1.6 system-ui,sans-serif;background:#f8f9fa;color:#212529;padding:24px;max-width:900px;margin:0 auto}}
#status{{font-size:18px;padding:8px 16px;border-radius:8px;margin-bottom:16px;display:inline-block;font-weight:600}}
.stopped{{background:#d4edda;color:#155724}}.running{{background:#fff3cd;color:#856404}}
.section{{background:#fff;border-radius:10px;padding:18px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,.06)}}
.section h3{{color:#533483;margin-bottom:8px;font-size:15px}}
textarea{{width:100%;height:80px;background:#fff;color:#212529;border:1px solid #ced4da;border-radius:6px;padding:10px;font:inherit;resize:vertical}}
button{{padding:10px 24px;background:#533483;color:#fff;border:none;border-radius:6px;font:inherit;cursor:pointer;font-weight:500}}
button:hover{{background:#7c3aed}}
pre{{background:#f1f3f5;padding:12px;border-radius:6px;overflow-x:auto;font:13px monospace;max-height:400px;overflow-y:auto}}
</style></head><body>
<div id="status" class="stopped">● Loading...</div>
<div class="section"><h3>Output</h3><div id="output"></div></div>
<div class="section"><h3>History</h3><div id="history"></div></div>
<textarea id="prompt" placeholder="Enter instruction..."></textarea>
<button onclick="send()">Send</button>
<script>
const K=new URLSearchParams(location.search).get('key')||'';
async function poll(){{let r=await fetch('/output',{{headers:{{'X-API-Key':K}}}});let d=await r.json();
let s=document.getElementById('status');s.textContent=(d.stopped?'● Stopped':'● Running');s.className=d.stopped?'stopped':'running';
document.getElementById('output').innerHTML=marked.parse(d.output||'_empty_');
document.getElementById('history').innerHTML=marked.parse(d.history||'_empty_');}}
async function send(){{let p=document.getElementById('prompt').value;if(!p)return;
await fetch('/input',{{method:'POST',headers:{{'X-API-Key':K,'Content-Type':'application/json'}},body:JSON.stringify({{prompt:p}})}});
document.getElementById('prompt').value='';poll();}}
poll();setInterval(poll,3000);
</script></body></html>""")
if __name__ == "__main__":
import uvicorn; uvicorn.run(app, host="0.0.0.0", port=PORT)