refactor: unify workflow/websocket logging and extract conversation state store
This commit is contained in:
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user