conversation_id显示用户对话
This commit is contained in:
parent
c0bba14ee8
commit
9153594375
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.1.5 on 2025-03-21 04:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('user_management', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='chathistory',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, default=dict, help_text="\n {\n 'model_id': 'xxx',\n 'dataset_id_list': ['id1', 'id2', ...],\n 'dataset_external_id_list': ['ext1', 'ext2', ...],\n 'primary_knowledge_base': 'id1'\n }\n "),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='chathistory',
|
||||||
|
index=models.Index(fields=['conversation_id', 'is_deleted'], name='chat_histor_convers_89bc43_idx'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,4 @@
|
|||||||
|
from itertools import count
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -286,13 +287,23 @@ class ChatHistory(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
# 保留与主知识库的关联
|
||||||
knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE)
|
knowledge_base = models.ForeignKey('KnowledgeBase', on_delete=models.CASCADE)
|
||||||
|
# 用于标识知识库组合的对话
|
||||||
conversation_id = models.CharField(max_length=100, db_index=True)
|
conversation_id = models.CharField(max_length=100, db_index=True)
|
||||||
parent_id = models.CharField(max_length=100, null=True, blank=True)
|
parent_id = models.CharField(max_length=100, null=True, blank=True)
|
||||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
tokens = models.IntegerField(default=0, help_text="消息token数")
|
tokens = models.IntegerField(default=0, help_text="消息token数")
|
||||||
metadata = models.JSONField(default=dict, blank=True)
|
# 扩展metadata字段,用于存储知识库组合信息
|
||||||
|
metadata = models.JSONField(default=dict, blank=True, help_text="""
|
||||||
|
{
|
||||||
|
'model_id': 'xxx',
|
||||||
|
'dataset_id_list': ['id1', 'id2', ...],
|
||||||
|
'dataset_external_id_list': ['ext1', 'ext2', ...],
|
||||||
|
'primary_knowledge_base': 'id1'
|
||||||
|
}
|
||||||
|
""")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
is_deleted = models.BooleanField(default=False)
|
is_deleted = models.BooleanField(default=False)
|
||||||
|
|
||||||
@ -302,6 +313,8 @@ class ChatHistory(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['conversation_id', 'created_at']),
|
models.Index(fields=['conversation_id', 'created_at']),
|
||||||
models.Index(fields=['user', 'created_at']),
|
models.Index(fields=['user', 'created_at']),
|
||||||
|
# 添加新的索引以支持知识库组合查询
|
||||||
|
models.Index(fields=['conversation_id', 'is_deleted']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -315,11 +328,69 @@ class ChatHistory(models.Model):
|
|||||||
is_deleted=False
|
is_deleted=False
|
||||||
).order_by('created_at')
|
).order_by('created_at')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_conversations_by_knowledge_bases(cls, dataset_ids, user):
|
||||||
|
"""根据知识库组合获取对话历史"""
|
||||||
|
# 对知识库ID列表排序以确保一致性
|
||||||
|
sorted_kb_ids = sorted(dataset_ids)
|
||||||
|
conversation_id = str(uuid.uuid5(
|
||||||
|
uuid.NAMESPACE_DNS,
|
||||||
|
'-'.join(sorted_kb_ids)
|
||||||
|
))
|
||||||
|
|
||||||
|
return cls.objects.filter(
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
user=user,
|
||||||
|
is_deleted=False
|
||||||
|
).order_by('created_at')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_knowledge_base_combinations(cls, user):
|
||||||
|
"""获取用户的所有知识库组合"""
|
||||||
|
return cls.objects.filter(
|
||||||
|
user=user,
|
||||||
|
is_deleted=False
|
||||||
|
).values('conversation_id').annotate(
|
||||||
|
last_message=max('created_at'),
|
||||||
|
message_count=count('id')
|
||||||
|
).values(
|
||||||
|
'conversation_id',
|
||||||
|
'last_message',
|
||||||
|
'message_count',
|
||||||
|
'metadata'
|
||||||
|
).order_by('-last_message')
|
||||||
|
|
||||||
|
def get_knowledge_bases(self):
|
||||||
|
"""获取此消息关联的所有知识库"""
|
||||||
|
if self.metadata and 'dataset_id_list' in self.metadata:
|
||||||
|
return KnowledgeBase.objects.filter(
|
||||||
|
id__in=self.metadata['dataset_id_list']
|
||||||
|
)
|
||||||
|
return KnowledgeBase.objects.filter(id=self.knowledge_base.id)
|
||||||
|
|
||||||
def soft_delete(self):
|
def soft_delete(self):
|
||||||
"""软删除消息"""
|
"""软删除消息"""
|
||||||
self.is_deleted = True
|
self.is_deleted = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""转换为字典格式"""
|
||||||
|
return {
|
||||||
|
'id': str(self.id),
|
||||||
|
'conversation_id': self.conversation_id,
|
||||||
|
'role': self.role,
|
||||||
|
'content': self.content,
|
||||||
|
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'metadata': self.metadata,
|
||||||
|
'knowledge_bases': [
|
||||||
|
{
|
||||||
|
'id': str(kb.id),
|
||||||
|
'name': kb.name,
|
||||||
|
'type': kb.type
|
||||||
|
} for kb in self.get_knowledge_bases()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.Model):
|
||||||
"""用户档案模型"""
|
"""用户档案模型"""
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||||
|
@ -26,7 +26,7 @@ import os
|
|||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpResponse
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from channels.exceptions import ChannelFull
|
from channels.exceptions import ChannelFull
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -93,79 +93,73 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request):
|
def list(self, request):
|
||||||
"""获取聊天记录列表"""
|
"""获取对话列表概览"""
|
||||||
try:
|
try:
|
||||||
# 获取查询参数
|
# 获取查询参数
|
||||||
dataset_id = request.query_params.get('dataset_id')
|
|
||||||
page = int(request.query_params.get('page', 1))
|
page = int(request.query_params.get('page', 1))
|
||||||
page_size = int(request.query_params.get('page_size', 10))
|
page_size = int(request.query_params.get('page_size', 10))
|
||||||
|
|
||||||
query = self.get_queryset()
|
# 获取所有对话的概览
|
||||||
|
latest_chats = self.get_queryset().values(
|
||||||
|
'conversation_id'
|
||||||
|
).annotate(
|
||||||
|
latest_id=Max('id'),
|
||||||
|
message_count=Count('id'),
|
||||||
|
last_message=Max('created_at')
|
||||||
|
).order_by('-last_message')
|
||||||
|
|
||||||
if dataset_id:
|
# 计算分页
|
||||||
# 获取特定知识库的完整对话历史
|
total = latest_chats.count()
|
||||||
records = query.filter(
|
start = (page - 1) * page_size
|
||||||
knowledge_base__id=dataset_id
|
end = start + page_size
|
||||||
).order_by('created_at')
|
chats = latest_chats[start:end]
|
||||||
|
|
||||||
conversation = {
|
results = []
|
||||||
'dataset_id': dataset_id,
|
for chat in chats:
|
||||||
'dataset_name': records.first().knowledge_base.name if records.exists() else None,
|
# 获取最新消息记录
|
||||||
'messages': [{
|
latest_record = ChatHistory.objects.get(id=chat['latest_id'])
|
||||||
'id': record.id,
|
|
||||||
'role': record.role,
|
# 从metadata中获取完整的知识库信息
|
||||||
'content': record.content,
|
dataset_info = []
|
||||||
'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
if latest_record.metadata:
|
||||||
} for record in records]
|
dataset_id_list = latest_record.metadata.get('dataset_id_list', [])
|
||||||
|
dataset_names = latest_record.metadata.get('dataset_names', [])
|
||||||
|
|
||||||
|
# 如果有知识库ID列表
|
||||||
|
if dataset_id_list:
|
||||||
|
# 如果同时有名称列表且长度匹配
|
||||||
|
if dataset_names and len(dataset_names) == len(dataset_id_list):
|
||||||
|
dataset_info = [{
|
||||||
|
'id': str(id),
|
||||||
|
'name': name
|
||||||
|
} for id, name in zip(dataset_id_list, dataset_names)]
|
||||||
|
else:
|
||||||
|
# 如果没有名称列表,则只返回ID
|
||||||
|
datasets = KnowledgeBase.objects.filter(id__in=dataset_id_list)
|
||||||
|
dataset_info = [{
|
||||||
|
'id': str(ds.id),
|
||||||
|
'name': ds.name
|
||||||
|
} for ds in datasets]
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'conversation_id': chat['conversation_id'],
|
||||||
|
'message_count': chat['message_count'],
|
||||||
|
'last_message': latest_record.content,
|
||||||
|
'last_time': chat['last_message'].strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'dataset_id_list': [ds['id'] for ds in dataset_info], # 添加完整的知识库ID列表
|
||||||
|
'datasets': dataset_info # 包含ID和名称的完整信息
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'results': results
|
||||||
}
|
}
|
||||||
|
})
|
||||||
return Response({
|
|
||||||
'code': 200,
|
|
||||||
'message': '获取成功',
|
|
||||||
'data': conversation
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
# 获取所有对话的概览
|
|
||||||
latest_chats = query.values(
|
|
||||||
'conversation_id',
|
|
||||||
'knowledge_base__id',
|
|
||||||
'knowledge_base__name'
|
|
||||||
).annotate(
|
|
||||||
latest_id=Max('id'),
|
|
||||||
message_count=Count('id'),
|
|
||||||
last_message=Max('created_at')
|
|
||||||
).order_by('-last_message')
|
|
||||||
|
|
||||||
# 计算分页
|
|
||||||
total = latest_chats.count()
|
|
||||||
start = (page - 1) * page_size
|
|
||||||
end = start + page_size
|
|
||||||
|
|
||||||
# 获取分页数据
|
|
||||||
chats = latest_chats[start:end]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for chat in chats:
|
|
||||||
latest_record = ChatHistory.objects.get(id=chat['latest_id'])
|
|
||||||
results.append({
|
|
||||||
'conversation_id': chat['conversation_id'],
|
|
||||||
'dataset_id': str(chat['knowledge_base__id']),
|
|
||||||
'dataset_name': chat['knowledge_base__name'],
|
|
||||||
'message_count': chat['message_count'],
|
|
||||||
'last_message': latest_record.content,
|
|
||||||
'last_time': chat['last_message'].strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
})
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'code': 200,
|
|
||||||
'message': '获取成功',
|
|
||||||
'data': {
|
|
||||||
'total': total,
|
|
||||||
'page': page,
|
|
||||||
'page_size': page_size,
|
|
||||||
'results': results
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取聊天记录失败: {str(e)}")
|
logger.error(f"获取聊天记录失败: {str(e)}")
|
||||||
@ -176,6 +170,119 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def conversation_detail(self, request):
|
||||||
|
"""获取特定对话的详细信息"""
|
||||||
|
try:
|
||||||
|
conversation_id = request.query_params.get('conversation_id')
|
||||||
|
if not conversation_id:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '缺少conversation_id参数',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 获取对话历史
|
||||||
|
messages = self.get_queryset().filter(
|
||||||
|
conversation_id=conversation_id
|
||||||
|
).order_by('created_at')
|
||||||
|
|
||||||
|
if not messages.exists():
|
||||||
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': '对话不存在',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 获取知识库信息
|
||||||
|
first_message = messages.first()
|
||||||
|
dataset_info = []
|
||||||
|
if first_message and first_message.metadata:
|
||||||
|
if 'dataset_id_list' in first_message.metadata:
|
||||||
|
datasets = KnowledgeBase.objects.filter(
|
||||||
|
id__in=first_message.metadata['dataset_id_list']
|
||||||
|
)
|
||||||
|
dataset_info = [{
|
||||||
|
'id': str(ds.id),
|
||||||
|
'name': ds.name,
|
||||||
|
'type': ds.type
|
||||||
|
} for ds in datasets]
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'conversation_id': conversation_id,
|
||||||
|
'datasets': dataset_info,
|
||||||
|
'messages': [{
|
||||||
|
'id': str(msg.id),
|
||||||
|
'role': msg.role,
|
||||||
|
'content': msg.content,
|
||||||
|
'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
} for msg in messages]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取对话详情失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取对话详情失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def available_datasets(self, request):
|
||||||
|
"""获取用户可访问的知识库列表"""
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
# 获取用户有权限访问的知识库
|
||||||
|
accessible_datasets = []
|
||||||
|
|
||||||
|
# 1. 获取用户通过权限表直接授权的知识库
|
||||||
|
permission_datasets = KnowledgeBase.objects.filter(
|
||||||
|
id__in=KBPermissionModel.objects.filter(
|
||||||
|
user=user,
|
||||||
|
can_read=True,
|
||||||
|
status='active'
|
||||||
|
).values_list('knowledge_base_id', flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 获取用户根据角色可以访问的知识库
|
||||||
|
role_datasets = KnowledgeBase.objects.filter(
|
||||||
|
Q(type='member', department=user.department) | # 成员级知识库
|
||||||
|
Q(type='leader', department=user.department) | # 部门级知识库,组长可访问
|
||||||
|
Q(type='admin') # 管理级知识库,管理员可访问
|
||||||
|
).exclude(
|
||||||
|
Q(type='private') & ~Q(user_id=str(user.id)) # 排除不属于自己的私人知识库
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 合并并去重
|
||||||
|
accessible_datasets = list(set(list(permission_datasets) + list(role_datasets)))
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': [{
|
||||||
|
'id': str(ds.id),
|
||||||
|
'name': ds.name,
|
||||||
|
'type': ds.type,
|
||||||
|
'department': ds.department,
|
||||||
|
'description': ds.desc
|
||||||
|
} for ds in accessible_datasets]
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取可用知识库列表失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取可用知识库列表失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
"""创建聊天记录"""
|
"""创建聊天记录"""
|
||||||
try:
|
try:
|
||||||
@ -192,9 +299,23 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
# 检查知识库ID:支持dataset_id或dataset_id_list格式
|
# 检查知识库ID:支持dataset_id或dataset_id_list格式
|
||||||
dataset_ids = []
|
dataset_ids = []
|
||||||
if 'dataset_id' in data:
|
if 'dataset_id' in data:
|
||||||
dataset_ids.append(data['dataset_id'])
|
dataset_id = data['dataset_id']
|
||||||
elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], list):
|
# 直接使用标准UUID格式
|
||||||
dataset_ids = data['dataset_id_list']
|
dataset_ids.append(str(dataset_id))
|
||||||
|
elif 'dataset_id_list' in data and isinstance(data['dataset_id_list'], (list, str)):
|
||||||
|
# 处理可能的字符串格式
|
||||||
|
if isinstance(data['dataset_id_list'], str):
|
||||||
|
try:
|
||||||
|
# 尝试解析JSON字符串
|
||||||
|
dataset_list = json.loads(data['dataset_id_list'])
|
||||||
|
if isinstance(dataset_list, list):
|
||||||
|
dataset_ids = [str(id) for id in dataset_list]
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# 如果解析失败,可能是单个ID
|
||||||
|
dataset_ids = [str(data['dataset_id_list'])]
|
||||||
|
else:
|
||||||
|
# 如果已经是列表,直接使用标准UUID格式
|
||||||
|
dataset_ids = [str(id) for id in data['dataset_id_list']]
|
||||||
else:
|
else:
|
||||||
return Response({
|
return Response({
|
||||||
'code': 400,
|
'code': 400,
|
||||||
@ -212,9 +333,9 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
# 验证所有知识库并收集external_ids
|
# 验证所有知识库并收集external_ids
|
||||||
external_id_list = []
|
external_id_list = []
|
||||||
user = request.user
|
user = request.user
|
||||||
primary_knowledge_base = None # 主知识库,用于关联聊天记录
|
knowledge_bases = [] # 存储所有知识库对象
|
||||||
|
|
||||||
for idx, kb_id in enumerate(dataset_ids):
|
for kb_id in dataset_ids:
|
||||||
try:
|
try:
|
||||||
knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first()
|
knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first()
|
||||||
if not knowledge_base:
|
if not knowledge_base:
|
||||||
@ -224,9 +345,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_404_NOT_FOUND)
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# 保存第一个知识库作为主知识库
|
knowledge_bases.append(knowledge_base)
|
||||||
if idx == 0:
|
|
||||||
primary_knowledge_base = knowledge_base
|
|
||||||
|
|
||||||
# 检查知识库权限
|
# 检查知识库权限
|
||||||
can_read = False
|
can_read = False
|
||||||
@ -242,7 +361,6 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
if permission:
|
if permission:
|
||||||
can_read = True
|
can_read = True
|
||||||
else:
|
else:
|
||||||
# 使用_can_read方法判断
|
|
||||||
can_read = self._can_read(
|
can_read = self._can_read(
|
||||||
type=knowledge_base.type,
|
type=knowledge_base.type,
|
||||||
user=user,
|
user=user,
|
||||||
@ -279,7 +397,20 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 获取或创建对话ID
|
# 获取或创建对话ID
|
||||||
conversation_id = data.get('conversation_id', str(uuid.uuid4()))
|
conversation_id = data.get('conversation_id')
|
||||||
|
|
||||||
|
# 如果没有提供 conversation_id,根据知识库组合生成新的ID
|
||||||
|
if not conversation_id:
|
||||||
|
# 对知识库ID列表排序以确保相同组合生成相同的hash
|
||||||
|
sorted_kb_ids = sorted(dataset_ids)
|
||||||
|
# 使用知识库ID组合生成唯一的conversation_id
|
||||||
|
conversation_id = str(uuid.uuid5(
|
||||||
|
uuid.NAMESPACE_DNS,
|
||||||
|
'-'.join(sorted_kb_ids)
|
||||||
|
))
|
||||||
|
logger.info(f"为知识库组合 {sorted_kb_ids} 生成新的conversation_id: {conversation_id}")
|
||||||
|
else:
|
||||||
|
logger.info(f"使用现有conversation_id: {conversation_id}")
|
||||||
|
|
||||||
# 调用外部API获取答案 (传递多个knowledge base的external_id)
|
# 调用外部API获取答案 (传递多个knowledge base的external_id)
|
||||||
answer = self._get_answer_from_external_api(
|
answer = self._get_answer_from_external_api(
|
||||||
@ -294,41 +425,44 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
# 创建用户问题记录 (关联到主知识库)
|
# 准备完整的metadata
|
||||||
|
metadata = {
|
||||||
|
'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'),
|
||||||
|
'dataset_id_list': [str(id) for id in dataset_ids],
|
||||||
|
'dataset_external_id_list': [str(id) for id in external_id_list],
|
||||||
|
'dataset_names': [kb.name for kb in knowledge_bases] # 添加知识库名称列表
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建用户问题记录
|
||||||
question_record = ChatHistory.objects.create(
|
question_record = ChatHistory.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
knowledge_base=primary_knowledge_base,
|
knowledge_base=knowledge_bases[0], # 仍然需要一个主知识库,使用第一个
|
||||||
conversation_id=conversation_id,
|
conversation_id=str(conversation_id),
|
||||||
role='user',
|
role='user',
|
||||||
content=data['question'],
|
content=data['question'],
|
||||||
metadata={
|
metadata=metadata
|
||||||
'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'),
|
|
||||||
'dataset_id_list': dataset_ids
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建AI回答记录
|
# 创建AI回答记录
|
||||||
answer_record = ChatHistory.objects.create(
|
answer_record = ChatHistory.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
knowledge_base=primary_knowledge_base,
|
knowledge_base=knowledge_bases[0], # 仍然需要一个主知识库,使用第一个
|
||||||
conversation_id=conversation_id,
|
conversation_id=str(conversation_id),
|
||||||
parent_id=str(question_record.id),
|
parent_id=str(question_record.id),
|
||||||
role='assistant',
|
role='assistant',
|
||||||
content=answer,
|
content=answer,
|
||||||
metadata={
|
metadata=metadata
|
||||||
'model_id': data.get('model_id', '58c5deb4-f2e2-11ef-9a1b-0242ac120009'),
|
|
||||||
'dataset_id_list': dataset_ids
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 返回完整的响应
|
||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'message': '创建成功',
|
'message': '创建成功',
|
||||||
'data': {
|
'data': {
|
||||||
'id': answer_record.id,
|
'id': str(answer_record.id),
|
||||||
'conversation_id': conversation_id,
|
'conversation_id': str(conversation_id),
|
||||||
'dataset_id': str(primary_knowledge_base.id),
|
'dataset_id_list': [str(id) for id in dataset_ids],
|
||||||
'dataset_name': primary_knowledge_base.name,
|
'dataset_names': [kb.name for kb in knowledge_bases], # 返回所有知识库名称
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'content': answer_record.content,
|
'content': answer_record.content,
|
||||||
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
@ -342,7 +476,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
'code': 500,
|
'code': 500,
|
||||||
'message': f'创建聊天记录失败: {str(e)}',
|
'message': f'创建聊天记录失败: {str(e)}',
|
||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def _get_answer_from_external_api(self, dataset_external_id_list, question):
|
def _get_answer_from_external_api(self, dataset_external_id_list, question):
|
||||||
"""调用外部API获取AI回答"""
|
"""调用外部API获取AI回答"""
|
||||||
@ -354,7 +488,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
# 第一个API调用创建聊天
|
# 第一个API调用创建聊天
|
||||||
chat_request_data = {
|
chat_request_data = {
|
||||||
"id": "d333dee2-b3c2-11ef-af2c-a4bb6dafa942",
|
"id": "65031f4d-c86d-430e-8089-d8ff2731a837",
|
||||||
"model_id": "58c5deb4-f2e2-11ef-9a1b-0242ac120009",
|
"model_id": "58c5deb4-f2e2-11ef-9a1b-0242ac120009",
|
||||||
"dataset_id_list": dataset_external_ids,
|
"dataset_id_list": dataset_external_ids,
|
||||||
"multiple_rounds_dialogue": False,
|
"multiple_rounds_dialogue": False,
|
||||||
@ -617,6 +751,210 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
|
|||||||
'message': f'搜索失败: {str(e)}',
|
'message': f'搜索失败: {str(e)}',
|
||||||
'data': None
|
'data': None
|
||||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def export(self, request):
|
||||||
|
"""导出聊天记录为Excel文件"""
|
||||||
|
try:
|
||||||
|
# 获取查询参数
|
||||||
|
conversation_id = request.query_params.get('conversation_id')
|
||||||
|
dataset_id = request.query_params.get('dataset_id')
|
||||||
|
history_days = request.query_params.get('history_days', '7') # 默认导出最近7天
|
||||||
|
|
||||||
|
# 至少需要一个筛选条件
|
||||||
|
if not conversation_id and not dataset_id:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '需要提供conversation_id或dataset_id参数',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 验证权限
|
||||||
|
user = request.user
|
||||||
|
if dataset_id:
|
||||||
|
knowledge_base = KnowledgeBase.objects.filter(id=dataset_id).first()
|
||||||
|
if not knowledge_base:
|
||||||
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': '知识库不存在',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 检查是否有读取权限
|
||||||
|
can_read = False
|
||||||
|
permission = KBPermissionModel.objects.filter(
|
||||||
|
knowledge_base=knowledge_base,
|
||||||
|
user=user,
|
||||||
|
can_read=True,
|
||||||
|
status='active'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if permission:
|
||||||
|
can_read = True
|
||||||
|
else:
|
||||||
|
can_read = self._can_read(
|
||||||
|
type=knowledge_base.type,
|
||||||
|
user=user,
|
||||||
|
department=knowledge_base.department,
|
||||||
|
group=knowledge_base.group,
|
||||||
|
creator_id=knowledge_base.user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not can_read:
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '无权访问该知识库',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 查询确认有聊天记录存在
|
||||||
|
query = self.get_queryset()
|
||||||
|
if conversation_id:
|
||||||
|
records = query.filter(conversation_id=conversation_id)
|
||||||
|
elif dataset_id:
|
||||||
|
records = query.filter(knowledge_base__id=dataset_id)
|
||||||
|
|
||||||
|
if not records.exists():
|
||||||
|
return Response({
|
||||||
|
'code': 404,
|
||||||
|
'message': '未找到相关对话记录',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 调用外部API导出Excel文件 - 使用GET请求
|
||||||
|
application_id = "65031f4d-c86d-430e-8089-d8ff2731a837" # 固定值
|
||||||
|
export_url = f"{settings.API_BASE_URL}/api/application/{application_id}/chat/export?history_day={history_days}"
|
||||||
|
|
||||||
|
logger.info(f"发送导出请求:{export_url}")
|
||||||
|
|
||||||
|
export_response = requests.get(
|
||||||
|
url=export_url,
|
||||||
|
timeout=60,
|
||||||
|
stream=True # 使用流式传输处理大文件
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查响应状态
|
||||||
|
if export_response.status_code != 200:
|
||||||
|
logger.error(f"导出API调用失败: {export_response.status_code}, {export_response.text}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '导出失败,外部服务返回错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 创建响应对象并设置文件下载头
|
||||||
|
response = HttpResponse(
|
||||||
|
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
)
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="data.xlsx"'
|
||||||
|
|
||||||
|
# 将API响应内容写入响应对象
|
||||||
|
for chunk in export_response.iter_content(chunk_size=8192):
|
||||||
|
if chunk:
|
||||||
|
response.write(chunk)
|
||||||
|
|
||||||
|
logger.info("导出成功完成")
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"导出聊天记录失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'导出聊天记录失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def chat_list(self, request):
|
||||||
|
"""获取对话列表"""
|
||||||
|
try:
|
||||||
|
# 获取查询参数
|
||||||
|
history_days = request.query_params.get('history_days', '7') # 默认7天
|
||||||
|
|
||||||
|
# 构建API请求
|
||||||
|
application_id = "65031f4d-c86d-430e-8089-d8ff2731a837"
|
||||||
|
api_url = f"{settings.API_BASE_URL}/api/application/{application_id}/chat"
|
||||||
|
|
||||||
|
# 添加查询参数
|
||||||
|
params = {
|
||||||
|
'history_day': history_days
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"发送获取对话列表请求:{api_url}")
|
||||||
|
|
||||||
|
# 调用外部API
|
||||||
|
response = requests.get(
|
||||||
|
url=api_url,
|
||||||
|
params=params,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"获取对话列表失败: {response.status_code}, {response.text}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '获取对话列表失败,外部服务返回错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 解析响应数据
|
||||||
|
try:
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('code') != 200:
|
||||||
|
logger.error(f"外部API返回错误: {result}")
|
||||||
|
return Response({
|
||||||
|
'code': result.get('code', 500),
|
||||||
|
'message': result.get('message', '获取对话列表失败'),
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 处理返回的数据
|
||||||
|
chat_list = result.get('data', [])
|
||||||
|
|
||||||
|
# 格式化返回数据
|
||||||
|
formatted_chats = []
|
||||||
|
for chat in chat_list:
|
||||||
|
formatted_chat = {
|
||||||
|
'id': chat['id'],
|
||||||
|
'chat_id': chat['chat_id'],
|
||||||
|
'abstract': chat['abstract'],
|
||||||
|
'message_count': chat['chat_record_count'],
|
||||||
|
'created_at': datetime.fromisoformat(chat['create_time'].replace('Z', '+00:00')).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'updated_at': datetime.fromisoformat(chat['update_time'].replace('Z', '+00:00')).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'star_count': chat['star_num'],
|
||||||
|
'trample_count': chat['trample_num'],
|
||||||
|
'mark_sum': chat['mark_sum'],
|
||||||
|
'is_deleted': chat['is_deleted']
|
||||||
|
}
|
||||||
|
formatted_chats.append(formatted_chat)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'total': len(formatted_chats),
|
||||||
|
'results': formatted_chats
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"解析响应数据失败: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '解析响应数据失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取对话列表失败: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'获取对话列表失败: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
def _highlight_keyword(self, text, keyword):
|
def _highlight_keyword(self, text, keyword):
|
||||||
"""高亮关键词"""
|
"""高亮关键词"""
|
||||||
|
Loading…
Reference in New Issue
Block a user