refactor: split websocket flows and add brain action decision pipeline
This commit is contained in:
181
core/websocket_followup_flow.py
Normal file
181
core/websocket_followup_flow.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("cs_agent")
|
||||
|
||||
|
||||
async def unreplied_followup_loop(client):
|
||||
"""定时补偿:对“最后一条是客户消息且长时间未回复”的会话,补发一次自然跟进。"""
|
||||
if not client.enable_agent or not client.agent:
|
||||
return
|
||||
while client.running:
|
||||
try:
|
||||
await asyncio.sleep(max(30, int(os.getenv("UNREPLIED_FOLLOWUP_SCAN_SECONDS", "90"))))
|
||||
await scan_and_send_unreplied_followups(client)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
client._activity_log("unreplied_followup_loop_error", error=str(e))
|
||||
|
||||
|
||||
async def scan_and_send_unreplied_followups(client):
|
||||
from db import chat_log_db as cdb
|
||||
|
||||
try:
|
||||
idle_minutes = max(5, int(os.getenv("UNREPLIED_FOLLOWUP_IDLE_MINUTES", "12")))
|
||||
max_age_minutes = max(idle_minutes, int(os.getenv("UNREPLIED_FOLLOWUP_MAX_AGE_MINUTES", "180")))
|
||||
followup_cd = max(300, int(os.getenv("UNREPLIED_FOLLOWUP_COOLDOWN_SECONDS", "3600")))
|
||||
limit = max(10, int(os.getenv("UNREPLIED_FOLLOWUP_LIMIT", "40")))
|
||||
except Exception:
|
||||
idle_minutes, max_age_minutes, followup_cd, limit = 12, 180, 3600, 40
|
||||
|
||||
now = datetime.now()
|
||||
window_start = (now - timedelta(minutes=max_age_minutes)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn = None
|
||||
try:
|
||||
conn = cdb._get_conn()
|
||||
rows = conn.execute(
|
||||
cdb._sql(
|
||||
"""
|
||||
SELECT acc_id, customer_id, MAX(id) AS last_id
|
||||
FROM chat_logs
|
||||
WHERE timestamp >= ?
|
||||
GROUP BY acc_id, customer_id
|
||||
ORDER BY MAX(id) DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
),
|
||||
(window_start, limit * 6),
|
||||
).fetchall()
|
||||
sessions = [dict(r) for r in rows]
|
||||
sent = 0
|
||||
for s in sessions:
|
||||
if sent >= limit:
|
||||
break
|
||||
acc_id = str(s.get("acc_id", "") or "")
|
||||
cid = str(s.get("customer_id", "") or "")
|
||||
if not acc_id or not cid:
|
||||
continue
|
||||
ckey = f"{acc_id}:{cid}"
|
||||
if not client._is_owned_by_this_worker(ckey):
|
||||
continue
|
||||
last = conn.execute(
|
||||
cdb._sql(
|
||||
"""
|
||||
SELECT id, direction, message, timestamp, customer_name, acc_id, platform
|
||||
FROM chat_logs
|
||||
WHERE acc_id = ? AND customer_id = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
(acc_id, cid),
|
||||
).fetchone()
|
||||
if not last:
|
||||
continue
|
||||
last = dict(last)
|
||||
if str(last.get("direction", "")) != "in":
|
||||
continue
|
||||
last_ts = last.get("timestamp")
|
||||
if isinstance(last_ts, datetime):
|
||||
last_dt = last_ts
|
||||
else:
|
||||
last_dt = datetime.strptime(str(last_ts)[:19], "%Y-%m-%d %H:%M:%S")
|
||||
idle_s = (now - last_dt).total_seconds()
|
||||
if idle_s < idle_minutes * 60 or idle_s > max_age_minutes * 60:
|
||||
continue
|
||||
now_mono = time.monotonic()
|
||||
if (now_mono - client._unreplied_followup_sent.get(ckey, 0.0)) < followup_cd:
|
||||
continue
|
||||
|
||||
last_msg = str(last.get("message", "") or "").strip().lower()
|
||||
if last_msg in {"好的", "好", "ok", "收到", "嗯", "哦"}:
|
||||
continue
|
||||
|
||||
followup = await compose_ai_scene_reply(
|
||||
client,
|
||||
original_msg={
|
||||
"acc_id": acc_id,
|
||||
"from_id": cid,
|
||||
"from_name": client.to_chinese(last.get("customer_name", "") or cid),
|
||||
"acc_type": str(last.get("platform", "") or "AliWorkbench"),
|
||||
"msg": str(last.get("message", "") or ""),
|
||||
},
|
||||
scene="unreplied_followup",
|
||||
intent_hint="客户上一条消息还没接上,先自然承接并请对方补一句当前要处理的图或要求。",
|
||||
fallback="刚看到你消息了,我在的。你把要处理的图或要求再发我一下,我马上接着看。",
|
||||
)
|
||||
fake = {
|
||||
"acc_id": acc_id,
|
||||
"from_id": cid,
|
||||
"from_name": client.to_chinese(last.get("customer_name", "") or cid),
|
||||
"cy_id": cid,
|
||||
"cy_name": client.to_chinese(last.get("customer_name", "") or cid),
|
||||
"acc_type": str(last.get("platform", "") or "AliWorkbench"),
|
||||
"msg": str(last.get("message", "") or ""),
|
||||
"msg_type": 0,
|
||||
}
|
||||
await client.send_reply(fake, followup)
|
||||
client._unreplied_followup_sent[ckey] = now_mono
|
||||
sent += 1
|
||||
client._activity_log(
|
||||
"unreplied_followup_sent",
|
||||
acc_id=acc_id,
|
||||
customer_id=cid,
|
||||
idle_seconds=int(idle_s),
|
||||
last_msg=str(last.get("message", "") or "")[:120],
|
||||
reply=followup,
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
if conn:
|
||||
conn.close()
|
||||
except Exception:
|
||||
logger.debug("关闭数据库连接失败", exc_info=True)
|
||||
|
||||
|
||||
async def compose_ai_scene_reply(client, *, original_msg: dict, scene: str, intent_hint: str, fallback: str) -> str:
|
||||
"""场景化 AI 直接生成回复(不依赖固定模板)。"""
|
||||
if not client.enable_agent or not client.agent or not client.AgentDeps:
|
||||
return fallback
|
||||
try:
|
||||
deps = client.AgentDeps(
|
||||
msg_id=str(original_msg.get("msg_id", "") or f"{scene}_gen"),
|
||||
acc_id=str(original_msg.get("acc_id", "") or ""),
|
||||
from_id=str(original_msg.get("from_id", "") or ""),
|
||||
platform=str(original_msg.get("acc_type", "") or ""),
|
||||
)
|
||||
customer_msg = client.to_chinese(str(original_msg.get("msg", "") or ""))
|
||||
prompt = (
|
||||
"你是淘宝客服,直接生成一条发给客户的话。\n"
|
||||
f"场景: {scene}\n"
|
||||
f"意图: {intent_hint}\n"
|
||||
f"客户原话: {customer_msg}\n"
|
||||
"要求: 1-2句,自然口语,不要模板腔,不要新增价格/承诺;只输出最终回复。\n"
|
||||
)
|
||||
result = await client.agent.agent_natural_reply.run(prompt, deps=deps, message_history=[])
|
||||
out = str(getattr(result, "output", "") or "").strip()
|
||||
if not out:
|
||||
return fallback
|
||||
if out.startswith("话术|") or "[转移会话]" in out or "TRANSFER_REQUESTED" in out:
|
||||
return fallback
|
||||
client._activity_log(
|
||||
"ai_scene_reply_generated",
|
||||
acc_id=str(original_msg.get("acc_id", "") or ""),
|
||||
customer_id=str(original_msg.get("from_id", "") or ""),
|
||||
scene=scene,
|
||||
generated=out[:160],
|
||||
)
|
||||
return out
|
||||
except Exception as e:
|
||||
client._activity_log(
|
||||
"ai_scene_reply_error",
|
||||
acc_id=str(original_msg.get("acc_id", "") or ""),
|
||||
customer_id=str(original_msg.get("from_id", "") or ""),
|
||||
scene=scene,
|
||||
error=str(e),
|
||||
)
|
||||
return fallback
|
||||
Reference in New Issue
Block a user