Files
banban/talkingq-url/services/conversation_history.py
2026-03-24 15:04:36 +08:00

413 lines
17 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 asyncio
import time
from typing import Dict, List, Optional, Tuple
from sqlalchemy import select, update, insert, and_, delete
from database.models import ConversationHistory, ConversationMessage
from services.database_service_base import DatabaseServiceBase
from utils.logger import session_logger
from config import settings
class DeviceConversationHistory:
def __init__(self):
self.history = []
self.last_interaction_time = asyncio.get_running_loop().time()
self.role_key = None # 角色标识
self.conversation_id = None # 会话ID用于关联消息
class ConversationHistoryManager(DatabaseServiceBase):
def __init__(self):
super().__init__(service_name="conversation_history")
self.histories: Dict[str, DeviceConversationHistory] = {} # 内存缓存
self.lock = asyncio.Lock()
self.role_histories: Dict[Tuple[str, str], DeviceConversationHistory] = {} # (device_id, role_key) -> history
self.max_cache_size = 500 # 最大缓存条目数
async def get_history(self, device_id: str, role_key: Optional[str] = None) -> DeviceConversationHistory:
"""获取设备指定角色的对话历史"""
await self._init_database()
if not role_key:
from services.device_config import device_config_manager
device_config = await device_config_manager.get_config(device_id)
if device_config:
role_key = device_config.selected_role_key
else:
from config import settings
role_key = settings.selected_role_key
cache_key = (device_id, role_key)
if cache_key in self.role_histories:
return self.role_histories[cache_key]
db_session = await self.get_session()
try:
query = select(ConversationHistory).where(
and_(
ConversationHistory.device_id == device_id,
ConversationHistory.role_key == role_key
)
)
result = await db_session.execute(query)
db_history = result.scalar_one_or_none()
history = DeviceConversationHistory()
history.role_key = role_key
if db_history:
messages_query = select(ConversationMessage).where(
ConversationMessage.conversation_id == db_history.id
).order_by(ConversationMessage.timestamp)
messages_result = await db_session.execute(messages_query)
messages = messages_result.scalars().all()
message_pairs = []
user_msg = None
for msg in messages:
if msg.is_user:
user_msg = msg.content
elif user_msg is not None:
message_pairs.append({"user": user_msg, "assistant": msg.content})
user_msg = None
if user_msg is not None:
message_pairs.append({"user": user_msg, "assistant": ""})
history.history = message_pairs
history.last_interaction_time = db_history.last_interaction_time
history.conversation_id = db_history.id
self._add_to_cache(cache_key, history)
return history
else:
history = DeviceConversationHistory()
history.history = []
history.role_key = role_key
self._add_to_cache(cache_key, history)
return history
except Exception as e:
session_logger.error(device_id, "conversation_history", f"从数据库获取对话历史失败: {str(e)}")
history = DeviceConversationHistory()
history.role_key = role_key
return history
finally:
await db_session.close()
async def set_history(self, device_id: str, history: DeviceConversationHistory, role_key: Optional[str] = None):
"""设置设备指定角色的对话历史"""
await self._init_database()
if not role_key and not history.role_key:
from services.device_config import device_config_manager
device_config = await device_config_manager.get_config(device_id)
role_key = device_config.selected_role_key if device_config else settings.selected_role_key
effective_role_key = role_key or history.role_key
history.role_key = effective_role_key
cache_key = (device_id, effective_role_key)
self._add_to_cache(cache_key, history)
db_session = await self.get_session()
try:
query = select(ConversationHistory).where(
and_(
ConversationHistory.device_id == device_id,
ConversationHistory.role_key == effective_role_key
)
)
result = await db_session.execute(query)
db_history = result.scalar_one_or_none()
current_time = time.time()
if db_history:
stmt = update(ConversationHistory).where(
ConversationHistory.id == db_history.id
).values(
last_interaction_time=current_time
)
await db_session.execute(stmt)
conversation_id = db_history.id
else:
stmt = insert(ConversationHistory).values(
device_id=device_id,
role_key=effective_role_key,
last_interaction_time=current_time
)
result = await db_session.execute(stmt)
await db_session.commit()
query = select(ConversationHistory).where(
and_(
ConversationHistory.device_id == device_id,
ConversationHistory.role_key == effective_role_key
)
)
result = await db_session.execute(query)
db_history = result.scalar_one()
conversation_id = db_history.id
history.conversation_id = conversation_id
if history.history and len(history.history) > 0:
latest_msg = history.history[-1]
latest_msg_query = select(ConversationMessage).where(
and_(
ConversationMessage.conversation_id == conversation_id,
ConversationMessage.content == latest_msg["user"],
ConversationMessage.is_user == True
)
).order_by(ConversationMessage.timestamp.desc())
latest_msg_result = await db_session.execute(latest_msg_query)
existing_user_msg = latest_msg_result.scalar_one_or_none()
if not existing_user_msg:
user_stmt = insert(ConversationMessage).values(
conversation_id=conversation_id,
is_user=True,
content=latest_msg["user"],
timestamp=current_time - 0.1 # 确保用户消息在助手消息之前
)
await db_session.execute(user_stmt)
assistant_stmt = insert(ConversationMessage).values(
conversation_id=conversation_id,
is_user=False,
content=latest_msg["assistant"],
timestamp=current_time
)
await db_session.execute(assistant_stmt)
await db_session.commit()
session_logger.info(
device_id,
"conversation_history",
f"已保存设备 {device_id} 的角色 {effective_role_key} 对话历史到数据库"
)
except Exception as e:
await db_session.rollback()
session_logger.error(
device_id,
"conversation_history",
f"保存对话历史到数据库失败: {str(e)}"
)
finally:
await db_session.close()
async def remove_history(self, device_id: str, role_key: Optional[str] = None):
"""删除设备的对话历史"""
await self._init_database()
if not role_key:
from services.device_config import device_config_manager
device_config = await device_config_manager.get_config(device_id)
role_key = device_config.selected_role_key if device_config else settings.selected_role_key
cache_key = (device_id, role_key)
if cache_key in self.role_histories:
del self.role_histories[cache_key]
db_session = await self.get_session()
try:
query = select(ConversationHistory.id).where(
and_(
ConversationHistory.device_id == device_id,
ConversationHistory.role_key == role_key
)
)
result = await db_session.execute(query)
conversation_id = result.scalar_one_or_none()
if conversation_id:
msg_stmt = delete(ConversationMessage).where(
ConversationMessage.conversation_id == conversation_id
)
await db_session.execute(msg_stmt)
hist_stmt = delete(ConversationHistory).where(
ConversationHistory.id == conversation_id
)
await db_session.execute(hist_stmt)
await db_session.commit()
session_logger.info(
device_id,
"conversation_history",
f"已删除设备 {device_id} 的角色 {role_key} 对话历史"
)
except Exception as e:
await db_session.rollback()
session_logger.error(
device_id,
"conversation_history",
f"删除对话历史失败: {str(e)}"
)
finally:
await db_session.close()
async def clear_all_histories(self, device_id: str):
"""清除设备的所有对话历史(所有角色)"""
await self._init_database()
keys_to_remove = []
for (dev_id, _) in self.role_histories.keys():
if dev_id == device_id:
keys_to_remove.append((dev_id, _))
for key in keys_to_remove:
del self.role_histories[key]
db_session = await self.get_session()
try:
query = select(ConversationHistory.id).where(
ConversationHistory.device_id == device_id
)
result = await db_session.execute(query)
conversation_ids = result.scalars().all()
if conversation_ids:
for cid in conversation_ids:
msg_stmt = delete(ConversationMessage).where(
ConversationMessage.conversation_id == cid
)
await db_session.execute(msg_stmt)
stmt = delete(ConversationHistory).where(
ConversationHistory.device_id == device_id
)
await db_session.execute(stmt)
await db_session.commit()
session_logger.info(
device_id,
"conversation_history",
f"已清除设备 {device_id} 的所有对话历史"
)
except Exception as e:
await db_session.rollback()
session_logger.error(
device_id,
"conversation_history",
f"清除所有对话历史失败: {str(e)}"
)
finally:
await db_session.close()
async def get_all_histories(self):
"""获取所有历史记录,主要用于清理过期记录"""
await self._init_database()
db_session = await self.get_session()
try:
query = select(ConversationHistory)
result = await db_session.execute(query)
db_histories = result.scalars().all()
histories = []
for db_history in db_histories:
history = DeviceConversationHistory()
history.last_interaction_time = db_history.last_interaction_time
history.role_key = db_history.role_key
history.conversation_id = db_history.id
histories.append(((db_history.device_id, db_history.role_key), history))
return histories
except Exception as e:
session_logger.error("system", "conversation_history", f"获取所有对话历史失败: {str(e)}")
return []
finally:
await db_session.close()
async def get_device_role_histories(self, device_id: str) -> Dict[str, DeviceConversationHistory]:
"""获取设备的所有角色会话历史"""
await self._init_database()
db_session = await self.get_session()
try:
query = select(ConversationHistory).where(
ConversationHistory.device_id == device_id
)
result = await db_session.execute(query)
db_histories = result.scalars().all()
histories = {}
for db_history in db_histories:
role_key = db_history.role_key
history = DeviceConversationHistory()
history.role_key = role_key
history.last_interaction_time = db_history.last_interaction_time
history.conversation_id = db_history.id
messages_query = select(ConversationMessage).where(
ConversationMessage.conversation_id == db_history.id
).order_by(ConversationMessage.timestamp)
messages_result = await db_session.execute(messages_query)
messages = messages_result.scalars().all()
for msg in messages:
if msg.is_user:
history.history.append({
"user": msg.content,
"assistant": "",
"timestamp": msg.timestamp
})
else:
if history.history and "assistant" in history.history[-1]:
history.history[-1]["assistant"] = msg.content
histories[role_key] = history
return histories
except Exception as e:
session_logger.error(device_id, "conversation_history", f"获取设备所有角色历史失败: {str(e)}")
return {}
finally:
await db_session.close()
async def clear_cache(self, device_id: str = None, role_key: str = None):
"""清除指定设备和角色的缓存,或全部缓存"""
if device_id and role_key:
cache_key = (device_id, role_key)
if cache_key in self.role_histories:
del self.role_histories[cache_key]
elif device_id:
keys_to_remove = []
for (dev_id, _), _ in self.role_histories.items():
if dev_id == device_id:
keys_to_remove.append((dev_id, _))
for key in keys_to_remove:
if key in self.role_histories:
del self.role_histories[key]
else:
self.role_histories.clear()
def _add_to_cache(self, cache_key: Tuple[str, str], history: DeviceConversationHistory):
"""添加到缓存,检查大小限制"""
# 检查缓存大小限制
if len(self.role_histories) >= self.max_cache_size:
self._cleanup_old_cache_entries()
self.role_histories[cache_key] = history
def _cleanup_old_cache_entries(self):
"""清理最旧的缓存条目"""
if not self.role_histories:
return
# 按最后交互时间排序删除最旧的25%条目
sorted_items = sorted(
self.role_histories.items(),
key=lambda x: x[1].last_interaction_time
)
cleanup_count = max(1, len(sorted_items) // 4)
for cache_key, _ in sorted_items[:cleanup_count]:
if cache_key in self.role_histories:
del self.role_histories[cache_key]
session_logger.info(
"system", "cache_cleanup",
f"对话历史缓存清理完成,删除了 {cleanup_count} 个条目,剩余 {len(self.role_histories)}"
)
conversation_history_manager = ConversationHistoryManager()