对话能通过status进行搜索

This commit is contained in:
wanjia 2025-06-03 18:25:20 +08:00
parent d13b21e284
commit 0e0620276d
3 changed files with 345 additions and 84 deletions

View File

@ -0,0 +1,29 @@
# Generated by Django 5.2.1 on 2025-06-03 09:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('chat', '0001_initial'),
('expertproducts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='NegotiationChat',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('conversation_id', models.CharField(db_index=True, max_length=100, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('negotiation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chats', to='expertproducts.negotiation')),
],
options={
'ordering': ['-updated_at'],
'indexes': [models.Index(fields=['negotiation', 'updated_at'], name='chat_negoti_negotia_2aa711_idx')],
},
),
]

View File

@ -5,6 +5,7 @@ import uuid
from itertools import count
from apps.user.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.expertproducts.models import Negotiation
class ChatHistory(models.Model):
"""聊天历史记录"""
@ -119,3 +120,19 @@ class ChatHistory(models.Model):
} for kb in self.get_knowledge_bases()
]
}
class NegotiationChat(models.Model):
"""谈判对话关联表"""
negotiation = models.ForeignKey(Negotiation, on_delete=models.CASCADE, related_name='chats')
conversation_id = models.CharField(max_length=100, unique=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-updated_at']
indexes = [
models.Index(fields=['negotiation', 'updated_at']),
]
def __str__(self):
return f"谈判 {self.negotiation.id} - 对话 {self.conversation_id}"

View File

@ -11,9 +11,10 @@ from rest_framework.response import Response
from rest_framework.decorators import action
from apps.user.models import User
from apps.knowledge_base.models import KnowledgeBase
from apps.chat.models import ChatHistory
from apps.chat.models import ChatHistory, NegotiationChat
from apps.chat.serializers import ChatHistorySerializer
from apps.common.services.chat_service import ChatService
from apps.expertproducts.models import Negotiation
from apps.chat.services.chat_api import (
ExternalAPIError, stream_chat_answer, get_chat_answer, generate_conversation_title,
get_hit_test_documents, generate_conversation_title_from_deepseek
@ -30,17 +31,33 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
serializer_class = ChatHistorySerializer
queryset = ChatHistory.objects.all()
def check_knowledge_base_permission(self, kb, user, permission_type):
"""
兼容方法始终返回True表示有权限
此方法替代了原来的权限检查现在我们不再检查知识库权限
"""
return True
def get_queryset(self):
"""确保用户只能看到自己的未删除的聊天记录以及有权限的知识库关联的聊天记录"""
"""确保用户只能看到自己的未删除的聊天记录"""
user = self.request.user
accessible_kb_ids = [
kb.id for kb in KnowledgeBase.objects.all()
if self.check_knowledge_base_permission(kb, user, 'read')
]
return ChatHistory.objects.filter(
Q(user=user) | Q(knowledge_base_id__in=accessible_kb_ids),
queryset = ChatHistory.objects.filter(
user=user,
is_deleted=False
)
# 获取status筛选参数
status_filter = self.request.query_params.get('status')
if status_filter:
# 查找与谈判关联的对话ID
negotiation_chats = NegotiationChat.objects.filter(
negotiation__status=status_filter
).values_list('conversation_id', flat=True)
# 筛选这些对话ID对应的聊天记录
queryset = queryset.filter(conversation_id__in=negotiation_chats)
return queryset
def list(self, request):
"""获取对话列表概览"""
@ -113,7 +130,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
def conversation_detail(self, request):
"""获取特定对话的详细信息"""
"""获取特定对话的详细信息支持按status筛选并返回creator和product信息"""
try:
conversation_id = request.query_params.get('conversation_id')
if not conversation_id:
@ -123,9 +140,73 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
chat_service = ChatService()
result = chat_service.get_conversation_detail(request.user, conversation_id)
# 获取状态筛选参数
status_filter = request.query_params.get('status')
# 查找对话记录
chat_records = ChatHistory.objects.filter(
conversation_id=conversation_id,
is_deleted=False
).order_by('created_at')
if not chat_records.exists():
return Response({
'code': 404,
'message': '对话不存在或已被删除',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 尝试查找关联的谈判记录
negotiation_chat = NegotiationChat.objects.filter(
conversation_id=conversation_id
).first()
# 准备基本的返回数据
result = {
'conversation_id': conversation_id,
'messages': []
}
# 如果存在谈判关联,添加谈判相关信息
if negotiation_chat:
negotiation = negotiation_chat.negotiation
# 如果有status筛选匹配谈判状态
if status_filter and negotiation.status != status_filter:
return Response({
'code': 404,
'message': f'没有找到状态为 {status_filter} 的对话',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 添加谈判相关信息
result['negotiation'] = {
'id': str(negotiation.id),
'status': negotiation.status,
'current_round': negotiation.current_round,
'context': negotiation.context,
'creator': {
'id': str(negotiation.creator.id),
'name': negotiation.creator.name if hasattr(negotiation.creator, 'name') else "未知",
'profile': negotiation.creator.profile if hasattr(negotiation.creator, 'profile') else {}
},
'product': {
'id': str(negotiation.product.id),
'name': negotiation.product.name if hasattr(negotiation.product, 'name') else "未知",
'description': negotiation.product.description if hasattr(negotiation.product, 'description') else ""
}
}
# 整理对话消息
for record in chat_records:
result['messages'].append({
'id': str(record.id),
'role': record.role,
'content': record.content,
'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'metadata': record.metadata
})
return Response({
'code': 200,
'message': '获取成功',
@ -153,10 +234,8 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
"""获取用户可访问的知识库列表"""
try:
user = request.user
accessible_datasets = [
dataset for dataset in KnowledgeBase.objects.all()
if self.check_knowledge_base_permission(dataset, user, 'read')
]
# 直接返回所有知识库,不做权限检查
accessible_datasets = KnowledgeBase.objects.all()
return Response({
'code': 200,
@ -183,83 +262,49 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['post'])
def create_conversation(self, request):
"""创建会话 - 先选择知识库创建会话ID不发送问题"""
"""创建会话 - 关联到谈判ID"""
try:
data = request.data
# 检查知识库ID支持dataset_id或dataset_id_list格式
dataset_ids = []
if 'dataset_id' in data:
dataset_id = data['dataset_id']
# 直接使用标准UUID格式
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:
# 检查谈判ID
negotiation_id = data.get('negotiation_id')
if not negotiation_id:
return Response({
'code': 400,
'message': '缺少必填字段: dataset_id 或 dataset_id_list',
'message': '缺少必填字段: negotiation_id',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not dataset_ids:
# 验证谈判存在性
try:
negotiation = Negotiation.objects.get(id=negotiation_id)
except Negotiation.DoesNotExist:
return Response({
'code': 400,
'message': '至少需要提供一个知识库ID',
'code': 404,
'message': f'谈判记录不存在: {negotiation_id}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
}, status=status.HTTP_404_NOT_FOUND)
# 验证所有知识库
user = request.user
knowledge_bases = [] # 存储所有知识库对象
for kb_id in dataset_ids:
try:
knowledge_base = KnowledgeBase.objects.filter(id=kb_id).first()
if not knowledge_base:
return Response({
'code': 404,
'message': f'知识库不存在: {kb_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
knowledge_bases.append(knowledge_base)
# 使用统一的权限检查方法
if not self.check_knowledge_base_permission(knowledge_base, user, 'read'):
return Response({
'code': 403,
'message': f'无权访问知识库: {knowledge_base.name}',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except Exception as e:
return Response({
'code': 400,
'message': f'处理知识库ID出错: {str(e)}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 获取达人邮箱
creator = negotiation.creator
creator_email = creator.email if hasattr(creator, 'email') else None
# 创建一个新的会话ID
conversation_id = str(uuid.uuid4())
logger.info(f"创建新的会话ID: {conversation_id}")
# 准备metadata (仍然保存知识库名称用于内部处理)
# 创建谈判关联
NegotiationChat.objects.create(
negotiation=negotiation,
conversation_id=conversation_id
)
# 准备metadata
metadata = {
'dataset_id_list': [str(id) for id in dataset_ids],
'dataset_names': [kb.name for kb in knowledge_bases]
'negotiation_id': str(negotiation_id),
'status': negotiation.status,
'creator_id': str(creator.id),
'creator_email': creator_email
}
return Response({
@ -267,7 +312,13 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'message': '会话创建成功',
'data': {
'conversation_id': conversation_id,
'dataset_id_list': metadata['dataset_id_list']
'negotiation_id': str(negotiation_id),
'status': negotiation.status,
'creator': {
'id': str(creator.id),
'name': creator.name if hasattr(creator, 'name') else "未知",
'email': creator_email
}
}
})
@ -279,7 +330,7 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'message': f'创建会话失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def create(self, request):
"""创建聊天记录"""
try:
@ -630,6 +681,9 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
end_date = request.query_params.get('end_date')
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
# 获取status筛选参数
status_filter = request.query_params.get('status')
query = self.get_queryset()
if keyword:
@ -638,14 +692,19 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
Q(knowledge_base__name__icontains=keyword)
)
if dataset_id:
knowledge_base = KnowledgeBase.objects.filter(id=dataset_id).first()
if knowledge_base and not self.check_knowledge_base_permission(knowledge_base, request.user, 'read'):
return Response({
'code': 403,
'message': '无权访问该知识库',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 移除权限检查直接筛选知识库ID
query = query.filter(knowledge_base__id=dataset_id)
# 添加status筛选条件
if status_filter:
# 查找与谈判关联的对话ID
negotiation_chats = NegotiationChat.objects.filter(
negotiation__status=status_filter
).values_list('conversation_id', flat=True)
# 筛选这些对话ID对应的聊天记录
query = query.filter(conversation_id__in=negotiation_chats)
if start_date:
query = query.filter(created_at__gte=start_date)
if end_date:
@ -793,3 +852,159 @@ class ChatHistoryViewSet(viewsets.ModelViewSet):
'message': f"更新会话标题失败: {str(e)}",
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'])
def send_email_to_creator(self, request):
"""通过对话ID发送邮件给达人"""
try:
data = request.data
conversation_id = data.get('conversation_id')
if not conversation_id:
return Response({
'code': 400,
'message': '缺少必填字段: conversation_id',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证邮件参数
subject = data.get('subject', '') # 设置默认主题
body = data.get('body')
from_email = data.get('from_email')
if not all([body, from_email]):
return Response({
'code': 400,
'message': '缺少必要的邮件参数请提供body和from_email字段',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 查找关联的谈判信息
negotiation_chat = NegotiationChat.objects.filter(
conversation_id=conversation_id
).first()
if not negotiation_chat:
return Response({
'code': 404,
'message': f'找不到与对话ID关联的谈判: {conversation_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 获取谈判中的达人信息
negotiation = negotiation_chat.negotiation
creator_id = negotiation.creator.id
# 直接从CreatorProfile模型中获取达人邮箱
from apps.daren_detail.models import CreatorProfile
creator = CreatorProfile.objects.filter(id=creator_id).first()
if not creator:
return Response({
'code': 404,
'message': f'找不到达人信息: {creator_id}',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
creator_email = creator.email
if not creator_email:
return Response({
'code': 404,
'message': '达人没有关联的邮箱地址',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 处理附件
attachments = []
for file_key, file_obj in request.FILES.items():
if file_obj:
# 保存临时文件
import os
from django.conf import settings
tmp_path = os.path.join(settings.MEDIA_ROOT, 'tmp', f'{file_key}_{file_obj.name}')
os.makedirs(os.path.dirname(tmp_path), exist_ok=True)
with open(tmp_path, 'wb+') as destination:
for chunk in file_obj.chunks():
destination.write(chunk)
attachments.append({
'path': tmp_path,
'filename': file_obj.name
})
# 直接调用Gmail服务发送邮件而不是通过视图
from apps.gmail.services.gmail_service import GmailService
success, result = GmailService.send_email(
request.user,
from_email,
creator_email,
subject or f'{creator.name}的谈判',
body or '',
attachments
)
if success:
# 发送成功后,将邮件记录添加到对话历史
# 查询一个有效的知识库ID
default_kb = KnowledgeBase.objects.first()
if not default_kb:
logger.warning("未找到有效的知识库,无法记录邮件发送历史")
# 返回成功但提示无法记录历史
return Response({
'code': 200,
'message': '邮件发送成功,但未能记录到对话历史',
'data': {
'conversation_id': conversation_id,
'creator_email': creator_email,
'creator_name': creator.name,
'message_id': result
}
})
ChatHistory.objects.create(
user=request.user,
knowledge_base=default_kb, # 使用查询到的默认知识库
conversation_id=conversation_id,
role='system',
content=f"发送邮件给 {creator.name}: {subject}\n\n{body}",
metadata={
'email_sent': True,
'to': creator_email,
'from': from_email,
'subject': subject,
'negotiation_id': str(negotiation.id),
'status': negotiation.status,
'has_attachments': len(attachments) > 0
}
)
return Response({
'code': 200,
'message': '邮件发送成功',
'data': {
'conversation_id': conversation_id,
'creator_email': creator_email,
'creator_name': creator.name,
'message_id': result
}
})
else:
# 返回发送失败信息
return Response({
'code': 500,
'message': f'邮件发送失败: {result}',
'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)