Files
012-kaopeilian/backend/app/services/ai/ability_analysis_service.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

480 lines
14 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.
"""
智能工牌能力分析与课程推荐服务 - 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()