Files
012-kaopeilian/backend/app/services/ai/duo_practice_analysis_service.py
yuliang_guo b6aea2e23d
Some checks failed
continuous-integration/drone/push Build is failing
feat: 添加双人对练功能
- 新增数据库迁移脚本 (practice_rooms, practice_room_messages)
- 新增后端 API: 房间创建/加入/消息同步/报告生成
- 新增前端页面: 入口页/对练房间/报告页
- 新增 AI 双人评估服务和提示词
2026-01-28 15:20:03 +08:00

324 lines
12 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.
"""
双人对练分析服务
功能:
- 分析双人对练对话
- 生成双方评估报告
- 对话标注和建议
"""
import json
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from app.services.ai.ai_service import AIService
from app.services.ai.prompts.duo_practice_prompts import SYSTEM_PROMPT, USER_PROMPT
logger = logging.getLogger(__name__)
@dataclass
class UserEvaluation:
"""用户评估结果"""
user_name: str
role_name: str
total_score: int
dimensions: Dict[str, Dict[str, Any]]
highlights: List[str]
improvements: List[Dict[str, str]]
@dataclass
class DuoPracticeAnalysisResult:
"""双人对练分析结果"""
# 整体评估
interaction_quality: int = 0
scene_restoration: int = 0
overall_comment: str = ""
# 用户A评估
user_a_evaluation: Optional[UserEvaluation] = None
# 用户B评估
user_b_evaluation: Optional[UserEvaluation] = None
# 对话标注
dialogue_annotations: List[Dict[str, Any]] = field(default_factory=list)
# AI 元数据
raw_response: str = ""
ai_provider: str = ""
ai_model: str = ""
ai_latency_ms: int = 0
class DuoPracticeAnalysisService:
"""
双人对练分析服务
使用示例:
```python
service = DuoPracticeAnalysisService()
result = await service.analyze(
scene_name="销售场景",
scene_background="客户咨询产品",
role_a_name="销售顾问",
role_b_name="顾客",
user_a_name="张三",
user_b_name="李四",
dialogue_history=dialogue_list,
duration_seconds=300,
total_turns=20
)
```
"""
MODULE_CODE = "duo_practice_analysis"
async def analyze(
self,
scene_name: str,
scene_background: str,
role_a_name: str,
role_b_name: str,
role_a_description: str,
role_b_description: str,
user_a_name: str,
user_b_name: str,
dialogue_history: List[Dict[str, Any]],
duration_seconds: int,
total_turns: int,
db: Any = None
) -> DuoPracticeAnalysisResult:
"""
分析双人对练
Args:
scene_name: 场景名称
scene_background: 场景背景
role_a_name: 角色A名称
role_b_name: 角色B名称
role_a_description: 角色A描述
role_b_description: 角色B描述
user_a_name: 用户A名称
user_b_name: 用户B名称
dialogue_history: 对话历史列表
duration_seconds: 对练时长(秒)
total_turns: 总对话轮次
db: 数据库会话
Returns:
DuoPracticeAnalysisResult: 分析结果
"""
try:
logger.info(f"开始双人对练分析: {scene_name}, 轮次={total_turns}")
# 格式化对话历史
dialogue_text = self._format_dialogue_history(dialogue_history)
# 创建 AI 服务
ai_service = AIService(module_code=self.MODULE_CODE, db_session=db)
# 构建用户提示词
user_prompt = USER_PROMPT.format(
scene_name=scene_name,
scene_background=scene_background or "未设置",
role_a_name=role_a_name,
role_b_name=role_b_name,
role_a_description=role_a_description or f"扮演{role_a_name}角色",
role_b_description=role_b_description or f"扮演{role_b_name}角色",
user_a_name=user_a_name,
user_b_name=user_b_name,
dialogue_history=dialogue_text,
duration_seconds=duration_seconds,
total_turns=total_turns
)
# 调用 AI
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
]
ai_response = await ai_service.chat(
messages=messages,
model="gemini-3-flash-preview", # 使用快速模型
temperature=0.3,
prompt_name="duo_practice_analysis"
)
logger.info(f"AI 分析完成: provider={ai_response.provider}, latency={ai_response.latency_ms}ms")
# 解析 AI 输出
result = self._parse_analysis_result(
ai_response.content,
user_a_name=user_a_name,
user_b_name=user_b_name,
role_a_name=role_a_name,
role_b_name=role_b_name
)
# 填充 AI 元数据
result.raw_response = ai_response.content
result.ai_provider = ai_response.provider
result.ai_model = ai_response.model
result.ai_latency_ms = ai_response.latency_ms
return result
except Exception as e:
logger.error(f"双人对练分析失败: {e}", exc_info=True)
# 返回空结果
return DuoPracticeAnalysisResult(
overall_comment=f"分析失败: {str(e)}"
)
def _format_dialogue_history(self, dialogues: List[Dict[str, Any]]) -> str:
"""格式化对话历史"""
lines = []
for d in dialogues:
speaker = d.get("role_name") or d.get("speaker", "未知")
content = d.get("content", "")
seq = d.get("sequence", 0)
lines.append(f"[{seq}] {speaker}{content}")
return "\n".join(lines)
def _parse_analysis_result(
self,
ai_output: str,
user_a_name: str,
user_b_name: str,
role_a_name: str,
role_b_name: str
) -> DuoPracticeAnalysisResult:
"""解析 AI 输出"""
result = DuoPracticeAnalysisResult()
try:
# 尝试提取 JSON
json_str = ai_output
# 如果输出包含 markdown 代码块,提取其中的 JSON
if "```json" in ai_output:
start = ai_output.find("```json") + 7
end = ai_output.find("```", start)
json_str = ai_output[start:end].strip()
elif "```" in ai_output:
start = ai_output.find("```") + 3
end = ai_output.find("```", start)
json_str = ai_output[start:end].strip()
data = json.loads(json_str)
# 解析整体评估
overall = data.get("overall_evaluation", {})
result.interaction_quality = overall.get("interaction_quality", 0)
result.scene_restoration = overall.get("scene_restoration", 0)
result.overall_comment = overall.get("overall_comment", "")
# 解析用户A评估
user_a_data = data.get("user_a_evaluation", {})
if user_a_data:
result.user_a_evaluation = UserEvaluation(
user_name=user_a_data.get("user_name", user_a_name),
role_name=user_a_data.get("role_name", role_a_name),
total_score=user_a_data.get("total_score", 0),
dimensions=user_a_data.get("dimensions", {}),
highlights=user_a_data.get("highlights", []),
improvements=user_a_data.get("improvements", [])
)
# 解析用户B评估
user_b_data = data.get("user_b_evaluation", {})
if user_b_data:
result.user_b_evaluation = UserEvaluation(
user_name=user_b_data.get("user_name", user_b_name),
role_name=user_b_data.get("role_name", role_b_name),
total_score=user_b_data.get("total_score", 0),
dimensions=user_b_data.get("dimensions", {}),
highlights=user_b_data.get("highlights", []),
improvements=user_b_data.get("improvements", [])
)
# 解析对话标注
result.dialogue_annotations = data.get("dialogue_annotations", [])
except json.JSONDecodeError as e:
logger.warning(f"JSON 解析失败: {e}")
result.overall_comment = "AI 输出格式异常,请重试"
except Exception as e:
logger.error(f"解析分析结果失败: {e}")
result.overall_comment = f"解析失败: {str(e)}"
return result
def result_to_dict(self, result: DuoPracticeAnalysisResult) -> Dict[str, Any]:
"""将结果转换为字典(用于 API 响应)"""
return {
"overall_evaluation": {
"interaction_quality": result.interaction_quality,
"scene_restoration": result.scene_restoration,
"overall_comment": result.overall_comment
},
"user_a_evaluation": {
"user_name": result.user_a_evaluation.user_name,
"role_name": result.user_a_evaluation.role_name,
"total_score": result.user_a_evaluation.total_score,
"dimensions": result.user_a_evaluation.dimensions,
"highlights": result.user_a_evaluation.highlights,
"improvements": result.user_a_evaluation.improvements
} if result.user_a_evaluation else None,
"user_b_evaluation": {
"user_name": result.user_b_evaluation.user_name,
"role_name": result.user_b_evaluation.role_name,
"total_score": result.user_b_evaluation.total_score,
"dimensions": result.user_b_evaluation.dimensions,
"highlights": result.user_b_evaluation.highlights,
"improvements": result.user_b_evaluation.improvements
} if result.user_b_evaluation else None,
"dialogue_annotations": result.dialogue_annotations,
"ai_metadata": {
"provider": result.ai_provider,
"model": result.ai_model,
"latency_ms": result.ai_latency_ms
}
}
# ==================== 全局实例 ====================
duo_practice_analysis_service = DuoPracticeAnalysisService()
# ==================== 便捷函数 ====================
async def analyze_duo_practice(
scene_name: str,
scene_background: str,
role_a_name: str,
role_b_name: str,
role_a_description: str,
role_b_description: str,
user_a_name: str,
user_b_name: str,
dialogue_history: List[Dict[str, Any]],
duration_seconds: int,
total_turns: int,
db: Any = None
) -> DuoPracticeAnalysisResult:
"""便捷函数:分析双人对练"""
return await duo_practice_analysis_service.analyze(
scene_name=scene_name,
scene_background=scene_background,
role_a_name=role_a_name,
role_b_name=role_b_name,
role_a_description=role_a_description,
role_b_description=role_b_description,
user_a_name=user_a_name,
user_b_name=user_b_name,
dialogue_history=dialogue_history,
duration_seconds=duration_seconds,
total_turns=total_turns,
db=db
)