feat: add simulator page and image quote analyzer
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
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
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
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
|
||||
|
||||
@@ -11,6 +17,17 @@ 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():
|
||||
@@ -58,6 +75,170 @@ def create_http_app(task_manager: TaskManager | None = None) -> Flask:
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user