Files
assist/src/dialogue/conversation_history.py
zhaojie c3560b43fd docs: update README and CLAUDE.md to v2.2.0
- Added documentation for audit tracking (IP address, invocation method).
- Updated database model descriptions for enhanced WorkOrder and Conversation fields.
- Documented the new UnifiedConfig system.
- Reflected enhanced logging transparency for knowledge base parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 00:08:09 +08:00

718 lines
29 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.
# -*- coding: utf-8 -*-
"""
对话历史管理器
支持Redis缓存和数据库持久化
"""
import json
import logging
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from ..core.database import db_manager
from ..core.models import Conversation, WorkOrder, WorkOrderSuggestion, KnowledgeEntry
from ..core.redis_manager import redis_manager
from src.config.unified_config import get_config
from sqlalchemy import and_, or_, desc
logger = logging.getLogger(__name__)
class ConversationHistoryManager:
"""对话历史管理器"""
def __init__(self):
self.max_history_length = 20 # 最大历史记录数
self.cache_ttl = 3600 * 24 # 缓存24小时
def _get_redis_client(self):
"""获取Redis客户端"""
return redis_manager.get_connection()
def _get_cache_key(self, user_id: str, work_order_id: Optional[int] = None) -> str:
"""生成缓存键"""
if work_order_id:
return f"conversation_history:work_order:{work_order_id}"
return f"conversation_history:user:{user_id}"
def save_conversation(
self,
user_id: str,
user_message: str,
assistant_response: str,
work_order_id: Optional[int] = None,
confidence_score: Optional[float] = None,
response_time: Optional[float] = None,
knowledge_used: Optional[List[int]] = None,
ip_address: Optional[str] = None,
invocation_method: Optional[str] = None
) -> int:
"""保存对话记录到数据库和Redis"""
conversation_id = 0
try:
# 保存到数据库
with db_manager.get_session() as session:
conversation = Conversation(
work_order_id=work_order_id,
user_message=user_message,
assistant_response=assistant_response,
confidence_score=confidence_score,
response_time=response_time,
knowledge_used=json.dumps(knowledge_used or [], ensure_ascii=False),
ip_address=ip_address,
invocation_method=invocation_method,
timestamp=datetime.now()
)
session.add(conversation)
session.commit()
conversation_id = conversation.id
# 保存到Redis缓存
self._save_to_cache(
user_id=user_id,
work_order_id=work_order_id,
user_message=user_message,
assistant_response=assistant_response,
conversation_id=conversation_id,
confidence_score=confidence_score,
response_time=response_time,
ip_address=ip_address,
invocation_method=invocation_method
)
logger.info(f"对话记录保存成功: ID={conversation_id}")
return conversation_id
except Exception as e:
logger.error(f"保存对话记录失败: {e}")
return conversation_id
def _save_to_cache(
self,
user_id: str,
work_order_id: Optional[int],
user_message: str,
assistant_response: str,
conversation_id: int,
confidence_score: Optional[float] = None,
response_time: Optional[float] = None,
ip_address: Optional[str] = None,
invocation_method: Optional[str] = None
):
"""保存对话到Redis缓存"""
redis_client = self._get_redis_client()
if not redis_client:
return
try:
cache_key = self._get_cache_key(user_id, work_order_id)
# 构建对话记录
conversation_record = {
"id": conversation_id,
"user_message": user_message,
"assistant_response": assistant_response,
"timestamp": datetime.now().isoformat(),
"confidence_score": confidence_score,
"response_time": response_time,
"ip_address": ip_address,
"invocation_method": invocation_method
}
# 添加到Redis列表
redis_client.lpush(cache_key, json.dumps(conversation_record, ensure_ascii=False))
# 限制列表长度
redis_client.ltrim(cache_key, 0, self.max_history_length - 1)
# 设置过期时间
redis_client.expire(cache_key, self.cache_ttl)
except Exception as e:
logger.error(f"保存到Redis缓存失败: {e}")
def get_conversation_history(
self,
user_id: str,
work_order_id: Optional[int] = None,
limit: int = 10,
offset: int = 0
) -> List[Dict[str, Any]]:
"""获取对话历史优先从Redis获取"""
try:
# 先尝试从Redis获取
if redis_client:
cached_history = self._get_from_cache(user_id, work_order_id, limit, offset)
if cached_history:
return cached_history
# 从数据库获取
return self._get_from_database(user_id, work_order_id, limit, offset)
except Exception as e:
logger.error(f"获取对话历史失败: {e}")
return []
def _get_from_cache(
self,
user_id: str,
work_order_id: Optional[int],
limit: int,
offset: int
) -> List[Dict[str, Any]]:
"""从Redis缓存获取对话历史"""
redis_client = self._get_redis_client()
if not redis_client:
return []
try:
cache_key = self._get_cache_key(user_id, work_order_id)
# 获取指定范围的记录
start = offset
end = offset + limit - 1
cached_data = redis_client.lrange(cache_key, start, end)
history = []
for data in cached_data:
try:
record = json.loads(data)
history.append(record)
except json.JSONDecodeError:
continue
return history
except Exception as e:
logger.error(f"从Redis获取对话历史失败: {e}")
return []
def _get_from_database(
self,
user_id: str,
work_order_id: Optional[int],
limit: int,
offset: int
) -> List[Dict[str, Any]]:
"""从数据库获取对话历史"""
try:
with db_manager.get_session() as session:
query = session.query(Conversation)
if work_order_id:
query = query.filter(Conversation.work_order_id == work_order_id)
conversations = query.order_by(Conversation.timestamp.desc()).offset(offset).limit(limit).all()
history = []
for conv in conversations:
history.append({
"id": conv.id,
"user_message": conv.user_message,
"assistant_response": conv.assistant_response,
"timestamp": conv.timestamp.isoformat(),
"confidence_score": conv.confidence_score,
"response_time": conv.response_time,
"ip_address": conv.ip_address,
"invocation_method": conv.invocation_method,
"knowledge_used": json.loads(conv.knowledge_used) if conv.knowledge_used else []
})
return history
except Exception as e:
logger.error(f"从数据库获取对话历史失败: {e}")
return []
def get_conversation_context(
self,
user_id: str,
work_order_id: Optional[int] = None,
context_length: int = 6
) -> str:
"""获取对话上下文用于LLM"""
try:
history = self.get_conversation_history(user_id, work_order_id, context_length)
if not history:
return ""
context_parts = []
for record in reversed(history): # 按时间正序
context_parts.append(f"用户: {record['user_message']}")
context_parts.append(f"助手: {record['assistant_response']}")
return "\n".join(context_parts)
except Exception as e:
logger.error(f"获取对话上下文失败: {e}")
return ""
def delete_conversation(self, conversation_id: int) -> bool:
"""删除对话记录"""
try:
with db_manager.get_session() as session:
conversation = session.query(Conversation).filter(
Conversation.id == conversation_id
).first()
if not conversation:
return False
# 从数据库删除
session.delete(conversation)
session.commit()
# 从Redis缓存删除需要重建缓存
self._invalidate_cache(conversation.work_order_id)
logger.info(f"对话记录删除成功: ID={conversation_id}")
return True
except Exception as e:
logger.error(f"删除对话记录失败: {e}")
return False
def delete_user_conversations(self, user_id: str, work_order_id: Optional[int] = None) -> int:
"""删除用户的所有对话记录"""
try:
with db_manager.get_session() as session:
query = session.query(Conversation)
if work_order_id:
query = query.filter(Conversation.work_order_id == work_order_id)
conversations = query.all()
count = len(conversations)
# 删除数据库记录
for conv in conversations:
session.delete(conv)
session.commit()
# 清除Redis缓存
self._invalidate_cache(work_order_id)
logger.info(f"删除用户对话记录成功: 用户={user_id}, 数量={count}")
return count
except Exception as e:
logger.error(f"删除用户对话记录失败: {e}")
return 0
def _invalidate_cache(self, work_order_id: Optional[int] = None):
"""清除相关缓存"""
redis_client = self._get_redis_client()
if not redis_client:
return
try:
# 清除工单相关缓存
if work_order_id:
cache_key = f"conversation_history:work_order:{work_order_id}"
redis_client.delete(cache_key)
# 清除所有用户缓存(简单粗暴的方式)
pattern = "conversation_history:user:*"
keys = redis_client.keys(pattern)
if keys:
redis_client.delete(*keys)
except Exception as e:
logger.error(f"清除缓存失败: {e}")
def get_conversation_stats(self, user_id: str, work_order_id: Optional[int] = None) -> Dict[str, Any]:
"""获取对话统计信息"""
try:
with db_manager.get_session() as session:
query = session.query(Conversation)
if work_order_id:
query = query.filter(Conversation.work_order_id == work_order_id)
total_count = query.count()
# 计算平均响应时间
conversations_with_time = query.filter(Conversation.response_time.isnot(None)).all()
avg_response_time = 0
if conversations_with_time:
total_time = sum(conv.response_time for conv in conversations_with_time)
avg_response_time = total_time / len(conversations_with_time)
# 计算平均置信度
conversations_with_confidence = query.filter(Conversation.confidence_score.isnot(None)).all()
avg_confidence = 0
if conversations_with_confidence:
total_confidence = sum(conv.confidence_score for conv in conversations_with_confidence)
avg_confidence = total_confidence / len(conversations_with_confidence)
return {
"total_conversations": total_count,
"avg_response_time": round(avg_response_time, 2),
"avg_confidence": round(avg_confidence, 2),
"cache_status": "connected" if redis_manager.test_connection() else "disconnected"
}
except Exception as e:
logger.error(f"获取对话统计失败: {e}")
return {
"total_conversations": 0,
"avg_response_time": 0,
"avg_confidence": 0,
"cache_status": "error"
}
def cleanup_old_conversations(self, days: int = 30) -> int:
"""清理旧对话记录"""
try:
cutoff_date = datetime.now() - timedelta(days=days)
with db_manager.get_session() as session:
old_conversations = session.query(Conversation).filter(
Conversation.timestamp < cutoff_date
).all()
count = len(old_conversations)
for conv in old_conversations:
session.delete(conv)
session.commit()
logger.info(f"清理旧对话记录成功: 数量={count}")
return count
except Exception as e:
logger.error(f"清理旧对话记录失败: {e}")
return 0
def get_workorder_complete_timeline(
self,
work_order_id: int,
include_ai_suggestions: bool = True,
include_feishu_sync: bool = True,
limit: int = 20
) -> List[Dict[str, Any]]:
"""获取工单的完整时间线包含对话、AI建议、飞书同步"""
try:
timeline = []
with db_manager.get_session() as session:
# 1. 获取基础对话记录
conversations = session.query(Conversation).filter(
Conversation.work_order_id == work_order_id
).order_by(Conversation.timestamp.desc()).limit(limit).all()
for conv in conversations:
timeline.append({
"id": conv.id,
"type": "conversation",
"timestamp": conv.timestamp,
"user_message": conv.user_message,
"assistant_response": conv.assistant_response,
"confidence_score": conv.confidence_score,
"response_time": conv.response_time,
"knowledge_used": json.loads(conv.knowledge_used) if conv.knowledge_used else []
})
# 2. 获取AI建议记录
if include_ai_suggestions:
suggestions = session.query(WorkOrderSuggestion).filter(
WorkOrderSuggestion.work_order_id == work_order_id
).order_by(WorkOrderSuggestion.created_at.desc()).limit(limit).all()
for suggestion in suggestions:
timeline.append({
"id": f"suggestion_{suggestion.id}",
"type": "ai_suggestion",
"timestamp": suggestion.created_at,
"ai_suggestion": suggestion.ai_suggestion,
"human_resolution": suggestion.human_resolution,
"ai_similarity": suggestion.ai_similarity,
"approved": suggestion.approved,
"use_human_resolution": suggestion.use_human_resolution,
"updated_at": suggestion.updated_at
})
# 3. 获取飞书同步记录从工单的feishu_record_id推断
if include_feishu_sync:
work_order = session.query(WorkOrder).filter(
WorkOrder.id == work_order_id
).first()
if work_order and work_order.feishu_record_id:
timeline.append({
"id": f"feishu_{work_order.feishu_record_id}",
"type": "feishu_sync",
"timestamp": work_order.created_at,
"feishu_record_id": work_order.feishu_record_id,
"order_id": work_order.order_id,
"title": work_order.title,
"description": work_order.description,
"category": work_order.category,
"priority": work_order.priority,
"status": work_order.status,
"source": work_order.source
})
# 按时间排序
timeline.sort(key=lambda x: x["timestamp"], reverse=True)
return timeline[:limit]
except Exception as e:
logger.error(f"获取工单完整时间线失败: {e}")
return []
def get_ai_suggestion_context(
self,
work_order_id: int,
suggestion_id: Optional[int] = None
) -> Dict[str, Any]:
"""获取AI建议的对话上下文"""
try:
context = {
"work_order_info": {},
"conversation_history": [],
"ai_suggestions": [],
"knowledge_base": []
}
with db_manager.get_session() as session:
# 1. 获取工单信息
work_order = session.query(WorkOrder).filter(
WorkOrder.id == work_order_id
).first()
if work_order:
context["work_order_info"] = {
"id": work_order.id,
"order_id": work_order.order_id,
"title": work_order.title,
"description": work_order.description,
"category": work_order.category,
"priority": work_order.priority,
"status": work_order.status,
"created_at": work_order.created_at.isoformat(),
"feishu_record_id": work_order.feishu_record_id
}
# 2. 获取相关对话历史
conversations = session.query(Conversation).filter(
Conversation.work_order_id == work_order_id
).order_by(Conversation.timestamp.desc()).limit(10).all()
for conv in conversations:
context["conversation_history"].append({
"id": conv.id,
"user_message": conv.user_message,
"assistant_response": conv.assistant_response,
"timestamp": conv.timestamp.isoformat(),
"confidence_score": conv.confidence_score
})
# 3. 获取AI建议历史
suggestions = session.query(WorkOrderSuggestion).filter(
WorkOrderSuggestion.work_order_id == work_order_id
).order_by(WorkOrderSuggestion.created_at.desc()).limit(5).all()
for suggestion in suggestions:
context["ai_suggestions"].append({
"id": suggestion.id,
"ai_suggestion": suggestion.ai_suggestion,
"human_resolution": suggestion.human_resolution,
"ai_similarity": suggestion.ai_similarity,
"approved": suggestion.approved,
"use_human_resolution": suggestion.use_human_resolution,
"created_at": suggestion.created_at.isoformat()
})
# 4. 获取相关知识库条目
if work_order:
knowledge_entries = session.query(KnowledgeEntry).filter(
and_(
KnowledgeEntry.is_active == True,
or_(
KnowledgeEntry.category == work_order.category,
KnowledgeEntry.question.contains(work_order.title[:20])
)
)
).limit(5).all()
for entry in knowledge_entries:
context["knowledge_base"].append({
"id": entry.id,
"question": entry.question,
"answer": entry.answer,
"category": entry.category,
"confidence_score": entry.confidence_score,
"is_verified": entry.is_verified
})
return context
except Exception as e:
logger.error(f"获取AI建议对话上下文失败: {e}")
return {}
def search_conversations_by_content(
self,
search_query: str,
work_order_id: Optional[int] = None,
conversation_type: Optional[str] = None,
limit: int = 20
) -> List[Dict[str, Any]]:
"""根据内容搜索对话记录包含AI建议"""
try:
results = []
with db_manager.get_session() as session:
# 搜索基础对话
conv_query = session.query(Conversation)
if work_order_id:
conv_query = conv_query.filter(Conversation.work_order_id == work_order_id)
conversations = conv_query.filter(
or_(
Conversation.user_message.contains(search_query),
Conversation.assistant_response.contains(search_query)
)
).order_by(Conversation.timestamp.desc()).limit(limit).all()
for conv in conversations:
results.append({
"id": conv.id,
"type": "conversation",
"timestamp": conv.timestamp,
"user_message": conv.user_message,
"assistant_response": conv.assistant_response,
"work_order_id": conv.work_order_id,
"confidence_score": conv.confidence_score
})
# 搜索AI建议
if not conversation_type or conversation_type == "ai_suggestion":
suggestion_query = session.query(WorkOrderSuggestion)
if work_order_id:
suggestion_query = suggestion_query.filter(
WorkOrderSuggestion.work_order_id == work_order_id
)
suggestions = suggestion_query.filter(
or_(
WorkOrderSuggestion.ai_suggestion.contains(search_query),
WorkOrderSuggestion.human_resolution.contains(search_query)
)
).order_by(WorkOrderSuggestion.created_at.desc()).limit(limit).all()
for suggestion in suggestions:
results.append({
"id": f"suggestion_{suggestion.id}",
"type": "ai_suggestion",
"timestamp": suggestion.created_at,
"ai_suggestion": suggestion.ai_suggestion,
"human_resolution": suggestion.human_resolution,
"work_order_id": suggestion.work_order_id,
"ai_similarity": suggestion.ai_similarity,
"approved": suggestion.approved
})
# 按时间排序
results.sort(key=lambda x: x["timestamp"], reverse=True)
return results[:limit]
except Exception as e:
logger.error(f"搜索对话记录失败: {e}")
return []
def get_conversation_analytics(
self,
work_order_id: Optional[int] = None,
days: int = 7
) -> Dict[str, Any]:
"""获取对话分析数据包含AI建议统计"""
try:
cutoff_date = datetime.now() - timedelta(days=days)
with db_manager.get_session() as session:
analytics = {
"period_days": days,
"conversations": {},
"ai_suggestions": {},
"performance": {}
}
# 对话统计
conv_query = session.query(Conversation)
if work_order_id:
conv_query = conv_query.filter(Conversation.work_order_id == work_order_id)
conversations = conv_query.filter(
Conversation.timestamp >= cutoff_date
).all()
analytics["conversations"] = {
"total": len(conversations),
"avg_confidence": 0,
"avg_response_time": 0,
"high_confidence_count": 0
}
if conversations:
confidences = [c.confidence_score for c in conversations if c.confidence_score]
response_times = [c.response_time for c in conversations if c.response_time]
if confidences:
analytics["conversations"]["avg_confidence"] = sum(confidences) / len(confidences)
analytics["conversations"]["high_confidence_count"] = len([c for c in confidences if c >= 0.8])
if response_times:
analytics["conversations"]["avg_response_time"] = sum(response_times) / len(response_times)
# AI建议统计
suggestion_query = session.query(WorkOrderSuggestion)
if work_order_id:
suggestion_query = suggestion_query.filter(
WorkOrderSuggestion.work_order_id == work_order_id
)
suggestions = suggestion_query.filter(
WorkOrderSuggestion.created_at >= cutoff_date
).all()
analytics["ai_suggestions"] = {
"total": len(suggestions),
"approved_count": len([s for s in suggestions if s.approved]),
"avg_similarity": 0,
"human_resolution_count": len([s for s in suggestions if s.use_human_resolution])
}
if suggestions:
similarities = [s.ai_similarity for s in suggestions if s.ai_similarity]
if similarities:
analytics["ai_suggestions"]["avg_similarity"] = sum(similarities) / len(similarities)
# 性能指标
analytics["performance"] = {
"conversation_success_rate": 0,
"ai_suggestion_approval_rate": 0,
"knowledge_base_usage_rate": 0
}
if conversations:
successful_convs = len([c for c in conversations if c.confidence_score and c.confidence_score >= 0.5])
analytics["performance"]["conversation_success_rate"] = successful_convs / len(conversations)
if suggestions:
analytics["performance"]["ai_suggestion_approval_rate"] = len([s for s in suggestions if s.approved]) / len(suggestions)
return analytics
except Exception as e:
logger.error(f"获取对话分析数据失败: {e}")
return {}