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()
|