merge: integrate ZSY customer service and profile capabilities
This commit is contained in:
@@ -9,8 +9,14 @@ from app.core.contracts import AgentResult, DomainEvent
|
||||
from app.core.errors import RunLeaseLostError
|
||||
from app.model.audit import InteractionAudit
|
||||
from app.model.conversation import ConversationMessage
|
||||
from app.model.platform import AgentRun, DomainEventOutbox, RequestIdempotency
|
||||
from app.model.platform import AgentRun, DomainEventOutbox, HandoverTicket, RequestIdempotency
|
||||
from app.model.risk import RiskUser
|
||||
from app.model.session import ConversationSession
|
||||
from app.service.customer_service_handover_context import (
|
||||
MAX_SUMMARY_MESSAGES,
|
||||
CustomerServiceHandoverContext,
|
||||
build_customer_service_handover_context,
|
||||
)
|
||||
|
||||
#: 治理层追加免责声明时使用的分隔形状(`app/service/agent/governance.py` 里定义)。
|
||||
#: 这里只用于**审计留痕**,不参与任何判定:判据是"末尾是否出现这个形状"。
|
||||
@@ -39,7 +45,7 @@ class AgentPersistenceService:
|
||||
|
||||
async def complete_run(
|
||||
self, run_id: str, result: AgentResult, memory_extraction_requested: bool = True,
|
||||
*, worker_id: str | None = None,
|
||||
*, worker_id: str | None = None, profile_candidate_requested: bool = False,
|
||||
) -> int:
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
async with self.session.begin():
|
||||
@@ -59,6 +65,20 @@ class AgentPersistenceService:
|
||||
raise RunLeaseLostError("运行租约失效或已取消")
|
||||
if run.status not in {"queued", "running"}:
|
||||
raise RunLeaseLostError("不能覆盖运行终态")
|
||||
clarification_round = 0
|
||||
if run.agent_type == "customer_service":
|
||||
session_row = await self.session.scalar(select(ConversationSession).where(
|
||||
ConversationSession.session_id == run.session_id,
|
||||
ConversationSession.user_id == run.user_id,
|
||||
).with_for_update())
|
||||
if session_row is not None:
|
||||
clarification_round = session_row.clarification_round
|
||||
if result.result.clarification_required:
|
||||
session_row.clarification_round = min(
|
||||
session_row.clarification_round + 1, 2
|
||||
)
|
||||
else:
|
||||
session_row.clarification_round = 0
|
||||
message = ConversationMessage(
|
||||
session_id=run.session_id, customer_id=run.user_id, portal="agent",
|
||||
role="assistant", content=result.result.text,
|
||||
@@ -73,6 +93,47 @@ class AgentPersistenceService:
|
||||
)
|
||||
self.session.add(message)
|
||||
await self.session.flush()
|
||||
handover_ticket: HandoverTicket | None = None
|
||||
handover_context: CustomerServiceHandoverContext | None = None
|
||||
if result.result.transfer_required:
|
||||
# 访客 subject 不是正式用户主键,先按 RiskUser 查询,查不到则保留空归属。
|
||||
ticket_customer_id = await self.session.scalar(
|
||||
select(RiskUser.id).where(RiskUser.id == run.user_id)
|
||||
)
|
||||
recent_messages = list(await self.session.scalars(
|
||||
select(ConversationMessage)
|
||||
.where(ConversationMessage.session_id == run.session_id)
|
||||
.order_by(ConversationMessage.id.desc())
|
||||
.limit(MAX_SUMMARY_MESSAGES)
|
||||
))
|
||||
recent_messages.reverse()
|
||||
confidence = (
|
||||
Decimal(str(result.result.intent.confidence))
|
||||
if result.result.intent else None
|
||||
)
|
||||
reason_code = result.result.transfer_reason or "agent_requested"
|
||||
handover_context = build_customer_service_handover_context(
|
||||
reason_code=reason_code,
|
||||
clarification_round=clarification_round,
|
||||
confidence=confidence,
|
||||
source_references=result.result.source_references,
|
||||
messages=recent_messages,
|
||||
)
|
||||
handover_ticket = HandoverTicket(
|
||||
ticket_no=f"ticket-{uuid4().hex[:24]}",
|
||||
session_id=run.session_id,
|
||||
customer_id=ticket_customer_id,
|
||||
source_agent=run.agent_type,
|
||||
source_message_id=message.id,
|
||||
intent=(result.result.intent.intent if result.result.intent else None),
|
||||
confidence=confidence,
|
||||
reason_code=reason_code,
|
||||
reason_detail=handover_context.reason_detail,
|
||||
conversation_summary=handover_context.conversation_summary,
|
||||
source_references=handover_context.source_references,
|
||||
status="pending", created_at=now, updated_at=now,
|
||||
)
|
||||
self.session.add(handover_ticket)
|
||||
run.result_message_id = message.id
|
||||
run.status = "succeeded"
|
||||
run.result_version = 1
|
||||
@@ -103,6 +164,21 @@ class AgentPersistenceService:
|
||||
},
|
||||
created_at=now,
|
||||
))
|
||||
if handover_ticket is not None:
|
||||
assert handover_context is not None
|
||||
self.session.add(InteractionAudit(
|
||||
actor_type="agent", actor_id=run.user_id,
|
||||
target_customer_id=handover_ticket.customer_id,
|
||||
session_id=run.session_id, portal="agent",
|
||||
action_type="agent.handover_requested",
|
||||
detail={
|
||||
"run_id": run_id, "ticket_no": handover_ticket.ticket_no,
|
||||
"reason_code": handover_ticket.reason_code,
|
||||
"clarification_round": clarification_round,
|
||||
"source_reference_count": len(handover_context.source_references),
|
||||
},
|
||||
created_at=now,
|
||||
))
|
||||
await self.session.execute(
|
||||
update(RequestIdempotency)
|
||||
.where(RequestIdempotency.id == run.idempotency_id)
|
||||
@@ -120,6 +196,31 @@ class AgentPersistenceService:
|
||||
payload={"run_id": run_id, "message_id": message.id,
|
||||
"customer_id": run.user_id}, occurred_at=now,
|
||||
))
|
||||
if profile_candidate_requested:
|
||||
# 候选画像只允许由已登录客服会话触发;Worker 会再次校验身份标记。
|
||||
events.append(DomainEvent(
|
||||
event_id=str(uuid4()),
|
||||
event_type="customer_profile.candidate_requested",
|
||||
aggregate_type="agent_run", aggregate_id=run_id, trace_id=run.trace_id,
|
||||
payload={
|
||||
"run_id": run_id, "message_id": message.id,
|
||||
"customer_id": run.user_id,
|
||||
"actor_type": "authenticated_customer",
|
||||
},
|
||||
occurred_at=now,
|
||||
))
|
||||
if handover_ticket is not None:
|
||||
assert handover_context is not None
|
||||
events.append(DomainEvent(
|
||||
event_id=str(uuid4()), event_type="conversation.transfer_requested",
|
||||
aggregate_type="conversation", aggregate_id=run.session_id,
|
||||
trace_id=run.trace_id,
|
||||
payload={
|
||||
"ticket_no": handover_ticket.ticket_no,
|
||||
"handover_context": handover_context.event_metadata,
|
||||
},
|
||||
occurred_at=now,
|
||||
))
|
||||
for event in events:
|
||||
self.session.add(DomainEventOutbox(
|
||||
event_id=event.event_id, event_type=event.event_type,
|
||||
|
||||
Reference in New Issue
Block a user