refactor: unify workflow/websocket logging and extract conversation state store

This commit is contained in:
2026-03-01 16:35:39 +08:00
parent 8dd5a11b4b
commit 4a07f9c726
4 changed files with 219 additions and 181 deletions

View File

@@ -71,6 +71,15 @@ from core.batch_quote_helpers import (
from core.prompt_builder import build_prompt as build_agent_prompt, split_customer_text
from core.image_workflow_router import handle_image_workflow as route_image_workflow
from core.message_orchestrator import process_incoming_message
from core.conversation_state_store import (
get_conversation_state as load_conversation_state,
mark_quote_ready as state_mark_quote_ready,
refresh_quote_phase as state_refresh_quote_phase,
should_defer_batch_quote as state_should_defer_batch_quote,
sync_pending_quote_state as state_sync_pending_quote_state,
restore_pending_quote_state as state_restore_pending_quote_state,
cleanup_inactive as state_cleanup_inactive,
)
load_dotenv()
@@ -285,6 +294,7 @@ class CustomerServiceAgent:
# 对话状态管理
self.conversations: Dict[str, ConversationState] = {}
self.ConversationStateClass = ConversationState
# 多轮对话历史PydanticAI ModelMessage 列表按客户ID存储
self.message_histories: Dict[str, list] = {}
self.evolution_candidate = self._load_evolution_candidate()
@@ -584,95 +594,26 @@ class CustomerServiceAgent:
CONVERSATION_TIMEOUT_HOURS = 12
def _get_conversation_state(self, customer_id: str) -> ConversationState:
"""获取或创建对话状态,超时自动重置"""
now = datetime.now()
if customer_id in self.conversations:
state = self.conversations[customer_id]
# 超过 12 小时没有消息,重置阶段和压价次数
if state.last_update:
try:
last = datetime.fromisoformat(state.last_update)
hours = (now - last).total_seconds() / 3600
if hours > self.CONVERSATION_TIMEOUT_HOURS:
state.stage = "售前"
state.discount_count = 0
# 同时清理对话历史,避免发送过期上下文
self.message_histories.pop(customer_id, None)
except Exception:
pass
# 进程内状态为空时,尝试从持久化恢复
if not state.pending_image_urls and not state.pending_requirements:
self._restore_pending_quote_state(customer_id, state)
else:
self.conversations[customer_id] = ConversationState(
customer_id=customer_id,
last_update=now.isoformat()
)
self._restore_pending_quote_state(customer_id, self.conversations[customer_id])
# 定期清理长期不活跃客户(超过 7 天)
self._cleanup_inactive(now)
return self.conversations[customer_id]
return load_conversation_state(self, customer_id)
def _cleanup_inactive(self, now: datetime):
"""清理超过 7 天没有消息的对话状态,释放内存"""
# 每 100 次调用清理一次,避免每次都遍历
if len(self.conversations) % 100 != 0:
return
expired = [
cid for cid, state in self.conversations.items()
if state.last_update and
(now - datetime.fromisoformat(state.last_update)).days > 7
]
for cid in expired:
self.conversations.pop(cid, None)
self.message_histories.pop(cid, None)
state_cleanup_inactive(self.conversations, self.message_histories, now)
def _sync_pending_quote_state(self, customer_id: str, state: ConversationState):
"""把待报价队列同步到客户库,避免重启丢失。"""
try:
self._refresh_quote_phase(state)
from db.customer_db import db
db.update_pending_quote_state(
customer_id,
state.pending_image_urls,
state.pending_requirements,
)
except Exception:
pass
state_sync_pending_quote_state(self, customer_id, state)
def _restore_pending_quote_state(self, customer_id: str, state: ConversationState):
"""从客户库恢复待报价队列。"""
try:
from db.customer_db import db
profile = db.get_customer(customer_id)
state.pending_image_urls = list(getattr(profile, "pending_quote_images", []) or [])
state.pending_requirements = list(getattr(profile, "pending_quote_requirements", []) or [])
state.image_count = len(state.pending_image_urls)
self._refresh_quote_phase(state)
except Exception:
pass
state_restore_pending_quote_state(customer_id, state)
@staticmethod
def _refresh_quote_phase(state: ConversationState, phase_hint: str = ""):
"""统一维护收图报价状态机。"""
QuoteStateMachine().refresh(state, phase_hint=phase_hint)
state_refresh_quote_phase(state, phase_hint=phase_hint)
def _should_defer_batch_quote(self, state: ConversationState, mark_ready: bool = False) -> bool:
"""
批量报价延后控制:
- 首次进入 ready_to_quote 时按配置等待 N 轮
- 等待轮次归零后,本轮即可报价
"""
self.quote_state_machine.delay_turns = max(0, int(self.batch_quote_delay_turns))
return self.quote_state_machine.should_defer_batch_quote(state, mark_ready=mark_ready)
return state_should_defer_batch_quote(self, state, mark_ready=mark_ready)
def _mark_quote_ready(self, state: ConversationState):
"""仅标记 ready 状态,不消费等待轮次。"""
self.quote_state_machine.delay_turns = max(0, int(self.batch_quote_delay_turns))
self.quote_state_machine.mark_ready(state)
state_mark_quote_ready(self, state)
def _build_reject_message(self, reason: str = "") -> str:
templates = [