feat: add customer profile candidate flow

This commit is contained in:
张胜宇
2026-09-11 17:16:43 +08:00
parent ef098e6a4b
commit 3a21afa847
10 changed files with 295 additions and 6 deletions
+26
View File
@@ -40,6 +40,7 @@ from app.service.memory_recall_service import MemoryRecallService
from app.service.memory_service import CacheDeleteAdapter, MemoryService
from app.service.memory_taxonomy import BUSINESS_EVENT_TYPES
from app.service.model_gateway import DatabaseModelEndpointResolver, ModelGenerationService
from app.worker.customer_profile_candidate_worker import CustomerProfileCandidateWorker
from app.worker.episode_worker import (
EpisodeConsumptionResult,
EpisodeExtractionConsumer,
@@ -145,6 +146,20 @@ class WorkerRuntime:
signals=MemoryService.detect_memory_signals(message),
)
@staticmethod
def should_request_profile_candidate(
*, agent_type: str, context: RequestContext, message: str,
) -> bool:
"""客服只为已登录客户生成候选,不为访客生成正式画像数据。"""
if agent_type != "customer_service" or "visitor" in context.roles:
return False
if not {"customer", "authenticated_user"}.intersection(context.roles):
return False
if context.data_scope != "self":
return False
# 只有明确的长期偏好、约束或目标才进入候选队列,普通问答不触发模型抽取。
return bool(MemoryService.detect_memory_signals(message))
async def dispatch_one(self, *, run_id: str | None = None) -> bool:
# Outbox acknowledges a durable SQL queue entry, not an in-memory task.
async with SessionFactory() as session:
@@ -162,6 +177,13 @@ class WorkerRuntime:
session, extractor=self.memory_extraction, cache=self.memory_cache
).handle(payload)
async def dispatch_profile_candidate(payload: dict[str, Any]) -> None:
if "message_id" not in payload or "customer_id" not in payload:
raise ValueError("profile candidate payload is incomplete")
await CustomerProfileCandidateWorker(
session, extractor=self.memory_extraction, cache=self.memory_cache
).handle(payload)
async def dispatch_run_completed(payload: dict[str, Any]) -> None:
# 结果消息与审计已由 complete_run 同事务落库,此事件只承担
# "运行已完成"的对外通知职责。当前没有独立外部消费者,
@@ -224,6 +246,7 @@ class WorkerRuntime:
handlers: dict[str, Callable[[dict[str, Any]], Awaitable[None]]] = {
"agent.run_requested": dispatch,
"memory.extraction_requested": dispatch_memory_extraction,
"customer_profile.candidate_requested": dispatch_profile_candidate,
"agent.run_completed": dispatch_run_completed,
"config.cache_invalidate_requested": dispatch_cache_invalidate,
"memory.deletion_requested": dispatch_memory_deletion,
@@ -536,6 +559,9 @@ class WorkerRuntime:
result=result,
business_events=business_events,
),
profile_candidate_requested=self.should_request_profile_candidate(
agent_type=run.agent_type, context=context, message=request.message,
),
)
# 只有数据库成功保存完整用户/助手轮次后才写短期 Redis;Redis 故障不应让已完成
# 的客服回答回滚或重试。短期组件会再次脱敏,形成持久化链路的第二道保护。