# ============================================================================= # 企微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