Files
wecom_it_smart_desk/backend/app/models/conversation.py
T

293 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微IT智能服务台 — 会话模型
# =============================================================================
# 说明:对应数据库 conversations 表,存储所有会话信息
# 核心概念:每个员工的每次咨询对应一个会话(Conversation
# 会话状态流转:ai_handling → queued → serving → resolved
# =============================================================================
import uuid
from datetime import datetime
from typing import Any, Dict, Optional
from sqlalchemy import Boolean, DateTime, Index, Integer, JSON, String
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class Conversation(Base):
"""会话模型 — 对应 conversations 表。
每个员工的一次完整咨询过程对应一个会话记录。
包含员工信息、会话状态、紧急度评分、标签等核心数据。
Attributes:
id: 会话唯一标识(UUID,数据库自动生成)
employee_id: 企微员工UserID(关联企微通讯录)
employee_name: 员工姓名(冗余存储,减少关联查询)
department: 员工部门
position: 员工岗位
level: 员工等级(用于 VIP 判断)
status: 会话状态(ai_handling/queued/serving/resolved
is_vip: VIP标记(基于企微通讯录规则自动匹配)
is_pinned: 置顶标记(坐席手动操作)
is_todo: 代办标记(坐席手动操作)
urgency_score: 紧急度评分(1-5,数值越大越紧急)
tags: 标签集合(JSONB,存储举手/需介入/情绪等标记)
assigned_agent_id: 分配的坐席ID
last_message_at: 最后消息时间(用于会话排序)
last_message_summary: 最后消息摘要(会话列表预览用)
created_at: 创建时间
updated_at: 更新时间
"""
# 表名(必须和架构文档 DDL 一致)
__tablename__ = "conversations"
# --------------------------------------------------------------------------
# 字段定义
# --------------------------------------------------------------------------
# 主键:UUIDPython端生成(兼容PostgreSQL和SQLite
id: Mapped[str] = mapped_column(
String(36),
primary_key=True,
default=lambda: str(uuid.uuid4()),
)
# 企业微信企业ID(US-7: 区分主企业和下游企业员工)
# 默认值为主企业 corp_id,下游企业员工使用下游企业 corp_id
corp_id: Mapped[str] = mapped_column(
String(64),
nullable=False,
default="",
comment="企业微信企业ID(主企业或下游企业)",
)
# 企微员工UserIDNOT NULL,配合 corp_id 唯一标识员工)
employee_id: Mapped[str] = mapped_column(
String(64),
nullable=False,
comment="企微员工UserID",
)
# 员工姓名(冗余存储,避免每次查询都要关联企微API)
employee_name: Mapped[str] = mapped_column(
String(128),
nullable=False,
default="",
comment="员工姓名",
)
# 部门
department: Mapped[str] = mapped_column(
String(256),
nullable=False,
default="",
comment="部门",
)
# 岗位
position: Mapped[str] = mapped_column(
String(128),
nullable=False,
default="",
comment="岗位",
)
# 等级(用于 VIP 判断:总监及以上为 VIP)
level: Mapped[str] = mapped_column(
String(64),
nullable=False,
default="",
comment="等级",
)
# 会话状态(CHECK 约束:只能取四种值)
# ai_handling: AI处理中(第二步启用)
# queued: 排队中,等待坐席接入
# serving: 服务中,坐席正在处理
# resolved: 已结单
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="queued",
comment="会话状态: ai_handling/queued/serving/resolved",
)
# VIP标记(基于企微通讯录API规则自动匹配)
is_vip: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="VIP标记",
)
# 置顶标记(坐席手动操作,置顶的会话在列表中优先显示)
is_pinned: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="置顶标记",
)
# 代办标记(坐席手动操作,标记需要后续跟进的会话)
is_todo: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="代办标记",
)
# 紧急度评分(1-5,数值越大越紧急)
# 计算公式:基础分 + 情绪加成 + VIP加成 + 重复追问加成
urgency_score: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=1,
comment="紧急度1-5",
)
# 标签集合(JSON 格式,存储结构化标记数据,兼容所有数据库)
# 示例:{"hand_raise": true, "emotion": "angry", "need_intervene": true}
tags: Mapped[Dict[str, Any]] = mapped_column(
JSON,
nullable=False,
default=dict,
comment="标签集合",
)
# 分配的坐席ID(可为空,表示尚未分配坐席)
assigned_agent_id: Mapped[Optional[str]] = mapped_column(
String(64),
nullable=True,
comment="分配的坐席ID",
)
# 协作坐席ID列表(JSON 数组,存储所有被邀请来协作的坐席ID)
# 和 assigned_agent_id 的区别:
# - assigned_agent_id:会话的「主责」坐席(接单人),只有他才能结单/转接
# - collaborating_agent_ids:被邀请来协助的坐席,可以查看和回复,但不能结单
# 设计决策:用 JSON 而非关联表,因为协作人数少(1-3人),JSON 查询足够
collaborating_agent_ids: Mapped[list] = mapped_column(
JSON,
nullable=False,
default=list,
comment="协作坐席ID列表",
)
# 被邀请参与会话的非坐席人员列表(JSON 数组,存储员工ID或部门ID)
# 和 collaborating_agent_ids 的区别:
# - collaborating_agent_ids:坐席 → 坐席协作(摇人)
# - participants:坐席 → 任意员工/部门(邀请功能 P0-09~P0-11
# 格式:[ {"id": "employee_id", "name": "姓名", "department": "部门", "type": "employee"},
# {"id": "dept_id", "name": "部门名", "type": "department"} ]
# 设计决策:存储完整信息,减少企微API调用
participants: Mapped[list] = mapped_column(
JSON,
nullable=False,
default=list,
comment="被邀请参与会话的人员列表(邀请功能)",
)
# AI 实质性回复计数(排除打招呼/呼叫人工的引导回复)
# 当计数 >= 3 时,前端显示「呼叫坐席」按钮
ai_substantive_reply_count: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
comment="AI实质性回复计数(满3次可呼叫坐席)",
)
# 影响范围(受影响人数,0=未评估,数值越大影响范围越广)
impact_scope: Mapped[int] = mapped_column(
Integer,
nullable=False,
default=0,
comment="影响范围",
)
# 阻断性标记(问题是否阻断员工正常工作流程)
is_blocking: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="阻断性标记",
)
# 情绪状态(normal: 正常, worried: 担忧, angry: 愤怒, urgent: 紧急)
emotion_state: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="normal",
comment="情绪状态",
)
# Dify 会话ID(用于多轮对话上下文保持)
# Dify 侧通过此 ID 关联同一员工的多轮对话
dify_conversation_id: Mapped[Optional[str]] = mapped_column(
String(128),
nullable=True,
default=None,
comment="Dify会话ID(多轮对话上下文)",
)
# 最后消息时间(用于会话列表按最新消息排序)
last_message_at: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
comment="最后消息时间",
)
# 最后消息摘要(会话列表预览用,截取消息前256字符)
last_message_summary: Mapped[str] = mapped_column(
String(256),
nullable=False,
default="",
comment="最后消息摘要",
)
# 创建时间
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=datetime.now,
comment="创建时间",
)
# 更新时间
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=datetime.now,
onupdate=datetime.now,
comment="更新时间",
)
# --------------------------------------------------------------------------
# 索引定义(和架构文档 DDL 严格一致)
# --------------------------------------------------------------------------
__table_args__ = (
# 按状态查询(如查询所有排队中的会话)
Index("idx_conversations_status", "status"),
# 按员工ID查询(如查询某个员工的所有会话)
Index("idx_conversations_employee_id", "employee_id"),
# US-7: 按企业ID查询(如查询某企业所有会话)
Index("idx_conversations_corp_id", "corp_id"),
# 按坐席ID查询(如查询某个坐席正在服务的所有会话)
Index("idx_conversations_assigned_agent", "assigned_agent_id"),
# 按紧急度倒序查询(紧急度高的排前面)
Index("idx_conversations_urgency_score", "urgency_score"),
# 按最后消息时间倒序查询(最新消息的排前面)
Index("idx_conversations_last_message_at", "last_message_at"),
)
def __repr__(self) -> str:
"""会话对象的字符串表示,方便调试。"""
return (
f"<Conversation(id={self.id}, employee={self.employee_name}, "
f"status={self.status}, urgency={self.urgency_score})>"
)