""" 陪练场景准备服务 - Python 原生实现 功能: - 根据课程ID获取知识点 - 调用 AI 生成陪练场景配置 - 解析并返回结构化场景数据 提供稳定可靠的陪练场景提取能力。 """ 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.practice_scene_prompts import ( SYSTEM_PROMPT, USER_PROMPT, PRACTICE_SCENE_SCHEMA, DEFAULT_SCENE_TYPE, DEFAULT_DIFFICULTY, ) logger = logging.getLogger(__name__) # ==================== 数据结构 ==================== @dataclass class PracticeScene: """陪练场景数据结构""" name: str description: str background: str ai_role: str objectives: List[str] keywords: List[str] type: str = DEFAULT_SCENE_TYPE difficulty: str = DEFAULT_DIFFICULTY @dataclass class PracticeSceneResult: """陪练场景生成结果""" success: bool scene: Optional[PracticeScene] = None raw_response: Dict[str, Any] = field(default_factory=dict) ai_provider: str = "" ai_model: str = "" ai_tokens: int = 0 ai_latency_ms: int = 0 knowledge_points_count: int = 0 error: str = "" # ==================== 服务类 ==================== class PracticeSceneService: """ 陪练场景准备服务 使用 Python 原生实现。 使用示例: ```python service = PracticeSceneService() result = await service.prepare_practice_knowledge( db=db_session, course_id=1 ) if result.success: print(result.scene.name) print(result.scene.objectives) ``` """ def __init__(self): """初始化服务""" self.ai_service = AIService(module_code="practice_scene") async def prepare_practice_knowledge( self, db: AsyncSession, course_id: int ) -> PracticeSceneResult: """ 准备陪练所需的知识内容并生成场景 陪练知识准备的 Python 实现。 Args: db: 数据库会话(支持多租户,由调用方传入对应租户的数据库连接) course_id: 课程ID Returns: PracticeSceneResult: 包含场景配置和元信息的结果对象 """ try: logger.info(f"开始陪练知识准备 - course_id: {course_id}") # 1. 查询知识点 knowledge_points = await self._fetch_knowledge_points(db, course_id) if not knowledge_points: logger.warning(f"课程没有知识点 - course_id: {course_id}") return PracticeSceneResult( success=False, error=f"课程 {course_id} 没有可用的知识点" ) logger.info(f"获取到 {len(knowledge_points)} 个知识点 - course_id: {course_id}") # 2. 格式化知识点为文本 knowledge_text = self._format_knowledge_points(knowledge_points) # 3. 调用 AI 生成场景 ai_response = await self._call_ai_generation(knowledge_text) logger.info( f"AI 生成完成 - provider: {ai_response.provider}, " f"tokens: {ai_response.total_tokens}, latency: {ai_response.latency_ms}ms" ) # 4. 解析 JSON 结果 scene_data = self._parse_scene_response(ai_response.content) if not scene_data: logger.error(f"场景解析失败 - course_id: {course_id}") return PracticeSceneResult( success=False, raw_response={"ai_output": ai_response.content}, ai_provider=ai_response.provider, ai_model=ai_response.model, ai_tokens=ai_response.total_tokens, ai_latency_ms=ai_response.latency_ms, knowledge_points_count=len(knowledge_points), error="AI 输出解析失败" ) # 5. 构建场景对象 scene = self._build_scene_object(scene_data) logger.info( f"陪练场景生成成功 - course_id: {course_id}, " f"scene_name: {scene.name}, type: {scene.type}" ) return PracticeSceneResult( success=True, scene=scene, raw_response=scene_data, ai_provider=ai_response.provider, ai_model=ai_response.model, ai_tokens=ai_response.total_tokens, ai_latency_ms=ai_response.latency_ms, knowledge_points_count=len(knowledge_points) ) except Exception as e: logger.error( f"陪练知识准备失败 - course_id: {course_id}, error: {e}", exc_info=True ) return PracticeSceneResult( success=False, error=str(e) ) async def _fetch_knowledge_points( self, db: AsyncSession, course_id: int ) -> List[Dict[str, Any]]: """ 从数据库获取课程知识点 获取课程知识点 """ # 知识点查询 SQL: # SELECT kp.name, kp.description # FROM knowledge_points kp # INNER JOIN course_materials cm ON kp.material_id = cm.id # WHERE kp.course_id = {course_id} # AND kp.is_deleted = 0 # AND cm.is_deleted = 0 # ORDER BY kp.id; sql = text(""" SELECT kp.name, kp.description FROM knowledge_points kp INNER JOIN course_materials cm ON kp.material_id = cm.id WHERE kp.course_id = :course_id AND kp.is_deleted = 0 AND cm.is_deleted = 0 ORDER BY kp.id """) try: result = await db.execute(sql, {"course_id": course_id}) rows = result.fetchall() knowledge_points = [] for row in rows: knowledge_points.append({ "name": row[0], "description": row[1] or "" }) return knowledge_points except Exception as e: logger.error(f"查询知识点失败: {e}") raise ExternalServiceError(f"数据库查询失败: {e}") def _format_knowledge_points(self, knowledge_points: List[Dict[str, Any]]) -> str: """ 将知识点列表格式化为文本 Args: knowledge_points: 知识点列表 Returns: 格式化后的文本 """ lines = [] for i, kp in enumerate(knowledge_points, 1): name = kp.get("name", "") description = kp.get("description", "") if description: lines.append(f"{i}. {name}\n {description}") else: lines.append(f"{i}. {name}") return "\n\n".join(lines) async def _call_ai_generation(self, knowledge_text: str) -> AIResponse: """ 调用 AI 生成陪练场景 Args: knowledge_text: 格式化后的知识点文本 Returns: AI 响应对象 """ # 构建用户消息 user_message = USER_PROMPT.format(knowledge_points=knowledge_text) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message} ] # 调用 AI(自动降级:4sapi.com → OpenRouter) response = await self.ai_service.chat( messages=messages, temperature=0.7, # 适中的创意性 prompt_name="practice_scene_generation" ) return response def _parse_scene_response(self, ai_output: str) -> Optional[Dict[str, Any]]: """ 解析 AI 输出的场景 JSON 使用 LLM JSON Parser 进行多层兜底解析 Args: ai_output: AI 原始输出 Returns: 解析后的字典,失败返回 None """ # 先清洗输出 cleaned_output, rules = clean_llm_output(ai_output) if rules: logger.debug(f"AI 输出已清洗: {rules}") # 使用带 Schema 校验的解析 result = parse_with_fallback( cleaned_output, schema=PRACTICE_SCENE_SCHEMA, default=None, validate_schema=True, on_error="none" ) return result def _build_scene_object(self, scene_data: Dict[str, Any]) -> PracticeScene: """ 从解析的字典构建场景对象 Args: scene_data: 解析后的场景数据 Returns: PracticeScene 对象 """ # 提取 scene 字段(JSON 格式为 {"scene": {...}}) scene = scene_data.get("scene", scene_data) return PracticeScene( name=scene.get("name", "陪练场景"), description=scene.get("description", ""), background=scene.get("background", ""), ai_role=scene.get("ai_role", "AI扮演客户"), objectives=scene.get("objectives", []), keywords=scene.get("keywords", []), type=scene.get("type", DEFAULT_SCENE_TYPE), difficulty=scene.get("difficulty", DEFAULT_DIFFICULTY) ) def scene_to_dict(self, scene: PracticeScene) -> Dict[str, Any]: """ 将场景对象转换为字典 便于 API 响应序列化 Args: scene: PracticeScene 对象 Returns: 字典格式的场景数据 """ return { "scene": { "name": scene.name, "description": scene.description, "background": scene.background, "ai_role": scene.ai_role, "objectives": scene.objectives, "keywords": scene.keywords, "type": scene.type, "difficulty": scene.difficulty } } # ==================== 全局实例 ==================== practice_scene_service = PracticeSceneService() # ==================== 便捷函数 ==================== async def prepare_practice_knowledge( db: AsyncSession, course_id: int ) -> PracticeSceneResult: """ 准备陪练所需的知识内容(便捷函数) Args: db: 数据库会话 course_id: 课程ID Returns: PracticeSceneResult 结果对象 """ return await practice_scene_service.prepare_practice_knowledge(db, course_id)