feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
377
backend/app/services/ai/practice_analysis_service.py
Normal file
377
backend/app/services/ai/practice_analysis_service.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
陪练分析报告服务 - Python 原生实现
|
||||
|
||||
功能:
|
||||
- 分析陪练对话历史
|
||||
- 生成综合评分、能力维度评估
|
||||
- 提供对话标注和改进建议
|
||||
|
||||
提供稳定可靠的陪练分析报告生成能力。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .ai_service import AIService, AIResponse
|
||||
from .llm_json_parser import parse_with_fallback, clean_llm_output
|
||||
from .prompts.practice_analysis_prompts import (
|
||||
SYSTEM_PROMPT,
|
||||
USER_PROMPT,
|
||||
PRACTICE_ANALYSIS_SCHEMA,
|
||||
SCORE_BREAKDOWN_ITEMS,
|
||||
ABILITY_DIMENSIONS,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==================== 数据结构 ====================
|
||||
|
||||
@dataclass
|
||||
class ScoreBreakdownItem:
|
||||
"""分数细分项"""
|
||||
name: str
|
||||
score: float
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AbilityDimensionItem:
|
||||
"""能力维度项"""
|
||||
name: str
|
||||
score: float
|
||||
feedback: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class DialogueAnnotation:
|
||||
"""对话标注"""
|
||||
sequence: int
|
||||
tags: List[str]
|
||||
comment: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Suggestion:
|
||||
"""改进建议"""
|
||||
title: str
|
||||
content: str
|
||||
example: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PracticeAnalysisResult:
|
||||
"""陪练分析结果"""
|
||||
success: bool
|
||||
total_score: float = 0.0
|
||||
score_breakdown: List[ScoreBreakdownItem] = field(default_factory=list)
|
||||
ability_dimensions: List[AbilityDimensionItem] = field(default_factory=list)
|
||||
dialogue_annotations: List[DialogueAnnotation] = field(default_factory=list)
|
||||
suggestions: List[Suggestion] = 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 {
|
||||
"analysis": {
|
||||
"total_score": self.total_score,
|
||||
"score_breakdown": [
|
||||
{"name": s.name, "score": s.score, "description": s.description}
|
||||
for s in self.score_breakdown
|
||||
],
|
||||
"ability_dimensions": [
|
||||
{"name": d.name, "score": d.score, "feedback": d.feedback}
|
||||
for d in self.ability_dimensions
|
||||
],
|
||||
"dialogue_annotations": [
|
||||
{"sequence": a.sequence, "tags": a.tags, "comment": a.comment}
|
||||
for a in self.dialogue_annotations
|
||||
],
|
||||
"suggestions": [
|
||||
{"title": s.title, "content": s.content, "example": s.example}
|
||||
for s in self.suggestions
|
||||
],
|
||||
},
|
||||
"ai_provider": self.ai_provider,
|
||||
"ai_model": self.ai_model,
|
||||
"ai_tokens": self.ai_tokens,
|
||||
"ai_latency_ms": self.ai_latency_ms,
|
||||
}
|
||||
|
||||
def to_db_format(self) -> Dict[str, Any]:
|
||||
"""转换为数据库存储格式(兼容 PracticeReport 模型)"""
|
||||
return {
|
||||
"total_score": int(self.total_score),
|
||||
"score_breakdown": [
|
||||
{"name": s.name, "score": s.score, "description": s.description}
|
||||
for s in self.score_breakdown
|
||||
],
|
||||
"ability_dimensions": [
|
||||
{"name": d.name, "score": d.score, "feedback": d.feedback}
|
||||
for d in self.ability_dimensions
|
||||
],
|
||||
"dialogue_review": [
|
||||
{"sequence": a.sequence, "tags": a.tags, "comment": a.comment}
|
||||
for a in self.dialogue_annotations
|
||||
],
|
||||
"suggestions": [
|
||||
{"title": s.title, "content": s.content, "example": s.example}
|
||||
for s in self.suggestions
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ==================== 服务类 ====================
|
||||
|
||||
class PracticeAnalysisService:
|
||||
"""
|
||||
陪练分析报告服务
|
||||
|
||||
使用 Python 原生实现。
|
||||
|
||||
使用示例:
|
||||
```python
|
||||
service = PracticeAnalysisService()
|
||||
result = await service.analyze(
|
||||
db=db_session, # 传入 db_session 用于记录调用日志
|
||||
dialogue_history=[
|
||||
{"speaker": "user", "content": "您好,我想咨询一下..."},
|
||||
{"speaker": "ai", "content": "您好!很高兴为您服务..."}
|
||||
]
|
||||
)
|
||||
print(result.total_score)
|
||||
print(result.suggestions)
|
||||
```
|
||||
"""
|
||||
|
||||
MODULE_CODE = "practice_analysis"
|
||||
|
||||
async def analyze(
|
||||
self,
|
||||
dialogue_history: List[Dict[str, Any]],
|
||||
db: Any = None # 数据库会话,用于记录 AI 调用日志
|
||||
) -> PracticeAnalysisResult:
|
||||
"""
|
||||
分析陪练对话
|
||||
|
||||
Args:
|
||||
dialogue_history: 对话历史列表,每项包含 speaker, content, timestamp 等字段
|
||||
db: 数据库会话,用于记录调用日志(符合 AI 接入规范)
|
||||
|
||||
Returns:
|
||||
PracticeAnalysisResult 分析结果
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始分析陪练对话 - 对话轮次: {len(dialogue_history)}")
|
||||
|
||||
# 1. 验证输入
|
||||
if not dialogue_history or len(dialogue_history) < 2:
|
||||
return PracticeAnalysisResult(
|
||||
success=False,
|
||||
error="对话记录太少,无法生成分析报告(至少需要2轮对话)"
|
||||
)
|
||||
|
||||
# 2. 格式化对话历史
|
||||
dialogue_text = self._format_dialogue_history(dialogue_history)
|
||||
|
||||
# 3. 创建 AIService 实例(传入 db_session 用于记录调用日志)
|
||||
self._ai_service = AIService(module_code=self.MODULE_CODE, db_session=db)
|
||||
|
||||
# 4. 调用 AI 分析
|
||||
ai_response = await self._call_ai_analysis(dialogue_text)
|
||||
|
||||
logger.info(
|
||||
f"AI 分析完成 - provider: {ai_response.provider}, "
|
||||
f"tokens: {ai_response.total_tokens}, latency: {ai_response.latency_ms}ms"
|
||||
)
|
||||
|
||||
# 4. 解析 JSON 结果
|
||||
analysis_data = self._parse_analysis_result(ai_response.content)
|
||||
|
||||
# 5. 构建返回结果
|
||||
result = PracticeAnalysisResult(
|
||||
success=True,
|
||||
total_score=analysis_data.get("total_score", 0),
|
||||
score_breakdown=[
|
||||
ScoreBreakdownItem(
|
||||
name=s.get("name", ""),
|
||||
score=s.get("score", 0),
|
||||
description=s.get("description", "")
|
||||
)
|
||||
for s in analysis_data.get("score_breakdown", [])
|
||||
],
|
||||
ability_dimensions=[
|
||||
AbilityDimensionItem(
|
||||
name=d.get("name", ""),
|
||||
score=d.get("score", 0),
|
||||
feedback=d.get("feedback", "")
|
||||
)
|
||||
for d in analysis_data.get("ability_dimensions", [])
|
||||
],
|
||||
dialogue_annotations=[
|
||||
DialogueAnnotation(
|
||||
sequence=a.get("sequence", 0),
|
||||
tags=a.get("tags", []),
|
||||
comment=a.get("comment", "")
|
||||
)
|
||||
for a in analysis_data.get("dialogue_annotations", [])
|
||||
],
|
||||
suggestions=[
|
||||
Suggestion(
|
||||
title=s.get("title", ""),
|
||||
content=s.get("content", ""),
|
||||
example=s.get("example", "")
|
||||
)
|
||||
for s in analysis_data.get("suggestions", [])
|
||||
],
|
||||
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"陪练分析完成 - total_score: {result.total_score}, "
|
||||
f"annotations: {len(result.dialogue_annotations)}, "
|
||||
f"suggestions: {len(result.suggestions)}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"陪练分析失败: {e}", exc_info=True)
|
||||
return PracticeAnalysisResult(
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
def _format_dialogue_history(self, dialogue_history: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
格式化对话历史为文本
|
||||
|
||||
Args:
|
||||
dialogue_history: 对话历史列表
|
||||
|
||||
Returns:
|
||||
格式化后的对话文本
|
||||
"""
|
||||
lines = []
|
||||
for i, d in enumerate(dialogue_history, 1):
|
||||
speaker = d.get('speaker', 'unknown')
|
||||
content = d.get('content', '')
|
||||
|
||||
# 统一说话者标识
|
||||
if speaker in ['user', 'employee', 'consultant', '员工', '用户']:
|
||||
speaker_label = '员工'
|
||||
elif speaker in ['ai', 'customer', 'client', '顾客', '客户', 'AI']:
|
||||
speaker_label = '顾客'
|
||||
else:
|
||||
speaker_label = speaker
|
||||
|
||||
lines.append(f"[{i}] {speaker_label}: {content}")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
async def _call_ai_analysis(self, dialogue_text: str) -> AIResponse:
|
||||
"""调用 AI 进行分析"""
|
||||
user_message = USER_PROMPT.format(dialogue_history=dialogue_text)
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
response = await self._ai_service.chat(
|
||||
messages=messages,
|
||||
temperature=0.7,
|
||||
prompt_name="practice_analysis"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def _parse_analysis_result(self, ai_output: str) -> 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=PRACTICE_ANALYSIS_SCHEMA,
|
||||
default={"analysis": {}},
|
||||
validate_schema=True,
|
||||
on_error="default"
|
||||
)
|
||||
|
||||
# 提取 analysis 部分
|
||||
analysis = parsed.get("analysis", {})
|
||||
|
||||
# 确保 score_breakdown 完整
|
||||
existing_breakdown = {s.get("name") for s in analysis.get("score_breakdown", [])}
|
||||
for item_name in SCORE_BREAKDOWN_ITEMS:
|
||||
if item_name not in existing_breakdown:
|
||||
logger.warning(f"缺少分数维度: {item_name},使用默认值")
|
||||
analysis.setdefault("score_breakdown", []).append({
|
||||
"name": item_name,
|
||||
"score": 75,
|
||||
"description": "暂无详细评价"
|
||||
})
|
||||
|
||||
# 确保 ability_dimensions 完整
|
||||
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": 75,
|
||||
"feedback": "暂无详细评价"
|
||||
})
|
||||
|
||||
# 确保有建议
|
||||
if not analysis.get("suggestions"):
|
||||
analysis["suggestions"] = [
|
||||
{
|
||||
"title": "持续练习",
|
||||
"content": "建议继续进行陪练练习,提升整体表现",
|
||||
"example": "每周进行2-3次陪练,针对薄弱环节重点练习"
|
||||
}
|
||||
]
|
||||
|
||||
return analysis
|
||||
|
||||
|
||||
# ==================== 全局实例 ====================
|
||||
|
||||
practice_analysis_service = PracticeAnalysisService()
|
||||
|
||||
|
||||
# ==================== 便捷函数 ====================
|
||||
|
||||
async def analyze_practice_session(
|
||||
dialogue_history: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
便捷函数:分析陪练会话
|
||||
|
||||
Args:
|
||||
dialogue_history: 对话历史列表
|
||||
|
||||
Returns:
|
||||
分析结果字典(兼容原有格式)
|
||||
"""
|
||||
result = await practice_analysis_service.analyze(dialogue_history)
|
||||
return result.to_dict()
|
||||
|
||||
Reference in New Issue
Block a user