Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
248 lines
10 KiB
Python
248 lines
10 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from collections import defaultdict
|
||
|
||
from flask import Flask, jsonify, request
|
||
|
||
from .auto_draw import auto_draw_preview
|
||
from .logger import setup_logger
|
||
from .orchestrator import Orchestrator
|
||
from .rules import extract_image_urls
|
||
from .runtime_switch import is_listen_only, set_listen_only
|
||
from .task_manager import TaskManager
|
||
|
||
|
||
def create_http_app(task_manager: TaskManager | None = None) -> Flask:
|
||
app = Flask(__name__)
|
||
logger = setup_logger()
|
||
tm = task_manager or TaskManager()
|
||
sim_orch = Orchestrator()
|
||
sim_pending_images: dict[str, list[str]] = defaultdict(list)
|
||
sim_recent_dialogue: dict[str, list[dict]] = defaultdict(list)
|
||
|
||
def _sim_append_dialogue(key: str, role: str, text: str) -> None:
|
||
t = str(text or "").strip()
|
||
if not t:
|
||
return
|
||
sim_recent_dialogue[key].append({"role": role, "text": t})
|
||
if len(sim_recent_dialogue[key]) > 24:
|
||
sim_recent_dialogue[key] = sim_recent_dialogue[key][-24:]
|
||
|
||
@app.get('/api/health')
|
||
def health():
|
||
return jsonify({'ok': True})
|
||
|
||
@app.get('/api/runtime/listen_only')
|
||
def get_listen_only():
|
||
return jsonify({'ok': True, 'listen_only': is_listen_only()})
|
||
|
||
@app.post('/api/runtime/listen_only')
|
||
def set_listen_only_mode():
|
||
body = request.get_json(silent=True) or {}
|
||
if "enabled" not in body:
|
||
return jsonify({'ok': False, 'error': 'enabled required'}), 400
|
||
enabled = bool(body.get("enabled"))
|
||
current = set_listen_only(enabled)
|
||
logger.info('[运行时] listen_only=%s', current)
|
||
return jsonify({'ok': True, 'listen_only': current})
|
||
|
||
@app.post('/api/task/receive')
|
||
def receive_task():
|
||
payload = request.get_json(silent=True) or {}
|
||
task_id = tm.create_task(payload)
|
||
logger.info('[任务] receive task_id=%s', task_id)
|
||
return jsonify({'ok': True, 'task_id': task_id})
|
||
|
||
@app.post('/api/task/cancel')
|
||
def cancel_task():
|
||
body = request.get_json(silent=True) or {}
|
||
task_id = str(body.get('task_id', '')).strip()
|
||
if not task_id:
|
||
return jsonify({'ok': False, 'error': 'task_id required'}), 400
|
||
ok = tm.cancel_task(task_id)
|
||
return jsonify({'ok': ok, 'task_id': task_id})
|
||
|
||
@app.get('/api/task/status/<task_id>')
|
||
def task_status(task_id: str):
|
||
task = tm.get_task(task_id)
|
||
if not task:
|
||
return jsonify({'ok': False, 'error': 'not found'}), 404
|
||
return jsonify({'ok': True, 'task': task})
|
||
|
||
@app.get('/api/task/list')
|
||
def task_list():
|
||
limit = int(request.args.get('limit', 100))
|
||
return jsonify({'ok': True, 'tasks': tm.list_tasks(limit=limit)})
|
||
|
||
@app.get('/debug/simulator')
|
||
def debug_simulator():
|
||
return """
|
||
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>链路测试页</title>
|
||
<style>
|
||
:root { --bg:#f6f8fb; --card:#fff; --ink:#1f2937; --line:#dbe3ef; --brand:#0f766e; }
|
||
body { margin:0; background:var(--bg); color:var(--ink); font-family:"Microsoft YaHei UI", "PingFang SC", sans-serif; }
|
||
.wrap { max-width:900px; margin:24px auto; padding:0 16px; }
|
||
.card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; }
|
||
h2 { margin:0 0 14px; font-size:20px; }
|
||
.row { display:grid; grid-template-columns:140px 1fr; gap:8px; margin:8px 0; align-items:center; }
|
||
input, textarea { width:100%; box-sizing:border-box; border:1px solid #c7d3e3; border-radius:8px; padding:8px 10px; font-size:14px; }
|
||
textarea { min-height:100px; resize:vertical; }
|
||
.btns { display:flex; gap:10px; margin-top:12px; }
|
||
button { border:0; border-radius:8px; padding:9px 14px; cursor:pointer; color:#fff; background:var(--brand); }
|
||
button.secondary { background:#475569; }
|
||
pre { margin-top:12px; background:#0f172a; color:#e2e8f0; padding:12px; border-radius:8px; overflow:auto; }
|
||
label.chk { display:flex; gap:8px; align-items:center; font-size:14px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div class="card">
|
||
<h2>AI链路模拟测试</h2>
|
||
<div class="row"><div>店铺acc_id</div><input id="acc_id" value="demo_shop:cs" /></div>
|
||
<div class="row"><div>客户customer_id</div><input id="customer_id" value="demo_user_001" /></div>
|
||
<div class="row"><div>商品标题</div><input id="goods_name" value="模糊图清晰处理专业代找原图素材淘宝图片找图修复服务" /></div>
|
||
<div class="row"><div>订单信息</div><input id="goods_order" value="" placeholder="可空" /></div>
|
||
<div class="row"><div>客户消息</div><textarea id="msg" placeholder="可直接粘贴图片URL,支持 #*# 拼接"></textarea></div>
|
||
<div class="row"><div>选项</div><label class="chk"><input type="checkbox" id="simulate_draw" />quote时顺带模拟作图</label></div>
|
||
<div class="btns">
|
||
<button onclick="sendMsg()">发送模拟消息</button>
|
||
<button class="secondary" onclick="resetCtx()">清空该客户上下文</button>
|
||
</div>
|
||
<pre id="out">等待发送...</pre>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
async function sendMsg() {
|
||
const body = {
|
||
acc_id: document.getElementById('acc_id').value.trim(),
|
||
customer_id: document.getElementById('customer_id').value.trim(),
|
||
goods_name: document.getElementById('goods_name').value.trim(),
|
||
goods_order: document.getElementById('goods_order').value.trim(),
|
||
msg: document.getElementById('msg').value.trim(),
|
||
simulate_draw: document.getElementById('simulate_draw').checked
|
||
};
|
||
const res = await fetch('/api/simulate/message', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(body),
|
||
});
|
||
const data = await res.json();
|
||
document.getElementById('out').textContent = JSON.stringify(data, null, 2);
|
||
}
|
||
async function resetCtx() {
|
||
const body = {
|
||
acc_id: document.getElementById('acc_id').value.trim(),
|
||
customer_id: document.getElementById('customer_id').value.trim(),
|
||
};
|
||
const res = await fetch('/api/simulate/reset', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(body),
|
||
});
|
||
const data = await res.json();
|
||
document.getElementById('out').textContent = JSON.stringify(data, null, 2);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
@app.post('/api/simulate/reset')
|
||
def sim_reset():
|
||
body = request.get_json(silent=True) or {}
|
||
acc_id = str(body.get("acc_id", "")).strip()
|
||
customer_id = str(body.get("customer_id", "")).strip()
|
||
if not acc_id or not customer_id:
|
||
return jsonify({'ok': False, 'error': 'acc_id and customer_id required'}), 400
|
||
key = f"{acc_id}:{customer_id}"
|
||
sim_pending_images.pop(key, None)
|
||
sim_recent_dialogue.pop(key, None)
|
||
try:
|
||
sim_orch.store.upsert_session(key, acc_id, customer_id, "pre_sales", {"after_sales_stage": "new"})
|
||
except Exception:
|
||
pass
|
||
return jsonify({'ok': True, 'customer_key': key, 'message': '上下文已清空'})
|
||
|
||
@app.post('/api/simulate/message')
|
||
def sim_message():
|
||
body = request.get_json(silent=True) or {}
|
||
acc_id = str(body.get("acc_id", "")).strip()
|
||
customer_id = str(body.get("customer_id", "")).strip()
|
||
goods_name = str(body.get("goods_name", "")).strip()
|
||
goods_order = str(body.get("goods_order", "")).strip()
|
||
msg = str(body.get("msg", "")).strip()
|
||
simulate_draw = bool(body.get("simulate_draw"))
|
||
if not acc_id or not customer_id or not msg:
|
||
return jsonify({'ok': False, 'error': 'acc_id, customer_id, msg required'}), 400
|
||
|
||
key = f"{acc_id}:{customer_id}"
|
||
urls = extract_image_urls(msg)
|
||
for u in urls:
|
||
if u not in sim_pending_images[key]:
|
||
sim_pending_images[key].append(u)
|
||
_sim_append_dialogue(key, "user", msg)
|
||
latest_image_url = urls[-1] if urls else (sim_pending_images[key][-1] if sim_pending_images[key] else "")
|
||
|
||
context = {
|
||
"customer_key": key,
|
||
"acc_id": acc_id,
|
||
"customer_id": customer_id,
|
||
"goods_name": goods_name,
|
||
"goods_order": goods_order,
|
||
"msg": msg,
|
||
"intent": "unknown",
|
||
"pending_images": len(sim_pending_images[key]),
|
||
"pending_image_urls": sim_pending_images[key][-5:],
|
||
"current_image_urls": urls[-3:],
|
||
"latest_image_url": latest_image_url,
|
||
"auto_quote_trigger": False,
|
||
"last_reply": "",
|
||
"recent_dialogue": sim_recent_dialogue[key][-12:],
|
||
}
|
||
|
||
route, decision, state = asyncio.run(sim_orch.decide(context))
|
||
if decision.reply:
|
||
_sim_append_dialogue(key, "assistant", decision.reply)
|
||
|
||
draw_result: dict = {}
|
||
if simulate_draw and decision.action == "quote" and latest_image_url:
|
||
draw_result = asyncio.run(
|
||
auto_draw_preview(
|
||
image_url=latest_image_url,
|
||
customer_id=customer_id,
|
||
requirement=msg,
|
||
)
|
||
)
|
||
|
||
logger.info("[模拟] key=%s route=%s action=%s", key, route, decision.action)
|
||
return jsonify(
|
||
{
|
||
"ok": True,
|
||
"customer_key": key,
|
||
"route": route,
|
||
"decision": {
|
||
"action": decision.action,
|
||
"reply": decision.reply,
|
||
"transfer_msg": decision.transfer_msg,
|
||
"quote_mode": decision.quote_mode,
|
||
"reason": decision.reason,
|
||
},
|
||
"state": state,
|
||
"pending_images": sim_pending_images[key],
|
||
"draw_result": draw_result,
|
||
}
|
||
)
|
||
|
||
return app
|
||
|
||
|
||
def run_http_server(host: str, port: int, task_manager: TaskManager | None = None) -> None:
|
||
app = create_http_app(task_manager=task_manager)
|
||
app.run(host=host, port=port, debug=False, use_reloader=False)
|