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