feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
479
backend/app/services/ai/ability_analysis_service.py
Normal file
479
backend/app/services/ai/ability_analysis_service.py
Normal file
@@ -0,0 +1,479 @@
|
||||
"""
|
||||
智能工牌能力分析与课程推荐服务 - 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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user