320 lines
12 KiB
Python
320 lines
12 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微IT智能服务台 — 会话 Pydantic Schema
|
|||
|
|
# =============================================================================
|
|||
|
|
# 说明:定义会话相关的请求/响应数据结构
|
|||
|
|
# 包含:创建、更新、响应三种 Schema
|
|||
|
|
# tags 字段使用 JSONB 结构,定义了详细的子结构
|
|||
|
|
# =============================================================================
|
|||
|
|
|
|||
|
|
from datetime import datetime
|
|||
|
|
from typing import Any, Dict, List, Optional
|
|||
|
|
|
|||
|
|
from pydantic import BaseModel, Field, field_validator
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# tags JSONB 字段的子结构定义
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationTags(BaseModel):
|
|||
|
|
"""会话标签集合 — 对应 conversations.tags JSONB 字段。
|
|||
|
|
|
|||
|
|
记录会话的各种标记状态,用于坐席端展示和排序。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
hand_raise: 举手标记(员工说"转人工"或点击摇人按钮)
|
|||
|
|
need_intervene: 需介入标记(追问超过N轮)
|
|||
|
|
emotion: 情绪标记(neutral/worried/angry/urgent)
|
|||
|
|
emotion_keywords: 触发情绪标记的关键词列表
|
|||
|
|
repeat_count: 追问轮次计数
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 举手标记(员工明确要求转人工)
|
|||
|
|
hand_raise: bool = Field(default=False, description="举手标记")
|
|||
|
|
# 需介入标记(同一问题追问超过阈值)
|
|||
|
|
need_intervene: bool = Field(default=False, description="需介入标记")
|
|||
|
|
# 情绪标记(neutral: 正常, worried: 担忧, angry: 愤怒, urgent: 紧急)
|
|||
|
|
emotion: str = Field(
|
|||
|
|
default="neutral",
|
|||
|
|
description="情绪标记: neutral/worried/angry/urgent",
|
|||
|
|
)
|
|||
|
|
# 触发情绪标记的关键词列表
|
|||
|
|
emotion_keywords: List[str] = Field(
|
|||
|
|
default_factory=list,
|
|||
|
|
description="触发情绪标记的关键词",
|
|||
|
|
)
|
|||
|
|
# 追问轮次计数(同一会话中员工连续追问的次数)
|
|||
|
|
repeat_count: int = Field(default=0, description="追问轮次计数")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 会话状态枚举值校验
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
VALID_STATUSES = {"ai_handling", "queued", "serving", "resolved"}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 创建会话 Schema(从企微消息创建会话时使用)
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationCreate(BaseModel):
|
|||
|
|
"""创建会话请求 Schema。
|
|||
|
|
|
|||
|
|
从企微消息回调创建新会话时使用,
|
|||
|
|
只需要员工ID和姓名,其他信息后续补充。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
employee_id: 企微员工UserID
|
|||
|
|
employee_name: 员工姓名
|
|||
|
|
department: 部门
|
|||
|
|
position: 岗位
|
|||
|
|
level: 等级
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
employee_id: str = Field(..., min_length=1, max_length=64, description="企微员工UserID")
|
|||
|
|
employee_name: str = Field(default="", max_length=128, description="员工姓名")
|
|||
|
|
department: str = Field(default="", max_length=256, description="部门")
|
|||
|
|
position: str = Field(default="", max_length=128, description="岗位")
|
|||
|
|
level: str = Field(default="", max_length=64, description="等级")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 更新会话 Schema(坐席修改会话信息时使用)
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationUpdate(BaseModel):
|
|||
|
|
"""更新会话请求 Schema。
|
|||
|
|
|
|||
|
|
坐席更新会话信息时使用,所有字段可选(只更新传入的字段)。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
employee_name: 员工姓名
|
|||
|
|
department: 部门
|
|||
|
|
position: 岗位
|
|||
|
|
level: 等级
|
|||
|
|
is_vip: VIP标记
|
|||
|
|
tags: 标签集合
|
|||
|
|
urgency_score: 紧急度评分
|
|||
|
|
assigned_agent_id: 分配的坐席ID
|
|||
|
|
last_message_summary: 最后消息摘要
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
employee_name: Optional[str] = Field(None, max_length=128, description="员工姓名")
|
|||
|
|
department: Optional[str] = Field(None, max_length=256, description="部门")
|
|||
|
|
position: Optional[str] = Field(None, max_length=128, description="岗位")
|
|||
|
|
level: Optional[str] = Field(None, max_length=64, description="等级")
|
|||
|
|
is_vip: Optional[bool] = Field(None, description="VIP标记")
|
|||
|
|
tags: Optional[ConversationTags] = Field(None, description="标签集合")
|
|||
|
|
urgency_score: Optional[int] = Field(None, ge=1, le=5, description="紧急度1-5")
|
|||
|
|
assigned_agent_id: Optional[str] = Field(None, max_length=64, description="分配的坐席ID")
|
|||
|
|
last_message_summary: Optional[str] = Field(None, max_length=256, description="最后消息摘要")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 更新会话状态 Schema
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationStatusUpdate(BaseModel):
|
|||
|
|
"""更新会话状态请求 Schema。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
status: 新的会话状态
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
status: str = Field(..., description="会话状态: ai_handling/queued/serving/resolved")
|
|||
|
|
|
|||
|
|
@field_validator("status")
|
|||
|
|
@classmethod
|
|||
|
|
def validate_status(cls, v: str) -> str:
|
|||
|
|
"""校验会话状态值是否合法。"""
|
|||
|
|
if v not in VALID_STATUSES:
|
|||
|
|
raise ValueError(f"无效的会话状态: {v},合法值为: {VALID_STATUSES}")
|
|||
|
|
return v
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 坐席接单 Schema
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationAssign(BaseModel):
|
|||
|
|
"""坐席接单请求 Schema。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
agent_id: 接单的坐席ID
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
agent_id: str = Field(..., min_length=1, max_length=64, description="坐席ID")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 摇人(邀请协作)Schema
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationInvite(BaseModel):
|
|||
|
|
"""摇人邀请请求 Schema。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
agent_id: 被邀请的坐席ID
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
agent_id: str = Field(..., min_length=1, max_length=64, description="被邀请的坐席ID")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 邀请员工/部门加入会话 Schema(P0-09~P0-11 邀请功能)
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ParticipantInfo(BaseModel):
|
|||
|
|
"""被邀请人信息。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
id: 企微员工UserID 或部门ID
|
|||
|
|
name: 姓名 或 部门名称
|
|||
|
|
department: 部门(仅员工类型有)
|
|||
|
|
type: 类型 — employee(个人)或 department(部门)
|
|||
|
|
avatar: 头像URL(从企微通讯录或employees表获取)
|
|||
|
|
joined: 是否已加入(邀请后、点击加入前为 False)
|
|||
|
|
joined_at: 加入时间(ISO 格式字符串)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
id: str = Field(..., min_length=1, max_length=64, description="企微员工UserID或部门ID")
|
|||
|
|
name: str = Field(..., min_length=1, max_length=128, description="姓名或部门名称")
|
|||
|
|
department: str = Field(default="", max_length=256, description="部门(仅员工类型)")
|
|||
|
|
type: str = Field(default="employee", description="类型: employee/department")
|
|||
|
|
avatar: str = Field(default="", max_length=512, description="头像URL")
|
|||
|
|
joined: Optional[bool] = Field(default=None, description="是否已加入")
|
|||
|
|
joined_at: Optional[str] = Field(default=None, description="加入时间(ISO格式)")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class InviteParticipantRequest(BaseModel):
|
|||
|
|
"""邀请员工/部门加入会话请求 Schema。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
participants: 被邀请人列表
|
|||
|
|
history_mode: 历史消息共享模式 — recent10(最近10条)/ all(全部)/ none(不共享)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
participants: List[ParticipantInfo] = Field(
|
|||
|
|
..., min_length=1, max_length=20, description="被邀请人列表"
|
|||
|
|
)
|
|||
|
|
history_mode: str = Field(
|
|||
|
|
default="recent10",
|
|||
|
|
description="历史消息共享模式: recent10/all/none",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
@field_validator("history_mode")
|
|||
|
|
@classmethod
|
|||
|
|
def validate_history_mode(cls, v: str) -> str:
|
|||
|
|
"""校验历史共享模式。"""
|
|||
|
|
valid_modes = {"recent10", "all", "none"}
|
|||
|
|
if v not in valid_modes:
|
|||
|
|
raise ValueError(f"无效的历史共享模式: {v},合法值为: {valid_modes}")
|
|||
|
|
return v
|
|||
|
|
|
|||
|
|
@field_validator("participants")
|
|||
|
|
@classmethod
|
|||
|
|
def validate_participants_unique(cls, v: List[ParticipantInfo]) -> List[ParticipantInfo]:
|
|||
|
|
"""校验参与者ID不重复。"""
|
|||
|
|
ids = [p.id for p in v]
|
|||
|
|
if len(ids) != len(set(ids)):
|
|||
|
|
raise ValueError("参与者ID不能重复")
|
|||
|
|
return v
|
|||
|
|
|
|||
|
|
|
|||
|
|
class JoinConversationRequest(BaseModel):
|
|||
|
|
"""被邀请人加入会话请求 Schema。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
employee_id: 被邀请人的企微UserID
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
employee_id: str = Field(..., min_length=1, max_length=64, description="企微员工UserID")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 会话响应 Schema(返回给前端的数据结构)
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationResponse(BaseModel):
|
|||
|
|
"""会话响应 Schema。
|
|||
|
|
|
|||
|
|
返回给前端(坐席端/H5端)的会话数据结构。
|
|||
|
|
使用 from_attributes=True 支持从 SQLAlchemy 模型直接转换。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
id: 会话ID
|
|||
|
|
employee_id: 企微员工UserID
|
|||
|
|
employee_name: 员工姓名
|
|||
|
|
department: 部门
|
|||
|
|
position: 岗位
|
|||
|
|
level: 等级
|
|||
|
|
status: 会话状态
|
|||
|
|
is_vip: VIP标记
|
|||
|
|
is_pinned: 置顶标记
|
|||
|
|
is_todo: 代办标记
|
|||
|
|
urgency_score: 紧急度评分
|
|||
|
|
tags: 标签集合
|
|||
|
|
assigned_agent_id: 分配的坐席ID
|
|||
|
|
collaborating_agent_ids: 协作坐席ID列表
|
|||
|
|
last_message_at: 最后消息时间
|
|||
|
|
last_message_summary: 最后消息摘要
|
|||
|
|
created_at: 创建时间
|
|||
|
|
updated_at: 更新时间
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
id: str
|
|||
|
|
employee_id: str
|
|||
|
|
employee_name: str
|
|||
|
|
department: str
|
|||
|
|
position: str
|
|||
|
|
level: str
|
|||
|
|
status: str
|
|||
|
|
is_vip: bool
|
|||
|
|
is_pinned: bool
|
|||
|
|
is_todo: bool
|
|||
|
|
urgency_score: int
|
|||
|
|
tags: Dict[str, Any]
|
|||
|
|
assigned_agent_id: Optional[str] = None
|
|||
|
|
collaborating_agent_ids: List[str] = Field(default_factory=list, description="协作坐席ID列表")
|
|||
|
|
# 被邀请参与会话的人员列表(邀请功能 P0-09~P0-11)
|
|||
|
|
participants: List[ParticipantInfo] = Field(default_factory=list, description="被邀请参与会话的人员列表")
|
|||
|
|
last_message_at: Optional[datetime] = None
|
|||
|
|
last_message_summary: str
|
|||
|
|
created_at: datetime
|
|||
|
|
updated_at: datetime
|
|||
|
|
|
|||
|
|
# ----- 坐席会话全局可见扩展字段 -----
|
|||
|
|
# 是否为当前坐席的会话
|
|||
|
|
is_mine: bool = Field(default=False, description="是否为当前坐席的会话")
|
|||
|
|
# 分配的坐席姓名(其他坐席会话显示用)
|
|||
|
|
assigned_agent_name: Optional[str] = Field(default=None, description="分配的坐席姓名")
|
|||
|
|
# 是否可以接手(其他坐席已接单的会话为 True)
|
|||
|
|
can_grab: bool = Field(default=False, description="是否可以接手")
|
|||
|
|
|
|||
|
|
# ----- 多坐席协作扩展字段 -----
|
|||
|
|
# 协作坐席姓名映射(agent_id → name)
|
|||
|
|
collaborating_agent_names: Dict[str, str] = Field(
|
|||
|
|
default_factory=dict, description="协作坐席姓名映射"
|
|||
|
|
)
|
|||
|
|
# 当前坐席是否为协作坐席(非主责)
|
|||
|
|
is_collaborator: bool = Field(default=False, description="是否为协作坐席")
|
|||
|
|
|
|||
|
|
# ----- v5.3 新增:影响范围 / 阻断性 / 情绪状态 -----
|
|||
|
|
# 影响范围(受影响人数,0=未评估)
|
|||
|
|
impact_scope: int = Field(default=0, description="影响范围")
|
|||
|
|
# 阻断性标记(问题是否阻断员工正常工作流程)
|
|||
|
|
is_blocking: bool = Field(default=False, description="阻断性标记")
|
|||
|
|
# 情绪状态(normal/worried/angry/urgent)
|
|||
|
|
emotion_state: str = Field(default="normal", description="情绪状态")
|
|||
|
|
|
|||
|
|
model_config = {"from_attributes": True}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
# 会话列表响应 Schema(包含分页信息)
|
|||
|
|
# --------------------------------------------------------------------------
|
|||
|
|
class ConversationListResponse(BaseModel):
|
|||
|
|
"""会话列表响应 Schema。
|
|||
|
|
|
|||
|
|
包含会话列表和总数,用于分页查询。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
items: 会话列表
|
|||
|
|
total: 总数
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
items: List[ConversationResponse]
|
|||
|
|
total: int
|