Some checks failed
continuous-integration/drone/push Build is failing
更新内容: - 后端 AI 服务优化(能力分析、知识点解析等) - 前端考试和陪练界面更新 - 修复多个 prompt 和 JSON 解析问题 - 更新 Coze 语音客户端
485 lines
14 KiB
Python
485 lines
14 KiB
Python
"""
|
||
智能工牌能力分析与课程推荐服务 - Python 原生实现
|
||
|
||
功能:
|
||
- 分析员工与顾客的对话记录
|
||
- 评估多维度能力得分
|
||
- 基于能力短板推荐课程
|
||
|
||
提供稳定可靠的能力分析和课程推荐能力。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
from dataclasses import dataclass, field
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.exceptions import ExternalServiceError
|
||
|
||
from .ai_service import AIService, AIResponse
|
||
from .llm_json_parser import parse_with_fallback, clean_llm_output
|
||
from .prompts.ability_analysis_prompts import (
|
||
SYSTEM_PROMPT,
|
||
USER_PROMPT,
|
||
ABILITY_ANALYSIS_SCHEMA,
|
||
ABILITY_DIMENSIONS,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ==================== 数据结构 ====================
|
||
|
||
@dataclass
|
||
class AbilityDimension:
|
||
"""能力维度评分"""
|
||
name: str
|
||
score: float
|
||
feedback: str
|
||
|
||
|
||
@dataclass
|
||
class CourseRecommendation:
|
||
"""课程推荐"""
|
||
course_id: int
|
||
course_name: str
|
||
recommendation_reason: str
|
||
priority: str # high, medium, low
|
||
match_score: float
|
||
|
||
|
||
@dataclass
|
||
class AbilityAnalysisResult:
|
||
"""能力分析结果"""
|
||
success: bool
|
||
total_score: float = 0.0
|
||
ability_dimensions: List[AbilityDimension] = field(default_factory=list)
|
||
course_recommendations: List[CourseRecommendation] = field(default_factory=list)
|
||
ai_provider: str = ""
|
||
ai_model: str = ""
|
||
ai_tokens: int = 0
|
||
ai_latency_ms: int = 0
|
||
error: str = ""
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""转换为字典"""
|
||
return {
|
||
"success": self.success,
|
||
"total_score": self.total_score,
|
||
"ability_dimensions": [
|
||
{"name": d.name, "score": d.score, "feedback": d.feedback}
|
||
for d in self.ability_dimensions
|
||
],
|
||
"course_recommendations": [
|
||
{
|
||
"course_id": c.course_id,
|
||
"course_name": c.course_name,
|
||
"recommendation_reason": c.recommendation_reason,
|
||
"priority": c.priority,
|
||
"match_score": c.match_score,
|
||
}
|
||
for c in self.course_recommendations
|
||
],
|
||
"ai_provider": self.ai_provider,
|
||
"ai_model": self.ai_model,
|
||
"ai_tokens": self.ai_tokens,
|
||
"ai_latency_ms": self.ai_latency_ms,
|
||
"error": self.error,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class UserPositionInfo:
|
||
"""用户岗位信息"""
|
||
position_id: int
|
||
position_name: str
|
||
code: str
|
||
description: str
|
||
skills: Optional[Dict[str, Any]]
|
||
level: str
|
||
status: str
|
||
|
||
|
||
@dataclass
|
||
class CourseInfo:
|
||
"""课程信息"""
|
||
id: int
|
||
name: str
|
||
description: str
|
||
category: str
|
||
tags: Optional[List[str]]
|
||
difficulty_level: int
|
||
duration_hours: float
|
||
|
||
|
||
# ==================== 服务类 ====================
|
||
|
||
class AbilityAnalysisService:
|
||
"""
|
||
智能工牌能力分析服务
|
||
|
||
使用 Python 原生实现。
|
||
|
||
使用示例:
|
||
```python
|
||
service = AbilityAnalysisService()
|
||
result = await service.analyze(
|
||
db=db_session,
|
||
user_id=1,
|
||
dialogue_history="顾客:你好,我想了解一下你们的服务..."
|
||
)
|
||
print(result.total_score)
|
||
print(result.course_recommendations)
|
||
```
|
||
"""
|
||
|
||
def __init__(self):
|
||
"""初始化服务"""
|
||
self.ai_service = AIService(module_code="ability_analysis")
|
||
|
||
async def analyze(
|
||
self,
|
||
db: AsyncSession,
|
||
user_id: int,
|
||
dialogue_history: str
|
||
) -> AbilityAnalysisResult:
|
||
"""
|
||
分析员工能力并推荐课程
|
||
|
||
Args:
|
||
db: 数据库会话(支持多租户,每个租户传入各自的会话)
|
||
user_id: 用户ID
|
||
dialogue_history: 对话记录
|
||
|
||
Returns:
|
||
AbilityAnalysisResult 分析结果
|
||
"""
|
||
try:
|
||
logger.info(f"开始能力分析 - user_id: {user_id}")
|
||
|
||
# 1. 验证输入
|
||
if not dialogue_history or not dialogue_history.strip():
|
||
return AbilityAnalysisResult(
|
||
success=False,
|
||
error="对话记录不能为空"
|
||
)
|
||
|
||
# 2. 查询用户岗位信息
|
||
user_positions = await self._get_user_positions(db, user_id)
|
||
user_info_str = self._format_user_info(user_positions)
|
||
|
||
logger.info(f"用户岗位信息: {len(user_positions)} 个岗位")
|
||
|
||
# 3. 查询所有可选课程
|
||
courses = await self._get_published_courses(db)
|
||
courses_str = self._format_courses(courses)
|
||
|
||
logger.info(f"可选课程: {len(courses)} 门")
|
||
|
||
# 4. 调用 AI 分析
|
||
ai_response = await self._call_ai_analysis(
|
||
dialogue_history=dialogue_history,
|
||
user_info=user_info_str,
|
||
courses=courses_str
|
||
)
|
||
|
||
logger.info(
|
||
f"AI 分析完成 - provider: {ai_response.provider}, "
|
||
f"tokens: {ai_response.total_tokens}, latency: {ai_response.latency_ms}ms"
|
||
)
|
||
|
||
# 5. 解析 JSON 结果
|
||
analysis_data = self._parse_analysis_result(ai_response.content, courses)
|
||
|
||
# 6. 构建返回结果
|
||
result = AbilityAnalysisResult(
|
||
success=True,
|
||
total_score=analysis_data.get("total_score", 0),
|
||
ability_dimensions=[
|
||
AbilityDimension(
|
||
name=d.get("name", ""),
|
||
score=d.get("score", 0),
|
||
feedback=d.get("feedback", "")
|
||
)
|
||
for d in analysis_data.get("ability_dimensions", [])
|
||
],
|
||
course_recommendations=[
|
||
CourseRecommendation(
|
||
course_id=c.get("course_id", 0),
|
||
course_name=c.get("course_name", ""),
|
||
recommendation_reason=c.get("recommendation_reason", ""),
|
||
priority=c.get("priority", "medium"),
|
||
match_score=c.get("match_score", 0)
|
||
)
|
||
for c in analysis_data.get("course_recommendations", [])
|
||
],
|
||
ai_provider=ai_response.provider,
|
||
ai_model=ai_response.model,
|
||
ai_tokens=ai_response.total_tokens,
|
||
ai_latency_ms=ai_response.latency_ms,
|
||
)
|
||
|
||
logger.info(
|
||
f"能力分析完成 - user_id: {user_id}, total_score: {result.total_score}, "
|
||
f"recommendations: {len(result.course_recommendations)}"
|
||
)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"能力分析失败 - user_id: {user_id}, error: {e}",
|
||
exc_info=True
|
||
)
|
||
return AbilityAnalysisResult(
|
||
success=False,
|
||
error=str(e)
|
||
)
|
||
|
||
async def _get_user_positions(
|
||
self,
|
||
db: AsyncSession,
|
||
user_id: int
|
||
) -> List[UserPositionInfo]:
|
||
"""
|
||
查询用户的岗位信息
|
||
|
||
获取用户基本信息
|
||
"""
|
||
query = text("""
|
||
SELECT
|
||
p.id as position_id,
|
||
p.name as position_name,
|
||
p.code,
|
||
p.description,
|
||
p.skills,
|
||
p.level,
|
||
p.status
|
||
FROM positions p
|
||
INNER JOIN position_members pm ON p.id = pm.position_id
|
||
WHERE pm.user_id = :user_id
|
||
AND pm.is_deleted = 0
|
||
AND p.is_deleted = 0
|
||
""")
|
||
|
||
result = await db.execute(query, {"user_id": user_id})
|
||
rows = result.fetchall()
|
||
|
||
positions = []
|
||
for row in rows:
|
||
# 解析 skills JSON
|
||
skills = None
|
||
if row.skills:
|
||
if isinstance(row.skills, str):
|
||
try:
|
||
skills = json.loads(row.skills)
|
||
except json.JSONDecodeError:
|
||
skills = None
|
||
else:
|
||
skills = row.skills
|
||
|
||
positions.append(UserPositionInfo(
|
||
position_id=row.position_id,
|
||
position_name=row.position_name,
|
||
code=row.code or "",
|
||
description=row.description or "",
|
||
skills=skills,
|
||
level=row.level or "",
|
||
status=row.status or ""
|
||
))
|
||
|
||
return positions
|
||
|
||
async def _get_published_courses(self, db: AsyncSession) -> List[CourseInfo]:
|
||
"""
|
||
查询所有已发布的课程
|
||
|
||
获取所有课程列表
|
||
"""
|
||
query = text("""
|
||
SELECT
|
||
id,
|
||
name,
|
||
description,
|
||
category,
|
||
tags,
|
||
difficulty_level,
|
||
duration_hours
|
||
FROM courses
|
||
WHERE status = 'published'
|
||
AND is_deleted = FALSE
|
||
ORDER BY sort_order
|
||
""")
|
||
|
||
result = await db.execute(query)
|
||
rows = result.fetchall()
|
||
|
||
courses = []
|
||
for row in rows:
|
||
# 解析 tags JSON
|
||
tags = None
|
||
if row.tags:
|
||
if isinstance(row.tags, str):
|
||
try:
|
||
tags = json.loads(row.tags)
|
||
except json.JSONDecodeError:
|
||
tags = None
|
||
else:
|
||
tags = row.tags
|
||
|
||
courses.append(CourseInfo(
|
||
id=row.id,
|
||
name=row.name,
|
||
description=row.description or "",
|
||
category=row.category or "",
|
||
tags=tags,
|
||
difficulty_level=row.difficulty_level or 3,
|
||
duration_hours=row.duration_hours or 0
|
||
))
|
||
|
||
return courses
|
||
|
||
def _format_user_info(self, positions: List[UserPositionInfo]) -> str:
|
||
"""格式化用户岗位信息为文本"""
|
||
if not positions:
|
||
return "暂无岗位信息"
|
||
|
||
lines = []
|
||
for p in positions:
|
||
info = f"- 岗位:{p.position_name}({p.code})"
|
||
if p.level:
|
||
info += f",级别:{p.level}"
|
||
if p.description:
|
||
info += f"\n 描述:{p.description}"
|
||
if p.skills:
|
||
skills_str = json.dumps(p.skills, ensure_ascii=False)
|
||
info += f"\n 核心技能:{skills_str}"
|
||
lines.append(info)
|
||
|
||
return "\n".join(lines)
|
||
|
||
def _format_courses(self, courses: List[CourseInfo]) -> str:
|
||
"""格式化课程列表为文本"""
|
||
if not courses:
|
||
return "暂无可选课程"
|
||
|
||
lines = []
|
||
for c in courses:
|
||
info = f"- ID: {c.id}, 课程名称: {c.name}"
|
||
if c.category:
|
||
info += f", 分类: {c.category}"
|
||
if c.difficulty_level:
|
||
info += f", 难度: {c.difficulty_level}"
|
||
if c.duration_hours:
|
||
info += f", 时长: {c.duration_hours}小时"
|
||
if c.description:
|
||
# 截断过长的描述
|
||
desc = c.description[:100] + "..." if len(c.description) > 100 else c.description
|
||
info += f"\n 描述: {desc}"
|
||
lines.append(info)
|
||
|
||
return "\n".join(lines)
|
||
|
||
async def _call_ai_analysis(
|
||
self,
|
||
dialogue_history: str,
|
||
user_info: str,
|
||
courses: str
|
||
) -> AIResponse:
|
||
"""调用 AI 进行能力分析"""
|
||
# 构建用户消息
|
||
user_message = USER_PROMPT.format(
|
||
dialogue_history=dialogue_history,
|
||
user_info=user_info,
|
||
courses=courses
|
||
)
|
||
|
||
messages = [
|
||
{"role": "system", "content": SYSTEM_PROMPT},
|
||
{"role": "user", "content": user_message}
|
||
]
|
||
|
||
# 调用 AI(自动支持 4sapi → OpenRouter 降级)
|
||
response = await self.ai_service.chat(
|
||
messages=messages,
|
||
temperature=0.7, # 保持一定创意性
|
||
prompt_name="ability_analysis"
|
||
)
|
||
|
||
return response
|
||
|
||
def _parse_analysis_result(
|
||
self,
|
||
ai_output: str,
|
||
courses: List[CourseInfo]
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
解析 AI 输出的分析结果 JSON
|
||
|
||
使用 LLM JSON Parser 进行多层兜底解析
|
||
"""
|
||
# 先清洗输出
|
||
cleaned_output, rules = clean_llm_output(ai_output)
|
||
if rules:
|
||
logger.debug(f"AI 输出已清洗: {rules}")
|
||
|
||
# 使用带 Schema 校验的解析
|
||
parsed = parse_with_fallback(
|
||
cleaned_output,
|
||
schema=ABILITY_ANALYSIS_SCHEMA,
|
||
default={"analysis": {}},
|
||
validate_schema=True,
|
||
on_error="default"
|
||
)
|
||
|
||
# 提取 analysis 部分
|
||
analysis = parsed.get("analysis", {})
|
||
|
||
# 后处理:验证课程推荐的有效性
|
||
valid_course_ids = {c.id for c in courses}
|
||
valid_recommendations = []
|
||
|
||
for rec in analysis.get("course_recommendations", []):
|
||
course_id = rec.get("course_id")
|
||
if course_id in valid_course_ids:
|
||
valid_recommendations.append(rec)
|
||
else:
|
||
logger.warning(f"推荐的课程ID不存在: {course_id}")
|
||
|
||
analysis["course_recommendations"] = valid_recommendations
|
||
|
||
# 确保能力维度完整
|
||
existing_dims = {d.get("name") for d in analysis.get("ability_dimensions", [])}
|
||
for dim_name in ABILITY_DIMENSIONS:
|
||
if dim_name not in existing_dims:
|
||
logger.warning(f"缺少能力维度: {dim_name},使用默认值")
|
||
analysis.setdefault("ability_dimensions", []).append({
|
||
"name": dim_name,
|
||
"score": 70,
|
||
"feedback": "暂无具体评价"
|
||
})
|
||
|
||
return analysis
|
||
|
||
|
||
# ==================== 全局实例 ====================
|
||
|
||
ability_analysis_service = AbilityAnalysisService()
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|