158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 趣味话术服务
|
||
# =============================================================================
|
||
# 说明:管理各场景的趣味话术,包括:
|
||
# 1. 根据触发场景返回对应话术
|
||
# 2. 从 funny_phrases 表读取话术配置
|
||
# 3. 支持按员工 VIP 等级自动切换话术(VIP → 正式版话术)
|
||
# 4. 预置 6 种场景的默认话术
|
||
# =============================================================================
|
||
|
||
import logging
|
||
import random
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.models.funny_phrase import FunnyPhrase
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class FunnyPhraseService:
|
||
"""趣味话术服务。
|
||
|
||
根据触发场景返回对应的趣味话术。
|
||
支持后台动态修改话术内容(通过 funny_phrases 表)。
|
||
"""
|
||
|
||
# 默认话术(当数据库未配置时使用,和 PRD 一致)
|
||
DEFAULT_PHRASES = {
|
||
"shake": "大哥,俺这就去摇人,稍等...",
|
||
"keyword": "收到!这就帮您摇位大神来",
|
||
"waiting": "人还在路上,别急别急~",
|
||
"connected": "人摇来了!IT坐席为您服务",
|
||
"timeout": "坐席都在忙,不过AI还在呢,要不先聊聊?我再继续摇",
|
||
"vip": "这就帮您安排专家,请稍候",
|
||
}
|
||
|
||
def __init__(self, db: AsyncSession):
|
||
"""初始化趣味话术服务。
|
||
|
||
Args:
|
||
db: 异步数据库会话
|
||
"""
|
||
self.db = db
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 获取话术
|
||
# --------------------------------------------------------------------------
|
||
async def get_phrase(
|
||
self, scene: str, is_vip: bool = False
|
||
) -> str:
|
||
"""根据触发场景获取趣味话术。
|
||
|
||
优先从 funny_phrases 表读取,如果未配置则使用默认话术。
|
||
VIP 员工自动使用 "vip" 场景的话术。
|
||
|
||
场景说明:
|
||
- click_shake / shake: 点击摇人按钮
|
||
- keyword: 关键词触发转人工
|
||
- waiting: 排队等待(30秒无人接单)
|
||
- connected: 坐席接入
|
||
- timeout: 等待超时(2分钟)
|
||
- vip: VIP员工专用
|
||
|
||
Args:
|
||
scene: 触发场景(shake/keyword/waiting/connected/timeout/vip)
|
||
is_vip: 是否 VIP 员工(VIP 优先使用 vip 场景话术)
|
||
|
||
Returns:
|
||
str: 话术内容
|
||
"""
|
||
# VIP 员工优先使用 vip 场景话术
|
||
actual_scene = scene
|
||
if is_vip and scene != "vip":
|
||
# 尝试获取 VIP 话术,如果不存在则回退到原场景
|
||
vip_phrase = await self._get_phrase_from_db("vip")
|
||
if vip_phrase:
|
||
logger.debug(f"VIP员工使用专属话术: scene=vip")
|
||
return vip_phrase
|
||
|
||
# 从数据库获取对应场景的话术
|
||
phrase = await self._get_phrase_from_db(actual_scene)
|
||
|
||
if phrase:
|
||
return phrase
|
||
|
||
# 数据库未配置,使用默认话术
|
||
default = self.DEFAULT_PHRASES.get(actual_scene, "请稍候...")
|
||
logger.debug(f"使用默认话术: scene={actual_scene}")
|
||
return default
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 从数据库获取话术
|
||
# --------------------------------------------------------------------------
|
||
async def _get_phrase_from_db(self, scene: str) -> Optional[str]:
|
||
"""从 funny_phrases 表获取指定场景的话术。
|
||
|
||
同一场景可能有多条话术,随机返回一条(增加趣味性)。
|
||
只返回 is_active=True 的话术。
|
||
|
||
Args:
|
||
scene: 触发场景
|
||
|
||
Returns:
|
||
Optional[str]: 话术内容,未找到返回 None
|
||
"""
|
||
stmt = (
|
||
select(FunnyPhrase)
|
||
.where(
|
||
FunnyPhrase.scene == scene,
|
||
FunnyPhrase.is_active == True,
|
||
)
|
||
.order_by(FunnyPhrase.sort_order)
|
||
)
|
||
result = await self.db.execute(stmt)
|
||
phrases = list(result.scalars().all())
|
||
|
||
if not phrases:
|
||
return None
|
||
|
||
# 随机选一条(如果有多个话术,增加随机趣味性)
|
||
chosen = random.choice(phrases)
|
||
return chosen.content
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 获取所有场景的话术
|
||
# --------------------------------------------------------------------------
|
||
async def get_all_phrases(self) -> dict:
|
||
"""获取所有场景的话术。
|
||
|
||
用于后台管理页面展示当前话术配置。
|
||
|
||
Returns:
|
||
dict: 按场景分组的话术字典
|
||
"""
|
||
stmt = select(FunnyPhrase).order_by(
|
||
FunnyPhrase.scene, FunnyPhrase.sort_order
|
||
)
|
||
result = await self.db.execute(stmt)
|
||
phrases = list(result.scalars().all())
|
||
|
||
# 按场景分组
|
||
grouped: dict = {}
|
||
for phrase in phrases:
|
||
if phrase.scene not in grouped:
|
||
grouped[phrase.scene] = []
|
||
grouped[phrase.scene].append({
|
||
"id": str(phrase.id),
|
||
"content": phrase.content,
|
||
"tone": phrase.tone,
|
||
"sort_order": phrase.sort_order,
|
||
"is_active": phrase.is_active,
|
||
})
|
||
|
||
return grouped
|