445 lines
14 KiB
Python
445 lines
14 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 知识库自动迭代服务
|
||
# =============================================================================
|
||
# 说明:知识库自动迭代核心服务
|
||
# 功能:
|
||
# 1. 分析错误标注的高频问题
|
||
# 2. 查找未命中知识库的会话
|
||
# 3. 生成优化建议
|
||
# 4. 审核通过后应用到知识库
|
||
# 5. 推送审核通知给管理员
|
||
# =============================================================================
|
||
|
||
import json
|
||
import logging
|
||
from datetime import datetime, timedelta
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
import httpx
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.config import settings
|
||
from app.models.knowledge_base import KnowledgeBase
|
||
from app.models.knowledge_suggestion import KnowledgeSuggestion
|
||
from app.models.conversation import Conversation
|
||
from app.models.conversation_annotation import ConversationAnnotation
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class KnowledgeIterationService:
|
||
"""知识库自动迭代服务。
|
||
|
||
分析会话标注和会话数据,生成知识库优化建议,
|
||
支持管理员审核后自动应用到知识库。
|
||
"""
|
||
|
||
def __init__(self):
|
||
"""初始化服务。"""
|
||
# AI 分析 API(复用 Dify)
|
||
self.ai_api_url = settings.dify_wingman_api_url
|
||
self.ai_api_key = settings.dify_wingman_api_key
|
||
self.ai_timeout = settings.dify_wingman_timeout
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 核心方法:分析并生成建议
|
||
# --------------------------------------------------------------------------
|
||
|
||
async def analyze_and_generate_suggestions(
|
||
self,
|
||
db: AsyncSession,
|
||
days: int = 7,
|
||
) -> Dict[str, Any]:
|
||
"""分析并生成知识库优化建议。
|
||
|
||
分析过去N天的标注数据和会话数据,生成优化建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
days: 分析过去N天的数据,默认7天
|
||
|
||
Returns:
|
||
Dict: {
|
||
"annotations_analyzed": int, # 分析的标注数量
|
||
"conversations_analyzed": int, # 分析的会话数量
|
||
"suggestions_generated": int, # 生成的建议数量
|
||
}
|
||
"""
|
||
result = {
|
||
"annotations_analyzed": 0,
|
||
"conversations_analyzed": 0,
|
||
"suggestions_generated": 0,
|
||
}
|
||
|
||
# 1. 分析错误标注的高频问题
|
||
annotation_suggestions = await self._analyze_annotation_data(db, days)
|
||
result["annotations_analyzed"] = annotation_suggestions.get("analyzed_count", 0)
|
||
result["suggestions_generated"] += annotation_suggestions.get(
|
||
"suggestions_count", 0
|
||
)
|
||
|
||
# 2. 分析未命中知识库的会话
|
||
conversation_suggestions = await self._analyze_conversation_data(db, days)
|
||
result["conversations_analyzed"] = conversation_suggestions.get(
|
||
"analyzed_count", 0
|
||
)
|
||
result["suggestions_generated"] += conversation_suggestions.get(
|
||
"suggestions_count", 0
|
||
)
|
||
|
||
logger.info(
|
||
f"知识库迭代分析完成: "
|
||
f"标注={result['annotations_analyzed']}, "
|
||
f"会话={result['conversations_analyzed']}, "
|
||
f"建议={result['suggestions_generated']}"
|
||
)
|
||
|
||
return result
|
||
|
||
async def _analyze_annotation_data(
|
||
self, db: AsyncSession, days: int
|
||
) -> Dict[str, Any]:
|
||
"""分析标注数据,生成优化建议。
|
||
|
||
查找被标记为"无用"的AI回复,分析高频错误原因,
|
||
尝试生成FAQ更新建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
days: 分析过去N天的数据
|
||
|
||
Returns:
|
||
Dict: 分析结果统计
|
||
"""
|
||
since = datetime.now() - timedelta(days=days)
|
||
|
||
# 查询过去N天的无效标注
|
||
stmt = (
|
||
select(ConversationAnnotation)
|
||
.where(ConversationAnnotation.feedback == "useless")
|
||
.where(ConversationAnnotation.created_at >= since)
|
||
)
|
||
result = await db.execute(stmt)
|
||
annotations = result.scalars().all()
|
||
|
||
if not annotations:
|
||
return {"analyzed_count": 0, "suggestions_count": 0}
|
||
|
||
# 按被标注的消息ID分组,统计高频错误
|
||
message_error_counts: Dict[str, int] = {}
|
||
for ann in annotations:
|
||
msg_id = ann.message_id
|
||
message_error_counts[msg_id] = message_error_counts.get(msg_id, 0) + 1
|
||
|
||
# 找出高频错误(被标注3次以上)
|
||
frequent_errors = {
|
||
msg_id: count
|
||
for msg_id, count in message_error_counts.items()
|
||
if count >= 3
|
||
}
|
||
|
||
if not frequent_errors:
|
||
return {"analyzed_count": len(annotations), "suggestions_count": 0}
|
||
|
||
# 调用AI分析错误模式,生成更新建议
|
||
suggestions_count = 0
|
||
for msg_id, error_count in frequent_errors.items():
|
||
# 生成优化建议
|
||
suggestion = await self._generate_update_suggestion(
|
||
db=db,
|
||
source_type="annotation",
|
||
source_data=[msg_id],
|
||
reason=f"该AI回复在过去{days}天内被标记为无用{error_count}次",
|
||
)
|
||
if suggestion:
|
||
db.add(suggestion)
|
||
suggestions_count += 1
|
||
|
||
await db.commit()
|
||
|
||
return {"analyzed_count": len(annotations), "suggestions_count": suggestions_count}
|
||
|
||
async def _analyze_conversation_data(
|
||
self, db: AsyncSession, days: int
|
||
) -> Dict[str, Any]:
|
||
"""分析会话数据,生成新增FAQ建议。
|
||
|
||
查找AI无法解决(转人工)的会话,分析生成新FAQ建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
days: 分析过去N天的数据
|
||
|
||
Returns:
|
||
Dict: 分析结果统计
|
||
"""
|
||
since = datetime.now() - timedelta(days=days)
|
||
|
||
# 查询过去N天的AI未解决会话(转人工的会话)
|
||
stmt = (
|
||
select(Conversation)
|
||
.where(Conversation.created_at >= since)
|
||
.where(Conversation.status.in_(["waiting_agent", "agentServing"]))
|
||
)
|
||
result = await db.execute(stmt)
|
||
conversations = result.scalars().all()
|
||
|
||
if not conversations:
|
||
return {"analyzed_count": 0, "suggestions_count": 0}
|
||
|
||
# 抽样分析(避免一次性处理太多)
|
||
sample_size = min(20, len(conversations))
|
||
sampled = conversations[:sample_size]
|
||
|
||
# 对每个会话进行分析
|
||
suggestions_count = 0
|
||
for conv in sampled:
|
||
# 检查是否已存在类似建议
|
||
existing = await self._check_existing_suggestion(db, conv.id)
|
||
if existing:
|
||
continue
|
||
|
||
# 生成新FAQ建议
|
||
suggestion = await self._generate_new_faq_suggestion(
|
||
db=db,
|
||
source_type="conversation",
|
||
source_data=[conv.id],
|
||
reason=f"会话'{conv.id}'中AI未能解决问题,需人工介入",
|
||
)
|
||
if suggestion:
|
||
db.add(suggestion)
|
||
suggestions_count += 1
|
||
|
||
await db.commit()
|
||
|
||
return {"analyzed_count": len(conversations), "suggestions_count": suggestions_count}
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 辅助方法
|
||
# --------------------------------------------------------------------------
|
||
|
||
async def _generate_update_suggestion(
|
||
self,
|
||
db: AsyncSession,
|
||
source_type: str,
|
||
source_data: List[str],
|
||
reason: str,
|
||
) -> Optional[KnowledgeSuggestion]:
|
||
"""生成知识库更新建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
source_type: 来源类型
|
||
source_data: 来源数据
|
||
reason: 生成理由
|
||
|
||
Returns:
|
||
Optional[KnowledgeSuggestion]: 建议对象
|
||
"""
|
||
# TODO: 调用AI生成具体的更新内容
|
||
# 当前返回示例数据,实际应调用 Dify API
|
||
|
||
return KnowledgeSuggestion(
|
||
suggestion_type="update",
|
||
status="pending",
|
||
title="[待AI生成] 优化建议",
|
||
content="请通过AI分析生成具体的更新内容",
|
||
category="其他",
|
||
tags=[],
|
||
source_type=source_type,
|
||
source_data=source_data,
|
||
reason=reason,
|
||
)
|
||
|
||
async def _generate_new_faq_suggestion(
|
||
self,
|
||
db: AsyncSession,
|
||
source_type: str,
|
||
source_data: List[str],
|
||
reason: str,
|
||
) -> Optional[KnowledgeSuggestion]:
|
||
"""生成新FAQ建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
source_type: 来源类型
|
||
source_data: 来源数据
|
||
reason: 生成理由
|
||
|
||
Returns:
|
||
Optional[KnowledgeSuggestion]: 建议对象
|
||
"""
|
||
# TODO: 调用AI生成具体的FAQ内容
|
||
# 当前返回示例数据,实际应调用 Dify API
|
||
|
||
return KnowledgeSuggestion(
|
||
suggestion_type="new_faq",
|
||
status="pending",
|
||
title="[待AI生成] 新FAQ建议",
|
||
content="请通过AI分析生成具体的问题和答案",
|
||
category="其他",
|
||
tags=[],
|
||
source_type=source_type,
|
||
source_data=source_data,
|
||
reason=reason,
|
||
)
|
||
|
||
async def _check_existing_suggestion(
|
||
self, db: AsyncSession, source_id: str
|
||
) -> bool:
|
||
"""检查是否已存在相关建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
source_id: 来源ID
|
||
|
||
Returns:
|
||
bool: 是否已存在
|
||
"""
|
||
stmt = (
|
||
select(KnowledgeSuggestion)
|
||
.where(KnowledgeSuggestion.status == "pending")
|
||
.where(KnowledgeSuggestion.source_data.contains(source_id))
|
||
)
|
||
result = await db.execute(stmt)
|
||
existing = result.scalars().first()
|
||
return existing is not None
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 审核与应用
|
||
# --------------------------------------------------------------------------
|
||
|
||
async def approve_suggestion(
|
||
self,
|
||
db: AsyncSession,
|
||
suggestion_id: str,
|
||
reviewer_id: str,
|
||
) -> Optional[KnowledgeSuggestion]:
|
||
"""审核通过建议并应用到知识库。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
suggestion_id: 建议ID
|
||
reviewer_id: 审核人ID
|
||
|
||
Returns:
|
||
Optional[KnowledgeSuggestion]: 更新后的建议对象
|
||
"""
|
||
# 获取建议
|
||
stmt = select(KnowledgeSuggestion).where(
|
||
KnowledgeSuggestion.id == suggestion_id
|
||
)
|
||
result = await db.execute(stmt)
|
||
suggestion = result.scalar_one_or_none()
|
||
|
||
if not suggestion:
|
||
return None
|
||
|
||
# 更新状态
|
||
suggestion.status = "approved"
|
||
suggestion.reviewer_id = reviewer_id
|
||
suggestion.reviewed_at = datetime.now()
|
||
|
||
# 如果是新FAQ或更新,创建对应的知识库条目
|
||
if suggestion.suggestion_type in ("new_faq", "update"):
|
||
kb = KnowledgeBase(
|
||
title=suggestion.title,
|
||
content=suggestion.content,
|
||
category=suggestion.category,
|
||
tags=suggestion.tags,
|
||
)
|
||
db.add(kb)
|
||
suggestion.status = "applied"
|
||
|
||
await db.commit()
|
||
await db.refresh(suggestion)
|
||
|
||
logger.info(f"建议已审核通过并应用: {suggestion_id}")
|
||
return suggestion
|
||
|
||
async def reject_suggestion(
|
||
self,
|
||
db: AsyncSession,
|
||
suggestion_id: str,
|
||
reviewer_id: str,
|
||
reject_reason: str,
|
||
) -> Optional[KnowledgeSuggestion]:
|
||
"""拒绝建议。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
suggestion_id: 建议ID
|
||
reviewer_id: 审核人ID
|
||
reject_reason: 拒绝理由
|
||
|
||
Returns:
|
||
Optional[KnowledgeSuggestion]: 更新后的建议对象
|
||
"""
|
||
stmt = select(KnowledgeSuggestion).where(
|
||
KnowledgeSuggestion.id == suggestion_id
|
||
)
|
||
result = await db.execute(stmt)
|
||
suggestion = result.scalar_one_or_none()
|
||
|
||
if not suggestion:
|
||
return None
|
||
|
||
suggestion.status = "rejected"
|
||
suggestion.reviewer_id = reviewer_id
|
||
suggestion.reviewed_at = datetime.now()
|
||
suggestion.reject_reason = reject_reason
|
||
|
||
await db.commit()
|
||
await db.refresh(suggestion)
|
||
|
||
logger.info(f"建议已拒绝: {suggestion_id}, 理由: {reject_reason}")
|
||
return suggestion
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 查询统计
|
||
# --------------------------------------------------------------------------
|
||
|
||
async def get_suggestion_stats(self, db: AsyncSession) -> Dict[str, int]:
|
||
"""获取建议统计。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
Dict: 统计数据
|
||
"""
|
||
# 总数
|
||
stmt = select(KnowledgeSuggestion)
|
||
result = await db.execute(stmt)
|
||
all_suggestions = result.scalars().all()
|
||
|
||
stats = {
|
||
"total": len(all_suggestions),
|
||
"pending": 0,
|
||
"approved": 0,
|
||
"rejected": 0,
|
||
"applied": 0,
|
||
"new_faq_count": 0,
|
||
"update_count": 0,
|
||
"outdated_count": 0,
|
||
}
|
||
|
||
for s in all_suggestions:
|
||
if s.status in stats:
|
||
stats[s.status] += 1
|
||
if s.suggestion_type == "new_faq":
|
||
stats["new_faq_count"] += 1
|
||
elif s.suggestion_type == "update":
|
||
stats["update_count"] += 1
|
||
elif s.suggestion_type == "outdated":
|
||
stats["outdated_count"] += 1
|
||
|
||
return stats
|
||
|
||
|
||
# 依赖注入函数
|
||
async def dep_knowledge_iteration_service() -> KnowledgeIterationService:
|
||
"""获取知识库迭代服务实例。"""
|
||
return KnowledgeIterationService()
|