agent-demo-closed-loop / web_server.py
3v324v23's picture
feat: 线索跟进工作台
8e0cf1c
import json
import os
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
from agent import run_agent_mode, tool_search_kb
HERE = Path(__file__).resolve().parent
WEB_DIR = HERE / "web"
OUT_DIR = HERE / "out"
KB_DIR = HERE / "kb"
def _read_body_json(handler: BaseHTTPRequestHandler):
length = int(handler.headers.get("Content-Length") or "0")
raw = handler.rfile.read(length) if length > 0 else b""
if not raw:
return {}
try:
return json.loads(raw.decode("utf-8"))
except Exception:
return None
class Handler(BaseHTTPRequestHandler):
server_version = "AgentDemo/1.0"
def log_message(self, format, *args):
return
def _send(self, status: int, content_type: str, body: bytes):
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def _send_json(self, status: int, obj):
body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self._send(status, "application/json; charset=utf-8", body)
def do_GET(self):
u = urlparse(self.path)
path = u.path or "/"
if path == "/":
p = WEB_DIR / "index.html"
self._send(200, "text/html; charset=utf-8", p.read_bytes())
return
if path == "/assets/app.js":
p = WEB_DIR / "app.js"
self._send(200, "application/javascript; charset=utf-8", p.read_bytes())
return
if path == "/assets/app.css":
p = WEB_DIR / "app.css"
self._send(200, "text/css; charset=utf-8", p.read_bytes())
return
if path == "/api/health":
self._send_json(200, {"ok": True, "service": "agent-demo", "version": "1.0"})
return
if path == "/api/kb/list":
items = []
for p in sorted(KB_DIR.glob("**/*.md")):
try:
txt = p.read_text(encoding="utf-8").splitlines()
title = ""
for ln in txt[:10]:
ln = ln.strip()
if ln.startswith("#"):
title = ln.lstrip("#").strip()
break
items.append({"path": str(p.relative_to(HERE)), "title": title or p.name})
except Exception:
continue
self._send_json(200, {"ok": True, "items": items})
return
self._send(404, "text/plain; charset=utf-8", "not found".encode("utf-8"))
def do_POST(self):
u = urlparse(self.path)
path = u.path or "/"
if path == "/api/kb/search":
payload = _read_body_json(self)
if payload is None:
self._send_json(400, {"ok": False, "error": "请求 JSON 解析失败"})
return
query = str(payload.get("query") or "").strip()
if not query:
self._send_json(400, {"ok": False, "error": "query 不能为空"})
return
res = tool_search_kb(query=query, kb_dir=str(KB_DIR), top_k=int(payload.get("top_k") or 6))
if not res.ok:
self._send_json(500, {"ok": False, "error": res.output})
return
try:
items = json.loads(res.output)
except Exception:
items = []
self._send_json(200, {"ok": True, "items": items})
return
if path != "/api/run":
self._send(404, "text/plain; charset=utf-8", "not found".encode("utf-8"))
return
payload = _read_body_json(self)
if payload is None:
self._send_json(400, {"ok": False, "error": "请求 JSON 解析失败"})
return
mode = str(payload.get("mode") or "general").strip()
if mode == "lead_followup":
lead = payload.get("lead") or {}
if not isinstance(lead, dict):
self._send_json(400, {"ok": False, "error": "lead 必须是对象"})
return
company = str(lead.get("company") or "").strip()
pain = str(lead.get("pain_points") or "").strip()
if not company and not pain:
self._send_json(400, {"ok": False, "error": "至少填写“公司/组织”或“主要诉求/痛点”"})
return
else:
goal = str(payload.get("goal") or "").strip()
if not goal:
self._send_json(400, {"ok": False, "error": "goal 不能为空"})
return
if len(goal) > 600:
self._send_json(400, {"ok": False, "error": "goal 过长(最多 600 字符)"})
return
OUT_DIR.mkdir(parents=True, exist_ok=True)
report_name = f"report_{uuid.uuid4().hex[:12]}.md"
out_report = str(OUT_DIR / report_name)
try:
result = run_agent_mode(mode=mode, payload=payload, kb_dir=str(KB_DIR), out_report=out_report, max_rounds=3)
except Exception as e:
self._send_json(500, {"ok": False, "error": f"执行失败: {e}"})
return
try:
report_text = Path(out_report).read_text(encoding="utf-8") if os.path.exists(out_report) else ""
except Exception:
report_text = ""
self._send_json(
200,
{
"ok": True,
"mode": result.get("mode") or mode,
"goal": result.get("goal"),
"final_answer": result.get("final_answer"),
"round_logs": result.get("round_logs"),
"structured": result.get("structured"),
"artifact_path": out_report,
"report_text": report_text,
},
)
def main():
host = "0.0.0.0"
if "PORT" in os.environ:
port = int(os.environ["PORT"])
httpd = ThreadingHTTPServer((host, port), Handler)
print(f"本地演示页已启动: http://127.0.0.1:{port}/")
httpd.serve_forever()
return
in_space = any(k.startswith("SPACE_") for k in os.environ.keys())
base_port = 7860 if in_space else 8000
last_err = None
for port in range(base_port, base_port + 20):
try:
httpd = ThreadingHTTPServer((host, port), Handler)
print(f"本地演示页已启动: http://127.0.0.1:{port}/")
httpd.serve_forever()
return
except OSError as e:
last_err = e
continue
raise SystemExit(f"无法绑定端口(从 {base_port} 起尝试 20 个):{last_err}")
if __name__ == "__main__":
main()