处理流式输出

This commit is contained in:
wanjia 2025-05-23 19:25:35 +08:00
parent cc2c647044
commit 113f13d501
32 changed files with 2580 additions and 34 deletions

0
apps/chat/__init__.py Normal file
View File

3
apps/chat/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/chat/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.chat'

419
apps/chat/chat.md Normal file
View File

@ -0,0 +1,419 @@
# Chat 模块接口文档
## 概述
Chat 模块提供了一组 API用于创建和管理对话。这包括创建新对话、发送消息、接收回答、查询历史记录等功能。所有接口都基于 REST 风格设计。
## 认证
除特殊说明外,所有接口都需要认证。请在请求头中添加认证信息:
```
Authorization: Bearer <jwt_token>
```
```
Authorization: Token <token>
```
## REST API接口列表
### 1. 创建会话
创建一个新的对话会话。
- **URL**: `/api/chat-history/create_conversation/`
- **方法**: `POST`
- **认证**: 不需要认证
- **响应**:
```json
{
"code": 200,
"message": "会话创建成功",
"data": {
"conversation_id": "uuid-string"
}
}
```
### 2. 发送消息并接收回答
发送用户问题并获取 AI 回答。
- **URL**: `/api/chat-history/`
- **方法**: `POST`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| conversation_id | string | 是 | 会话 ID |
| question | string | 是 | 用户问题 |
| title | string | 否 | 会话标题,默认为 "New chat" |
| stream | boolean | 否 | 是否使用流式响应,默认为 true |
- **响应** (非流式):
```json
{
"code": 200,
"message": "成功",
"data": {
"id": "记录ID",
"conversation_id": "会话ID",
"title": "会话标题",
"role": "assistant",
"content": "AI回答内容",
"created_at": "创建时间"
}
}
```
- **响应** (流式):
返回 `text/event-stream` 类型的响应,包含多个 data 事件:
```
data: {"code": 200, "message": "开始流式传输", "data": {"id": "记录ID", "conversation_id": "会话ID", "content": "", "is_end": false}}
data: {"code": 200, "message": "partial", "data": {"id": "记录ID", "conversation_id": "会话ID", "title": "标题", "content": "部分内容", "is_end": false}}
...
data: {"code": 200, "message": "完成", "data": {"id": "记录ID", "conversation_id": "会话ID", "title": "标题", "role": "assistant", "content": "完整内容", "created_at": "时间", "is_end": true}}
```
### 3. 获取对话列表
获取用户的所有对话列表。
- **URL**: `/api/chat-history/`
- **方法**: `GET`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| page | integer | 否 | 页码,默认为 1 |
| page_size | integer | 否 | 每页条数,默认为 10 |
- **响应**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"total": 总记录数,
"page": 当前页码,
"page_size": 每页条数,
"results": [
{
"conversation_id": "会话ID",
"message_count": 消息数量,
"last_message": "最后一条消息内容",
"last_time": "最后更新时间"
}
]
}
}
```
### 4. 获取对话详情
获取特定对话的所有消息。
- **URL**: `/api/chat-history/conversation_detail/`
- **方法**: `GET`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| conversation_id | string | 是 | 会话 ID |
- **响应**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"conversation_id": "会话ID",
"title": "会话标题",
"created_at": "创建时间",
"messages": [
{
"id": "消息ID",
"role": "用户角色(user/assistant)",
"content": "消息内容",
"created_at": "创建时间"
}
]
}
}
```
### 5. 搜索聊天记录
根据关键词搜索聊天记录。
- **URL**: `/api/chat-history/search/`
- **方法**: `GET`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| keyword | string | 否 | 搜索关键词 |
| start_date | string | 否 | 开始日期,格式为 YYYY-MM-DD |
| end_date | string | 否 | 结束日期,格式为 YYYY-MM-DD |
| page | integer | 否 | 页码,默认为 1 |
| page_size | integer | 否 | 每页条数,默认为 10 |
- **响应**:
```json
{
"code": 200,
"message": "搜索成功",
"data": {
"total": 总记录数,
"page": 当前页码,
"page_size": 每页条数,
"results": [
{
"id": "记录ID",
"conversation_id": "会话ID",
"role": "角色",
"content": "内容",
"created_at": "创建时间",
"metadata": {},
"highlights": {
"content": "高亮后的内容"
}
}
]
}
}
```
### 6. 删除会话
删除整个会话及其所有消息。
- **URL**: `/api/chat-history/delete_conversation/`
- **方法**: `DELETE`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| conversation_id | string | 是 | 要删除的会话 ID |
- **响应**:
```json
{
"code": 200,
"message": "删除成功",
"data": {
"conversation_id": "会话ID",
"deleted_count": 删除的消息数量
}
}
```
### 7. 更新消息内容
更新单条消息的内容。
- **URL**: `/api/chat-history/{id}/`
- **方法**: `PUT``PATCH`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| content | string | 否 | 更新的消息内容 |
| metadata | object | 否 | 更新的元数据 |
- **响应**:
```json
{
"code": 200,
"message": "更新成功",
"data": {
"id": "记录ID",
"conversation_id": "会话ID",
"role": "角色",
"content": "更新后的内容",
"metadata": {},
"updated_at": "更新时间"
}
}
```
### 8. 删除单条消息
删除单条消息(软删除)。
- **URL**: `/api/chat-history/{id}/`
- **方法**: `DELETE`
- **响应**:
```json
{
"code": 200,
"message": "删除成功",
"data": null
}
```
### 9. 更新会话标题
设置或更新会话的标题。
- **URL**: `/api/chat-history/generate-conversation-title/`
- **方法**: `GET`
- **参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|------|------|------|
| conversation_id | string | 是 | 会话 ID |
| title | string | 是 | 新标题 |
- **响应**:
```json
{
"code": 200,
"message": "更新会话标题成功",
"data": {
"conversation_id": "会话ID",
"title": "新标题"
}
}
```
### 10. 获取可用知识库
获取系统中可用的知识库列表。
- **URL**: `/api/chat-history/available_datasets/`
- **方法**: `GET`
- **响应**:
```json
{
"code": 200,
"message": "获取成功",
"data": []
}
```
## WebSocket API
除了REST API之外Chat模块还提供了WebSocket接口用于实时流式获取对话回答。
### 1. 建立WebSocket连接
- **URL**: `ws://yourdomain:port/ws/chat/stream/?token=<your_token>`
- **认证**: 需要在URL参数中提供token
- **说明**: token是用户的认证token即登录接口获取的token通过UserToken模型验证
- **获取token**: 通过API登录接口获取token
```
POST /api/user/login/
{
"email": "用户邮箱",
"password": "用户密码"
}
```
成功登录后响应将返回token字段值
### 2. 发送消息
连接建立后可以向WebSocket发送JSON格式的消息
```json
{
"conversation_id": "会话ID",
"question": "用户问题"
}
```
### 3. 接收响应
WebSocket会返回以下JSON格式的消息
1. 开始流式传输:
```json
{
"code": 200,
"message": "开始流式传输",
"data": {
"id": "记录ID",
"conversation_id": "会话ID",
"content": "",
"is_end": false
}
}
```
2. 部分内容:
```json
{
"code": 200,
"message": "partial",
"data": {
"id": "记录ID",
"conversation_id": "会话ID",
"content": "部分内容",
"is_end": false
}
}
```
3. 完成响应:
```json
{
"code": 200,
"message": "完成",
"data": {
"id": "记录ID",
"conversation_id": "会话ID",
"title": "会话标题",
"role": "assistant",
"content": "完整内容",
"created_at": "时间",
"is_end": true
}
}
```
4. 错误响应:
```json
{
"code": 500,
"message": "错误信息",
"data": {
"is_end": true
}
}
```
## 错误响应
所有接口在出错时都会返回类似的错误响应结构:
```json
{
"code": 错误代码,
"message": "错误描述",
"data": null
}
```
常见错误代码:
- 400: 请求参数错误
- 401: 未认证或认证失败
- 404: 资源不存在
- 500: 服务器内部错误

384
apps/chat/consumers.py Normal file
View File

@ -0,0 +1,384 @@
# apps/chat/consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
from channels.db import database_sync_to_async
from apps.chat.models import ChatHistory
from apps.user.models import UserToken
from django.conf import settings
import logging
import traceback
import uuid
import aiohttp
from urllib.parse import parse_qs
from django.utils import timezone
logger = logging.getLogger(__name__)
class ChatStreamConsumer(AsyncWebsocketConsumer):
# 固定知识库ID
DEFAULT_KNOWLEDGE_BASE_ID = "b680a4fa-37be-11f0-a7cb-0242ac120002"
async def connect(self):
"""建立WebSocket连接"""
try:
# 从URL参数中获取token
query_string = self.scope.get('query_string', b'').decode()
query_params = parse_qs(query_string)
token_key = query_params.get('token', [''])[0]
if not token_key:
logger.warning("WebSocket连接尝试但没有提供token")
await self.close()
return
# 验证token
self.user = await self.get_user_from_token(token_key)
if not self.user:
logger.warning(f"WebSocket连接尝试但token无效: {token_key}")
await self.close()
return
# 将用户信息存储在scope中
self.scope["user"] = self.user
await self.accept()
logger.info(f"用户 {self.user.email} 流式输出WebSocket连接成功")
except Exception as e:
logger.error(f"WebSocket连接错误: {str(e)}")
await self.close()
@database_sync_to_async
def get_user_from_token(self, token_key):
try:
# 使用项目的UserToken模型而不是rest_framework的Token
token = UserToken.objects.select_related('user').get(
token=token_key,
expired_at__gt=timezone.now() # 确保token未过期
)
return token.user
except UserToken.DoesNotExist:
return None
async def disconnect(self, close_code):
"""关闭WebSocket连接"""
logger.info(f"用户 {self.user.email if hasattr(self, 'user') else 'unknown'} WebSocket连接断开代码: {close_code}")
async def receive(self, text_data):
"""接收消息并处理"""
try:
data = json.loads(text_data)
# 检查必填字段
if 'question' not in data:
await self.send_error("缺少必填字段: question")
return
if 'conversation_id' not in data:
await self.send_error("缺少必填字段: conversation_id")
return
# 处理新会话或现有会话
await self.process_chat_request(data)
except Exception as e:
logger.error(f"处理消息时出错: {str(e)}")
logger.error(traceback.format_exc())
await self.send_error(f"处理消息时出错: {str(e)}")
async def process_chat_request(self, data):
"""处理聊天请求"""
try:
conversation_id = data['conversation_id']
question = data['question']
# 准备metadata
metadata = {}
# 创建问题记录
question_record = await self.create_question_record(
conversation_id,
question,
metadata
)
if not question_record:
return
# 创建AI回答记录
answer_record = await self.create_answer_record(
conversation_id,
question_record,
metadata
)
# 发送初始响应
await self.send_json({
'code': 200,
'message': '开始流式传输',
'data': {
'id': str(answer_record.id),
'conversation_id': str(conversation_id),
'content': '',
'is_end': False
}
})
# 设置外部API需要的ID列表 - 简化为空列表
dataset_external_id_list = []
# 调用外部API获取流式响应
await self.stream_from_external_api(
conversation_id,
question,
dataset_external_id_list,
answer_record,
metadata
)
except Exception as e:
logger.error(f"处理聊天请求时出错: {str(e)}")
logger.error(traceback.format_exc())
await self.send_error(f"处理聊天请求时出错: {str(e)}")
@database_sync_to_async
def create_question_record(self, conversation_id, question, metadata):
"""创建问题记录"""
try:
title = "New chat"
# 创建用户问题记录
return ChatHistory.objects.create(
user=self.scope["user"],
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
conversation_id=str(conversation_id),
title=title,
role='user',
content=question,
metadata=metadata
)
except Exception as e:
logger.error(f"创建问题记录时出错: {str(e)}")
return None
@database_sync_to_async
def create_answer_record(self, conversation_id, question_record, metadata):
"""创建AI回答记录"""
try:
return ChatHistory.objects.create(
user=self.scope["user"],
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
conversation_id=str(conversation_id),
title=question_record.title,
parent_id=str(question_record.id),
role='assistant',
content="", # 初始内容为空
metadata=metadata
)
except Exception as e:
logger.error(f"创建回答记录时出错: {str(e)}")
return None
async def stream_from_external_api(self, conversation_id, question, dataset_external_id_list, answer_record, metadata):
"""从外部API获取流式响应"""
try:
# 获取标题
title = answer_record.title or 'New chat'
# 异步收集完整内容,用于最后保存
full_content = ""
# 使用aiohttp进行异步HTTP请求
async with aiohttp.ClientSession() as session:
# 第一步: 创建聊天会话
async with session.post(
f"{settings.API_BASE_URL}/api/application/chat/open",
json={
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
"model_id": metadata.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'),
"dataset_id_list": dataset_external_id_list,
"multiple_rounds_dialogue": False,
"dataset_setting": {
"top_n": 10, "similarity": "0.3",
"max_paragraph_char_number": 10000,
"search_mode": "blend",
"no_references_setting": {
"value": "{question}",
"status": "ai_questioning"
}
},
"model_setting": {
"prompt": "**相关文档内容**{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**{question}"
},
"problem_optimization": False
}
) as chat_response:
if chat_response.status != 200:
error_msg = f"外部API调用失败: {await chat_response.text()}"
logger.error(error_msg)
await self.send_error(error_msg)
return
chat_data = await chat_response.json()
if chat_data.get('code') != 200 or not chat_data.get('data'):
error_msg = f"外部API返回错误: {chat_data}"
logger.error(error_msg)
await self.send_error(error_msg)
return
chat_id = chat_data['data']
logger.info(f"成功创建聊天会话, chat_id: {chat_id}")
# 第二步: 建立流式连接
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
logger.info(f"开始流式请求: {message_url}")
# 创建流式请求
async with session.post(
url=message_url,
json={"message": question, "re_chat": False, "stream": True},
headers={"Content-Type": "application/json"}
) as message_request:
if message_request.status != 200:
error_msg = f"外部API聊天消息调用失败: {message_request.status}, {await message_request.text()}"
logger.error(error_msg)
await self.send_error(error_msg)
return
# 创建一个缓冲区以处理分段的数据
buffer = ""
# 读取并处理每个响应块
logger.info("开始处理流式响应")
async for chunk in message_request.content.iter_any():
chunk_str = chunk.decode('utf-8')
buffer += chunk_str
# 检查是否有完整的数据行
while '\n\n' in buffer:
parts = buffer.split('\n\n', 1)
line = parts[0]
buffer = parts[1]
if line.startswith('data: '):
try:
# 提取JSON数据
json_str = line[6:] # 去掉 "data: " 前缀
data = json.loads(json_str)
# 记录并处理部分响应
if 'content' in data:
content_part = data['content']
full_content += content_part
# 发送部分内容
await self.send_json({
'code': 200,
'message': 'partial',
'data': {
'id': str(answer_record.id),
'conversation_id': str(conversation_id),
'content': content_part,
'is_end': data.get('is_end', False)
}
})
# 处理结束标记
if data.get('is_end', False):
logger.info("收到流式响应结束标记")
# 保存完整内容
await self.update_answer_content(answer_record.id, full_content.strip())
# 处理标题
title = await self.get_or_generate_title(
conversation_id,
question,
full_content.strip()
)
# 发送最终响应
await self.send_json({
'code': 200,
'message': '完成',
'data': {
'id': str(answer_record.id),
'conversation_id': str(conversation_id),
'title': title,
'role': 'assistant',
'content': full_content.strip(),
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'is_end': True
}
})
return
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}, 数据: {line}")
continue
except Exception as e:
logger.error(f"流式处理出错: {str(e)}")
logger.error(traceback.format_exc())
await self.send_error(str(e))
# 保存已收集的内容
if 'full_content' in locals() and full_content:
try:
await self.update_answer_content(answer_record.id, full_content.strip())
except Exception as save_error:
logger.error(f"保存部分内容失败: {str(save_error)}")
@database_sync_to_async
def update_answer_content(self, answer_id, content):
"""更新回答内容"""
try:
answer_record = ChatHistory.objects.get(id=answer_id)
answer_record.content = content
answer_record.save()
return True
except Exception as e:
logger.error(f"更新回答内容失败: {str(e)}")
return False
@database_sync_to_async
def get_or_generate_title(self, conversation_id, question, answer):
"""获取或生成对话标题"""
try:
# 先检查是否已有标题
current_title = ChatHistory.objects.filter(
conversation_id=str(conversation_id)
).exclude(
title__in=["New chat", "新对话", ""]
).values_list('title', flat=True).first()
if current_title:
return current_title
# 简单的标题生成逻辑 (可替换为调用DeepSeek API生成标题)
generated_title = question[:20] + "..." if len(question) > 20 else question
# 更新所有相关记录的标题
ChatHistory.objects.filter(
conversation_id=str(conversation_id)
).update(title=generated_title)
return generated_title
except Exception as e:
logger.error(f"获取或生成标题失败: {str(e)}")
return "新对话"
async def send_json(self, content):
"""发送JSON格式的消息"""
await self.send(text_data=json.dumps(content))
async def send_error(self, message):
"""发送错误消息"""
await self.send_json({
'code': 500,
'message': message,
'data': {'is_end': True}
})

View File

@ -0,0 +1,38 @@
# Generated by Django 5.2.1 on 2025-05-23 10:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChatHistory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('knowledge_base_id', models.CharField(default='b680a4fa-37be-11f0-a7cb-0242ac120002', help_text='知识库ID用于兼容原有结构', max_length=100)),
('conversation_id', models.CharField(db_index=True, max_length=100)),
('title', models.CharField(blank=True, default='New chat', help_text='对话标题', max_length=100, null=True)),
('parent_id', models.CharField(blank=True, max_length=100, null=True)),
('role', models.CharField(choices=[('user', '用户'), ('assistant', 'AI助手'), ('system', '系统')], max_length=20)),
('content', models.TextField()),
('tokens', models.IntegerField(default=0, help_text='消息token数')),
('metadata', models.JSONField(blank=True, default=dict, help_text='存储额外信息')),
('created_at', models.DateTimeField(auto_now_add=True)),
('is_deleted', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created_at'],
'indexes': [models.Index(fields=['conversation_id', 'created_at'], name='chat_chathi_convers_90ca0c_idx'), models.Index(fields=['user', 'created_at'], name='chat_chathi_user_id_326d36_idx'), models.Index(fields=['conversation_id', 'is_deleted'], name='chat_chathi_convers_bcd094_idx')],
},
),
]

View File

73
apps/chat/models.py Normal file
View File

@ -0,0 +1,73 @@
# apps/chat/models.py
from django.db import models
from django.utils import timezone
import uuid
from itertools import count
from apps.user.models import User
class ChatHistory(models.Model):
"""聊天历史记录"""
ROLE_CHOICES = [
('user', '用户'),
('assistant', 'AI助手'),
('system', '系统')
]
user = models.ForeignKey(User, on_delete=models.CASCADE)
# 改为使用字符串字段而非外键关联
knowledge_base_id = models.CharField(max_length=100, default="b680a4fa-37be-11f0-a7cb-0242ac120002", help_text="知识库ID用于兼容原有结构")
# 用于标识知识库组合的对话
conversation_id = models.CharField(max_length=100, db_index=True)
# 对话标题
title = models.CharField(max_length=100, null=True, blank=True, default='New chat', help_text="对话标题")
parent_id = models.CharField(max_length=100, null=True, blank=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
content = models.TextField()
tokens = models.IntegerField(default=0, help_text="消息token数")
# 扩展metadata字段
metadata = models.JSONField(default=dict, blank=True, help_text="存储额外信息")
created_at = models.DateTimeField(auto_now_add=True)
is_deleted = models.BooleanField(default=False)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['conversation_id', 'created_at']),
models.Index(fields=['user', 'created_at']),
models.Index(fields=['conversation_id', 'is_deleted']),
]
def __str__(self):
return f"{self.user.email} - {self.title} - {self.created_at}"
@classmethod
def get_conversation(cls, conversation_id):
"""获取完整对话历史"""
return cls.objects.filter(
conversation_id=conversation_id,
is_deleted=False
).order_by('created_at')
@classmethod
def get_conversations_by_user(cls, user):
"""根据用户获取对话历史"""
return cls.objects.filter(
user=user,
is_deleted=False
).order_by('created_at')
def soft_delete(self):
"""软删除消息"""
self.is_deleted = True
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
}

7
apps/chat/routing.py Normal file
View File

@ -0,0 +1,7 @@
# apps/chat/routing.py
from django.urls import re_path
from apps.chat.consumers import ChatStreamConsumer
websocket_urlpatterns = [
re_path(r'ws/chat/stream/$', ChatStreamConsumer.as_asgi()),
]

18
apps/chat/serializers.py Normal file
View File

@ -0,0 +1,18 @@
# apps/chat/serializers.py
from rest_framework import serializers
from apps.chat.models import ChatHistory
class ChatHistorySerializer(serializers.ModelSerializer):
knowledge_base_id = serializers.UUIDField(source='knowledge_base.id', read_only=True)
dataset_name = serializers.CharField(source='knowledge_base.name', read_only=True)
user_id = serializers.UUIDField(source='user.id', read_only=True)
class Meta:
model = ChatHistory
fields = [
'id', 'user_id', 'knowledge_base_id', 'dataset_name', 'conversation_id',
'title', 'role', 'content', 'parent_id', 'metadata', 'created_at', 'is_deleted'
]
read_only_fields = ['id', 'user_id', 'knowledge_base_id', 'dataset_name', 'created_at', 'is_deleted']

View File

View File

@ -0,0 +1,258 @@
import requests
import json
import logging
from django.conf import settings
from rest_framework.exceptions import APIException
from daren import settings
logger = logging.getLogger(__name__)
class ExternalAPIError(APIException):
status_code = 500
default_detail = '外部API调用失败'
default_code = 'external_api_error'
def stream_chat_answer(conversation_id, question, dataset_external_ids, metadata):
"""流式调用外部聊天API返回生成器以实时传输回答"""
try:
# 构造聊天请求数据
chat_request_data = {
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
"model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006",
"dataset_id_list": [str(id) for id in dataset_external_ids],
"multiple_rounds_dialogue": False,
"dataset_setting": {
"top_n": 10, "similarity": "0.3",
"max_paragraph_char_number": 10000,
"search_mode": "blend",
"no_references_setting": {
"value": "{question}",
"status": "ai_questioning"
}
},
"model_setting": {
"prompt": "**相关文档内容**{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**{question}"
},
"problem_optimization": False
}
# 发起聊天请求
logger.info(f"调用流式聊天API: {settings.API_BASE_URL}/api/application/chat/open")
chat_response = requests.post(
url=f"{settings.API_BASE_URL}/api/application/chat/open",
json=chat_request_data,
headers={"Content-Type": "application/json"}
)
logger.info(f"聊天API响应状态码: {chat_response.status_code}")
if chat_response.status_code != 200:
error_msg = f"聊天API调用失败: {chat_response.text}"
logger.error(error_msg)
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
return
chat_data = chat_response.json()
if chat_data.get('code') != 200 or not chat_data.get('data'):
error_msg = f"聊天API返回错误: {chat_data}"
logger.error(error_msg)
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
return
chat_id = chat_data['data']
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
logger.info(f"调用消息API: {message_url}")
# 发起消息请求(流式)
message_request = requests.post(
url=message_url,
json={"message": question, "re_chat": False, "stream": True},
headers={"Content-Type": "application/json"},
stream=True
)
if message_request.status_code != 200:
error_msg = f"消息API调用失败: {message_request.status_code}, {message_request.text}"
logger.error(error_msg)
yield f"data: {json.dumps({'code': 500, 'message': error_msg, 'data': {'is_end': True}})}\n\n"
return
buffer = ""
for chunk in message_request.iter_content(chunk_size=1):
if not chunk:
continue
chunk_str = chunk.decode('utf-8')
buffer += chunk_str
if '\n\n' in buffer:
lines = buffer.split('\n\n')
for line in lines[:-1]:
if line.startswith('data: '):
try:
json_str = line[6:]
data = json.loads(json_str)
if 'content' in data:
content_part = data['content']
response_data = {
'code': 200,
'message': 'partial',
'data': {
'content': content_part,
'is_end': data.get('is_end', False)
}
}
yield f"data: {json.dumps(response_data)}\n\n"
if data.get('is_end', False):
return
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}, 数据: {line}")
buffer = lines[-1]
if buffer and buffer.startswith('data: '):
try:
json_str = buffer[6:]
data = json.loads(json_str)
if 'content' in data:
content_part = data['content']
response_data = {
'code': 200,
'message': 'partial',
'data': {
'content': content_part,
'is_end': data.get('is_end', False)
}
}
yield f"data: {json.dumps(response_data)}\n\n"
except json.JSONDecodeError:
logger.error(f"处理剩余数据时JSON解析错误: {buffer}")
except Exception as e:
logger.error(f"流式聊天API处理出错: {str(e)}")
yield f"data: {json.dumps({'code': 500, 'message': f'流式处理出错: {str(e)}', 'data': {'is_end': True}})}\n\n"
def get_chat_answer(dataset_external_ids, question):
"""非流式调用外部聊天API获取回答"""
try:
chat_request_data = {
"id": "d5d11efa-ea9a-11ef-9933-0242ac120006",
"model_id": "7a214d0e-e65e-11ef-9f4a-0242ac120006",
"dataset_id_list": [str(id) for id in dataset_external_ids],
"multiple_rounds_dialogue": False,
"dataset_setting": {
"top_n": 10,
"similarity": "0.3",
"max_paragraph_char_number": 10000,
"search_mode": "blend",
"no_references_setting": {
"value": "{question}",
"status": "ai_questioning"
}
},
"model_setting": {
"prompt": "**相关文档内容**{data} **回答要求**:如果相关文档内容中没有可用信息,请回答\"没有在知识库中查找到相关信息,建议咨询相关技术支持或参考官方文档进行操作\"。请根据相关文档内容回答用户问题。不要输出与用户问题无关的内容。请使用中文回答客户问题。**用户问题**{question}"
},
"problem_optimization": False
}
logger.info(f"调用非流式聊天API: {settings.API_BASE_URL}/api/application/chat/open")
chat_response = requests.post(
url=f"{settings.API_BASE_URL}/api/application/chat/open",
json=chat_request_data,
headers={"Content-Type": "application/json"}
)
logger.info(f"聊天API响应状态码: {chat_response.status_code}")
if chat_response.status_code != 200:
logger.error(f"聊天API调用失败: {chat_response.text}")
raise ExternalAPIError(f"聊天API调用失败: {chat_response.text}")
chat_data = chat_response.json()
if chat_data.get('code') != 200 or not chat_data.get('data'):
logger.error(f"聊天API返回错误: {chat_data}")
raise ExternalAPIError(f"聊天API返回错误: {chat_data}")
chat_id = chat_data['data']
message_url = f"{settings.API_BASE_URL}/api/application/chat_message/{chat_id}"
logger.info(f"调用消息API: {message_url}")
message_response = requests.post(
url=message_url,
json={"message": question, "re_chat": False, "stream": False},
headers={"Content-Type": "application/json"}
)
if message_response.status_code != 200:
logger.error(f"消息API调用失败: {message_response.status_code}, {message_response.text}")
raise ExternalAPIError(f"消息API调用失败: {message_response.status_code}, {message_response.text}")
response_data = message_response.json()
if response_data.get('code') != 200 or 'data' not in response_data:
logger.error(f"消息API返回错误: {response_data}")
raise ExternalAPIError(f"消息API返回错误: {response_data}")
answer_content = response_data.get('data', {}).get('content', '')
return answer_content if answer_content else "无法获取回答内容"
except requests.exceptions.RequestException as e:
logger.error(f"聊天API网络错误: {str(e)}")
raise ExternalAPIError(f"聊天API网络错误: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"解析聊天API响应JSON失败: {str(e)}")
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
except Exception as e:
logger.error(f"调用聊天API失败: {str(e)}")
raise ExternalAPIError(f"调用聊天API失败: {str(e)}")
def generate_conversation_title_from_deepseek(user_question, assistant_answer):
"""调用SiliconCloud API生成会话标题直接基于当前问题和回答内容"""
try:
# 从Django设置中获取API密钥
api_key = settings.SILICON_CLOUD_API_KEY
if not api_key:
return "新对话"
# 构建提示信息
prompt = f"请根据用户的问题和助手的回答生成一个简短的对话标题不超过20个字\n\n用户问题: {user_question}\n\n助手回答: {assistant_answer}"
import requests
url = "https://api.siliconflow.cn/v1/chat/completions"
payload = {
"model": "deepseek-ai/DeepSeek-V3",
"stream": False,
"max_tokens": 512,
"temperature": 0.7,
"top_p": 0.7,
"top_k": 50,
"frequency_penalty": 0.5,
"n": 1,
"stop": [],
"messages": [
{
"role": "user",
"content": prompt
}
]
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
response_data = response.json()
if response.status_code == 200 and 'choices' in response_data and response_data['choices']:
title = response_data['choices'][0]['message']['content'].strip()
return title[:50] # 截断过长的标题
else:
logger.error(f"生成标题时出错: {response.text}")
return "新对话"
except Exception as e:
logger.exception(f"生成对话标题时发生错误: {str(e)}")
return "新对话"

3
apps/chat/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
apps/chat/urls.py Normal file
View File

@ -0,0 +1,11 @@
# apps/chat/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.chat.views import ChatHistoryViewSet
router = DefaultRouter()
router.register(r'', ChatHistoryViewSet, basename='chat-history')
urlpatterns = [
path('', include(router.urls)),
]

601
apps/chat/views.py Normal file
View File

@ -0,0 +1,601 @@
import logging
import json
import traceback
import uuid
from datetime import datetime
from django.db.models import Q, Max, Count
from django.http import HttpResponse, StreamingHttpResponse
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.decorators import action
from apps.user.models import User
from apps.chat.models import ChatHistory
from apps.chat.serializers import ChatHistorySerializer
from apps.common.services.chat_service import ChatService
from apps.chat.services.chat_api import (
ExternalAPIError, stream_chat_answer, get_chat_answer,
generate_conversation_title_from_deepseek
)
logger = logging.getLogger(__name__)
class ChatHistoryViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
serializer_class = ChatHistorySerializer
queryset = ChatHistory.objects.all()
# 固定知识库ID
DEFAULT_KNOWLEDGE_BASE_ID = "b680a4fa-37be-11f0-a7cb-0242ac120002"
def get_queryset(self):
"""只过滤用户自己的未删除的聊天记录"""
user = self.request.user
return ChatHistory.objects.filter(
user=user,
is_deleted=False
)
def list(self, request):
"""获取对话列表概览"""
try:
page = int(request.query_params.get('page', 1))
page_size = int(request.query_params.get('page_size', 10))
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')
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'],
'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:
logger.error(f"获取聊天记录失败: {str(e)}")
import traceback
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 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)
chat_service = ChatService()
result = chat_service.get_conversation_detail(request.user, conversation_id)
return Response({
'code': 200,
'message': '获取成功',
'data': result
})
except ValueError as e:
return Response({
'code': 404,
'message': str(e),
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"获取对话详情失败: {str(e)}")
import traceback
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):
"""获取默认知识库"""
return Response({
'code': 200,
'message': '获取成功',
'data': []
})
@action(detail=False, methods=['post'])
def create_conversation(self, request):
"""创建会话 - 使用默认知识库ID"""
try:
# 创建一个新的会话ID
conversation_id = str(uuid.uuid4())
logger.info(f"创建新的会话ID: {conversation_id}")
return Response({
'code': 200,
'message': '会话创建成功',
'data': {
'conversation_id': conversation_id
}
})
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):
"""创建聊天记录 - 使用默认知识库"""
try:
# 验证请求数据
if 'question' not in request.data:
return Response({
'code': 400,
'message': '缺少必填字段: question',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if 'conversation_id' not in request.data:
return Response({
'code': 400,
'message': '缺少必填字段: conversation_id',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
conversation_id = request.data['conversation_id']
question = request.data['question']
title = request.data.get('title', 'New chat')
# 准备metadata
metadata = {}
# 创建用户问题记录
question_record = ChatHistory.objects.create(
user=request.user,
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
conversation_id=conversation_id,
title=title,
role='user',
content=question,
metadata=metadata
)
# 设置外部API需要的ID列表
external_id_list = []
use_stream = request.data.get('stream', True)
if use_stream:
def stream_response():
answer_record = ChatHistory.objects.create(
user=question_record.user,
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
conversation_id=conversation_id,
title=title,
parent_id=str(question_record.id),
role='assistant',
content="",
metadata=metadata
)
yield f"data: {json.dumps({'code': 200, 'message': '开始流式传输', 'data': {'id': str(answer_record.id), 'conversation_id': conversation_id, 'content': '', 'is_end': False}})}\n\n"
full_content = ""
for data in stream_chat_answer(conversation_id, request.data['question'], external_id_list, metadata):
parsed_data = json.loads(data[5:-2]) # 移除"data: "和"\n\n"
if parsed_data['code'] == 200 and 'content' in parsed_data['data']:
content_part = parsed_data['data']['content']
full_content += content_part
response_data = {
'code': 200,
'message': 'partial',
'data': {
'id': str(answer_record.id),
'conversation_id': conversation_id,
'title': title,
'content': content_part,
'is_end': parsed_data['data']['is_end']
}
}
yield f"data: {json.dumps(response_data)}\n\n"
if parsed_data['data']['is_end']:
answer_record.content = full_content.strip()
answer_record.save()
current_title = ChatHistory.objects.filter(
conversation_id=conversation_id
).exclude(
title__in=["New chat", "新对话", ""]
).values_list('title', flat=True).first()
if current_title:
title_updated = current_title
else:
try:
generated_title = generate_conversation_title_from_deepseek(
request.data['question'], full_content.strip()
)
if generated_title:
ChatHistory.objects.filter(
conversation_id=conversation_id
).update(title=generated_title)
title_updated = generated_title
else:
title_updated = "新对话"
except Exception as e:
logger.error(f"自动生成标题失败: {str(e)}")
title_updated = "新对话"
final_response = {
'code': 200,
'message': '完成',
'data': {
'id': str(answer_record.id),
'conversation_id': conversation_id,
'title': title_updated,
'role': 'assistant',
'content': full_content.strip(),
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'is_end': True
}
}
yield f"data: {json.dumps(final_response)}\n\n"
break
elif parsed_data['code'] != 200:
yield data
break
if full_content:
try:
answer_record.content = full_content.strip()
answer_record.save()
except Exception as save_error:
logger.error(f"保存部分内容失败: {str(save_error)}")
response = StreamingHttpResponse(
stream_response(),
content_type='text/event-stream',
status=status.HTTP_201_CREATED
)
response['Cache-Control'] = 'no-cache, no-store'
response['Connection'] = 'keep-alive'
return response
else:
logger.info("使用非流式输出模式")
try:
answer = get_chat_answer(external_id_list, request.data['question'])
except ExternalAPIError as e:
logger.error(f"获取回答失败: {str(e)}")
return Response({
'code': 500,
'message': f'获取回答失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if answer is None:
return Response({
'code': 500,
'message': '获取回答失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
answer_record = ChatHistory.objects.create(
user=request.user,
knowledge_base_id=self.DEFAULT_KNOWLEDGE_BASE_ID,
conversation_id=conversation_id,
title=title,
parent_id=str(question_record.id),
role='assistant',
content=answer,
metadata=metadata
)
existing_records = ChatHistory.objects.filter(conversation_id=conversation_id)
should_generate_title = not existing_records.exclude(id=question_record.id).exists() and (not title or title == 'New chat')
if should_generate_title:
try:
generated_title = generate_conversation_title_from_deepseek(
request.data['question'], answer
)
if generated_title:
ChatHistory.objects.filter(conversation_id=conversation_id).update(title=generated_title)
title = generated_title
except Exception as e:
logger.error(f"自动生成标题失败: {str(e)}")
return Response({
'code': 200,
'message': '成功',
'data': {
'id': str(answer_record.id),
'conversation_id': conversation_id,
'title': title,
'role': 'assistant',
'content': answer,
'created_at': answer_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
}
}, status=status.HTTP_201_CREATED)
except Exception as e:
logger.error(f"创建聊天记录失败: {str(e)}")
import traceback
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):
"""高亮关键词"""
if not keyword or not text:
return text
return text.replace(keyword, f'<em class="highlight">{keyword}</em>')
def update(self, request, pk=None):
"""更新聊天记录"""
try:
record = self.get_queryset().filter(id=pk).first()
if not record:
return Response({
'code': 404,
'message': '记录不存在或无权限',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
data = request.data
updateable_fields = ['content', 'metadata']
if 'content' in data:
record.content = data['content']
if 'metadata' in data:
current_metadata = record.metadata or {}
current_metadata.update(data['metadata'])
record.metadata = current_metadata
record.save()
return Response({
'code': 200,
'message': '更新成功',
'data': {
'id': str(record.id),
'conversation_id': record.conversation_id,
'role': record.role,
'content': record.content,
'metadata': record.metadata,
'updated_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
})
except Exception as e:
logger.error(f"更新聊天记录失败: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return Response({
'code': 500,
'message': f'更新聊天记录失败: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def destroy(self, request, pk=None):
"""删除聊天记录(软删除)"""
try:
record = self.get_queryset().filter(id=pk).first()
if not record:
return Response({
'code': 404,
'message': '记录不存在或无权限',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
record.soft_delete()
return Response({
'code': 200,
'message': '删除成功',
'data': None
})
except Exception as e:
logger.error(f"删除聊天记录失败: {str(e)}")
import traceback
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 search(self, request):
"""搜索聊天记录"""
try:
keyword = request.query_params.get('keyword', '').strip()
start_date = request.query_params.get('start_date')
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))
query = self.get_queryset()
if keyword:
query = query.filter(
Q(content__icontains=keyword)
)
if start_date:
query = query.filter(created_at__gte=start_date)
if end_date:
query = query.filter(created_at__lte=end_date)
total = query.count()
start = (page - 1) * page_size
end = start + page_size
records = query.order_by('-created_at')[start:end]
results = [
{
'id': str(record.id),
'conversation_id': record.conversation_id,
'role': record.role,
'content': record.content,
'created_at': record.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'metadata': record.metadata,
'highlights': {'content': self._highlight_keyword(record.content, keyword)} if keyword else {}
}
for record in records
]
return Response({
'code': 200,
'message': '搜索成功',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'results': results
}
})
except Exception as e:
logger.error(f"搜索聊天记录失败: {str(e)}")
import traceback
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=['delete'])
def delete_conversation(self, request):
"""通过conversation_id删除一组会话"""
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)
records = self.get_queryset().filter(conversation_id=conversation_id)
if not records.exists():
return Response({
'code': 404,
'message': '未找到该会话或无权限访问',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
records_count = records.count()
for record in records:
record.soft_delete()
return Response({
'code': 200,
'message': '删除成功',
'data': {
'conversation_id': conversation_id,
'deleted_count': records_count
}
})
except Exception as e:
logger.error(f"删除会话失败: {str(e)}")
import traceback
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'], url_path='generate-conversation-title')
def generate_conversation_title(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,
is_deleted=False,
user=request.user
).order_by('created_at')
if not messages.exists():
return Response({
'code': 404,
'message': '对话不存在或无权访问',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 检查是否有自定义标题参数
custom_title = request.query_params.get('title')
if not custom_title:
return Response({
'code': 400,
'message': '缺少title参数',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 更新所有相关记录的标题
ChatHistory.objects.filter(
conversation_id=conversation_id,
user=request.user
).update(title=custom_title)
return Response({
'code': 200,
'message': '更新会话标题成功',
'data': {
'conversation_id': conversation_id,
'title': custom_title
}
})
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)

0
apps/common/__init__.py Normal file
View File

3
apps/common/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/common/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CommonConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.common'

View File

@ -0,0 +1,46 @@
from django.db import close_old_connections
from rest_framework.authtoken.models import Token
from channels.middleware import BaseMiddleware
from channels.db import database_sync_to_async
from urllib.parse import parse_qs
import logging
logger = logging.getLogger(__name__)
@database_sync_to_async
def get_user_from_token(token_key):
try:
token = Token.objects.select_related('user').get(key=token_key)
return token.user
except Token.DoesNotExist:
return None
except Exception as e:
logger.error(f"获取用户Token失败: {str(e)}")
return None
class TokenAuthMiddleware(BaseMiddleware):
async def __call__(self, scope, receive, send):
# 关闭之前的数据库连接
close_old_connections()
# 从查询字符串中提取token
query_string = scope.get('query_string', b'').decode()
query_params = parse_qs(query_string)
token_key = query_params.get('token', [''])[0]
if token_key:
user = await get_user_from_token(token_key)
if user:
scope['user'] = user
logger.info(f"WebSocket认证成功: 用户 {user.id}")
else:
logger.warning(f"WebSocket认证失败: 无效的Token {token_key}")
scope['user'] = None
else:
logger.warning("WebSocket连接未提供Token")
scope['user'] = None
return await super().__call__(scope, receive, send)
def TokenAuthMiddlewareStack(inner):
return TokenAuthMiddleware(inner)

View File

3
apps/common/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

View File

@ -0,0 +1,202 @@
import requests
import logging
import json
from django.conf import settings
from datetime import datetime
logger = logging.getLogger(__name__)
class AIService:
"""
通用AI服务类提供对不同AI模型的统一调用接口
"""
@staticmethod
def call_silicon_cloud_api(messages, model="deepseek-ai/DeepSeek-V3", max_tokens=512, temperature=0.7):
"""
调用SiliconCloud API
Args:
messages: 消息列表格式为[{"role": "user", "content": "..."}, ...]
model: 使用的模型名称默认为DeepSeek-V3
max_tokens: 最大生成token数
temperature: 温度参数控制创造性
Returns:
tuple: (生成内容, 错误信息)
"""
try:
# 获取API密钥
api_key = getattr(settings, 'SILICON_CLOUD_API_KEY', '')
if not api_key:
return None, "未配置Silicon Cloud API密钥"
url = "https://api.siliconflow.cn/v1/chat/completions"
payload = {
"model": model,
"stream": False,
"max_tokens": max_tokens,
"temperature": temperature,
"top_p": 0.7,
"top_k": 50,
"frequency_penalty": 0.5,
"n": 1,
"stop": [],
"messages": messages
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
# 设置代理(如果配置了)
proxies = None
proxy_url = getattr(settings, 'PROXY_URL', None)
if proxy_url:
proxies = {
"http": proxy_url,
"https": proxy_url
}
# 发送请求
logger.info(f"正在调用SiliconCloud API模型: {model}")
response = requests.post(url, json=payload, headers=headers, proxies=proxies)
if response.status_code != 200:
logger.error(f"API请求失败状态码: {response.status_code},响应: {response.text}")
return None, f"API请求失败状态码: {response.status_code}"
result = response.json()
# 从API响应中提取内容
if 'choices' in result and result['choices']:
content = result['choices'][0]['message']['content'].strip()
return content, None
else:
logger.error(f"API响应格式错误: {result}")
return None, "API响应格式错误无法提取内容"
except requests.exceptions.RequestException as e:
logger.error(f"API请求异常: {str(e)}")
return None, f"API请求异常: {str(e)}"
except Exception as e:
logger.error(f"调用AI服务失败: {str(e)}")
return None, f"调用AI服务失败: {str(e)}"
@staticmethod
def generate_email_reply(goal_description, conversation_summary, last_message):
"""
生成邮件回复话术
Args:
goal_description: 用户目标描述
conversation_summary: 对话摘要
last_message: 达人最后发送的消息内容
Returns:
tuple: (推荐话术内容, 错误信息)
"""
try:
# 验证必要参数
if not goal_description or not last_message:
return None, "缺少必要参数:目标描述或最后消息"
# 准备提示信息
prompt = f"""
请为用户提供一条回复达人邮件的推荐话术帮助用户实现销售目标
用户目标{goal_description}
对话摘要{conversation_summary or '无对话摘要'}
达人最后发送的消息
{last_message}
要求
1. 回复应当专业礼貌并围绕用户目标展开
2. 提供有说服力的话术推动促成合作
3. 不超过200字
4. 直接给出回复内容不要包含任何额外解释
"""
messages = [
{
"role": "system",
"content": "你是一名专业的邮件回复助手,你的任务是生成有效的销售话术,帮助用户实现销售目标。"
},
{
"role": "user",
"content": prompt
}
]
# 调用通用API方法
return AIService.call_silicon_cloud_api(
messages,
model="Pro/deepseek-ai/DeepSeek-R1",
max_tokens=512,
temperature=0.7
)
except Exception as e:
logger.error(f"生成回复话术失败: {str(e)}")
return None, f"生成回复话术失败: {str(e)}"
@staticmethod
def generate_conversation_summary(conversation_history):
"""
生成对话摘要
Args:
conversation_history: 对话历史记录列表
Returns:
tuple: (摘要内容, 错误信息)
"""
try:
if not conversation_history:
return None, "无对话历史,无法生成摘要"
# 构造对话历史文本
conversation_text = "\n\n".join([
f"**{'用户' if msg.get('is_from_user', False) else '达人'}**: {msg.get('content', '')}"
for msg in conversation_history
])
# 准备提示信息
prompt = f"""
请对以下用户与达人之间的对话内容进行总结:
{conversation_text}
要求:
1. 总结应包含双方主要讨论的话题和关键点
2. 特别关注产品详情价格谈判合作意向等商务要点
3. 简明扼要不超过200字
4. 直接给出总结内容不要包含任何额外解释
"""
messages = [
{
"role": "system",
"content": "你是一名专业的对话总结助手,擅长提取商务沟通中的关键信息。"
},
{
"role": "user",
"content": prompt
}
]
# 调用通用API方法
return AIService.call_silicon_cloud_api(
messages,
model="Pro/deepseek-ai/DeepSeek-R1",
max_tokens=512,
temperature=0.5
)
except Exception as e:
logger.error(f"生成对话摘要失败: {str(e)}")
return None, f"生成对话摘要失败: {str(e)}"

View File

@ -0,0 +1,118 @@
# apps/common/services/chat_service.py
import logging
import json
from uuid import uuid4
from django.db import transaction
from apps.user.models import User
from apps.chat.models import ChatHistory
logger = logging.getLogger(__name__)
class ChatService:
@transaction.atomic
def create_chat_record(self, user, data, conversation_id=None):
"""创建聊天记录供chat、gmail、feishu模块复用"""
try:
# 验证必填字段
if 'question' not in data:
raise ValueError("缺少必填字段: question")
# 如果未提供conversation_id生成新的
if not conversation_id:
conversation_id = str(uuid4())
logger.info(f"生成新的会话ID: {conversation_id}")
# 创建metadata
metadata = {
'model_id': data.get('model_id', '7a214d0e-e65e-11ef-9f4a-0242ac120006'),
}
# 设置标题
title = data.get('title', 'New chat')
# 创建用户问题记录
question_record = ChatHistory.objects.create(
user=user,
knowledge_base_id="b680a4fa-37be-11f0-a7cb-0242ac120002", # 使用默认知识库ID
conversation_id=conversation_id,
title=title,
role='user',
content=data['question'],
metadata=metadata
)
return question_record, conversation_id, metadata, [], []
except Exception as e:
logger.error(f"创建聊天记录失败: {str(e)}")
raise
def get_conversation_detail(self, user, conversation_id):
"""获取会话详情供chat、gmail、feishu模块复用"""
try:
# 查询会话记录
messages = ChatHistory.objects.filter(
conversation_id=conversation_id,
user=user,
is_deleted=False
).order_by('created_at')
if not messages.exists():
raise ValueError("对话不存在或无权限")
# 构建消息列表
message_list = [
{
'id': str(msg.id),
'parent_id': msg.parent_id,
'role': msg.role,
'content': msg.content,
'created_at': msg.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'metadata': msg.metadata
}
for msg in messages
]
return {
'conversation_id': conversation_id,
'messages': message_list
}
except Exception as e:
logger.error(f"获取会话详情失败: {str(e)}")
raise
def can_user_access_conversation(self, user, conversation_id):
"""检查用户是否有权限访问指定的对话"""
return ChatHistory.objects.filter(
conversation_id=conversation_id,
user=user,
is_deleted=False
).exists()
def format_chat_response(self, chat_history_list):
"""格式化聊天历史记录为前端需要的格式"""
try:
result = []
for item in chat_history_list:
formatted_item = {
"id": str(item.id),
"conversation_id": str(item.conversation_id),
"role": item.role,
"content": item.content,
"created_at": item.created_at.strftime('%Y-%m-%d %H:%M:%S'),
"title": item.title
}
# 添加parent_id如果存在
if item.parent_id:
formatted_item["parent_id"] = str(item.parent_id)
result.append(formatted_item)
return result
except Exception as e:
logger.error(f"格式化聊天响应失败: {e}")
return []

View File

@ -0,0 +1,320 @@
# apps/common/services/external_api_service.py
import traceback
import requests
import json
import logging
from django.conf import settings
from rest_framework.exceptions import APIException
logger = logging.getLogger(__name__)
class ExternalAPIError(APIException):
status_code = 500
default_detail = '外部API调用失败'
default_code = 'external_api_error'
def create_external_dataset(knowledge_base):
"""创建外部知识库"""
try:
api_data = {
"name": knowledge_base.name,
"desc": knowledge_base.desc,
"type": "0",
"meta": {},
"documents": []
}
response = requests.post(
f'{settings.API_BASE_URL}/api/dataset',
json=api_data,
headers={'Content-Type': 'application/json'},
)
if response.status_code != 200:
raise ExternalAPIError(f"创建失败,状态码: {response.status_code}, 响应: {response.text}")
api_response = response.json()
if not api_response.get('code') == 200:
raise ExternalAPIError(f"业务处理失败: {api_response.get('message', '未知错误')}")
dataset_id = api_response.get('data', {}).get('id')
if not dataset_id:
raise ExternalAPIError("响应数据中缺少dataset id")
return dataset_id
except requests.exceptions.Timeout:
raise ExternalAPIError("请求超时,请稍后重试")
except requests.exceptions.RequestException as e:
raise ExternalAPIError(f"API请求失败: {str(e)}")
except Exception as e:
raise ExternalAPIError(f"创建外部知识库失败: {str(e)}")
def delete_external_dataset(external_id):
"""删除外部知识库"""
try:
if not external_id:
logger.warning("外部知识库ID为空跳过删除")
return True
response = requests.delete(
f'{settings.API_BASE_URL}/api/dataset/{external_id}',
headers={'Content-Type': 'application/json'},
)
logger.info(f"删除外部知识库响应: status_code={response.status_code}, response={response.text}")
if response.status_code == 404:
logger.warning(f"外部知识库不存在: {external_id}")
return True
elif response.status_code not in [200, 204]:
return True # 允许本地删除继续
if response.status_code == 204:
logger.info(f"外部知识库删除成功: {external_id}")
return True
try:
api_response = response.json()
if api_response.get('code') != 200:
if "不存在" in api_response.get('message', ''):
logger.warning(f"外部知识库ID不存在视为删除成功: {external_id}")
return True
logger.warning(f"业务处理返回非200状态码: {api_response.get('code')}, {api_response.get('message')}")
return True
logger.info(f"外部知识库删除成功: {external_id}")
return True
except ValueError:
logger.warning(f"外部知识库删除响应无法解析JSON但状态码为200视为成功: {external_id}")
return True
except requests.exceptions.Timeout:
logger.error(f"删除外部知识库超时: {external_id}")
return False
except requests.exceptions.RequestException as e:
logger.error(f"删除外部知识库请求异常: {external_id}, error={str(e)}")
return False
except Exception as e:
logger.error(f"删除外部知识库其他错误: {external_id}, error={str(e)}")
return False
def call_split_api_multiple(files):
"""调用文档分割API支持多文件批量处理"""
try:
url = f'{settings.API_BASE_URL}/api/dataset/document/split'
# 准备请求数据 - 将所有文件作为 'file' 字段
files_data = [('file', (file.name, file, file.content_type)) for file in files]
# 记录上传的文件信息
for file in files:
logger.info(f"准备上传文件: {file.name}, 大小: {file.size}字节, 类型: {file.content_type}")
# 读取文件内容前100个字符进行记录
if hasattr(file, 'read') and hasattr(file, 'seek'):
file.seek(0)
content_preview = file.read(100).decode('utf-8', errors='ignore')
logger.info(f"文件内容预览: {content_preview}")
file.seek(0) # 重置文件指针
logger.info(f"调用分割API URL: {url}")
logger.info(f"上传文件数量: {len(files_data)}")
# 发送请求
response = requests.post(
url,
files=files_data
)
# 记录请求头和响应信息
logger.info(f"请求头: {response.request.headers}")
logger.info(f"响应状态码: {response.status_code}")
if response.status_code != 200:
logger.error(f"分割API返回错误状态码: {response.status_code}, 响应: {response.text}")
return None
# 解析响应
result = response.json()
logger.info(f"分割API响应详情: {result}")
# 如果数据为空可能是API处理失败尝试后备方案
if len(result.get('data', [])) == 0:
logger.warning("分割API返回的数据为空尝试使用后备方案")
fallback_data = {
'code': 200,
'message': '成功',
'data': [
{
'name': file.name,
'content': [
{
'title': '文档内容',
'content': '文件内容无法自动分割请检查外部API。这是一个后备内容。'
}
]
} for file in files
]
}
logger.info("使用后备数据结构")
return fallback_data
return result
except Exception as e:
logger.error(f"调用分割API失败: {str(e)}")
logger.error(traceback.format_exc())
# 创建后备响应
fallback_response = {
'code': 200,
'message': '成功',
'data': [
{
'name': file.name,
'content': [
{
'title': '文档内容',
'content': '文件内容无法自动分割请检查API连接。'
}
]
} for file in files
]
}
logger.info("由于异常,返回后备响应")
return fallback_response
def call_upload_api(external_id, doc_data):
"""调用文档上传API"""
try:
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document'
logger.info(f"调用文档上传API: {url}")
logger.info(f"上传文档数据: 文档名={doc_data.get('name')}, 段落数={len(doc_data.get('paragraphs', []))}")
response = requests.post(url, json=doc_data)
logger.info(f"上传API响应状态码: {response.status_code}")
if response.status_code != 200:
logger.error(f"上传API HTTP错误: {response.status_code}, 响应: {response.text}")
return {
'code': response.status_code,
'message': f"上传失败HTTP状态码: {response.status_code}",
'data': None
}
result = response.json()
logger.info(f"上传API响应内容: {result}")
if result.get('code') != 200:
error_msg = result.get('message', '未知错误')
logger.error(f"上传API业务错误: {error_msg}")
return {
'code': result.get('code', 500),
'message': error_msg,
'data': None
}
return result
except requests.exceptions.RequestException as e:
logger.error(f"调用上传API网络错误: {str(e)}")
return {
'code': 500,
'message': f"网络请求错误: {str(e)}",
'data': None
}
except json.JSONDecodeError as e:
logger.error(f"解析API响应JSON失败: {str(e)}")
return {
'code': 500,
'message': f"解析响应数据失败: {str(e)}",
'data': None
}
except Exception as e:
logger.error(f"调用上传API其他错误: {str(e)}")
return {
'code': 500,
'message': f"上传API调用失败: {str(e)}",
'data': None
}
def call_delete_document_api(external_id, document_id):
"""调用文档删除API"""
try:
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document/{document_id}'
response = requests.delete(url)
return response.json()
except Exception as e:
logger.error(f"调用删除API失败: {str(e)}")
return None
def get_external_document_list(external_id):
"""获取外部知识库的文档列表"""
try:
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document'
logger.info(f"调用获取文档列表API: {url}")
response = requests.get(
url,
headers={'Content-Type': 'application/json'},
)
logger.info(f"文档列表API响应状态码: {response.status_code}")
if response.status_code != 200:
logger.error(f"获取文档列表失败: {response.status_code}, 响应: {response.text}")
raise ExternalAPIError(f"获取文档列表失败,状态码: {response.status_code}")
result = response.json()
logger.info(f"文档列表API响应内容: {result}")
if result.get('code') != 200:
logger.error(f"获取文档列表业务错误: {result.get('message', '未知错误')}")
raise ExternalAPIError(f"获取文档列表失败: {result.get('message', '未知错误')}")
return result.get('data', [])
except requests.exceptions.RequestException as e:
logger.error(f"获取文档列表网络错误: {str(e)}")
raise ExternalAPIError(f"获取文档列表失败: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"解析文档列表响应JSON失败: {str(e)}")
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
except Exception as e:
logger.error(f"获取文档列表其他错误: {str(e)}")
raise ExternalAPIError(f"获取文档列表失败: {str(e)}")
def get_external_document_paragraphs(external_id, document_external_id):
"""获取外部文档的段落内容"""
try:
url = f'{settings.API_BASE_URL}/api/dataset/{external_id}/document/{document_external_id}/paragraph'
logger.info(f"调用获取文档段落API: {url}")
response = requests.get(url)
logger.info(f"文档段落API响应状态码: {response.status_code}")
if response.status_code != 200:
logger.error(f"获取文档段落内容失败: {response.status_code}, 响应: {response.text}")
raise ExternalAPIError(f"获取文档段落内容失败,状态码: {response.status_code}")
result = response.json()
logger.info(f"文档段落API响应内容: {result}")
if result.get('code') != 200:
logger.error(f"获取文档段落内容业务错误: {result.get('message', '未知错误')}")
raise ExternalAPIError(f"获取文档段落内容失败: {result.get('message', '未知错误')}")
return result.get('data', [])
except requests.exceptions.RequestException as e:
logger.error(f"获取文档段落内容网络错误: {str(e)}")
raise ExternalAPIError(f"获取文档段落内容失败: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"解析文档段落响应JSON失败: {str(e)}")
raise ExternalAPIError(f"解析响应数据失败: {str(e)}")
except Exception as e:
logger.error(f"获取文档段落内容其他错误: {str(e)}")
raise ExternalAPIError(f"获取文档段落内容失败: {str(e)}")

3
apps/common/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

0
apps/common/urls.py Normal file
View File

3
apps/common/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -501,6 +501,7 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet):
import logging
import json
from apps.daren_detail.models import CreatorProfile
from apps.user.models import User
logger = logging.getLogger(__name__)
@ -562,38 +563,46 @@ class CreatorDiscoveryViewSet(viewsets.ReadOnlyModelViewSet):
matched_creator_ids = []
for creator_data in api_results:
# 根据API结果获取或创建CreatorProfile
try:
# 尝试查找CreatorProfile使用filter和first而非get避免多个结果的问题
profile_obj = CreatorProfile.objects.filter(
name=creator_data.get('name', 'Unknown')
).first()
# 安全获取创作者名称,确保有一个默认值
creator_name = creator_data.get('name', 'Unknown Creator')
# 按名称找到第一个匹配的CreatorProfile或创建新的
profile_obj = None
try:
profile_obj = CreatorProfile.objects.filter(name=creator_name).first()
except Exception as e:
logger.error(f"查询CreatorProfile时出错: {str(e)}")
# 如果没有找到,则创建一个新的
if not profile_obj:
profile_obj = CreatorProfile.objects.create(
name=creator_data.get('name', 'Unknown'),
avatar_url=creator_data.get('avatar_url'),
email=creator_data.get('email'),
instagram=creator_data.get('instagram'),
tiktok_link=creator_data.get('tiktok_link'),
location=creator_data.get('location'),
live_schedule=creator_data.get('live_schedule'),
category=creator_data.get('category'),
e_commerce_level=creator_data.get('e_commerce_level'),
exposure_level=creator_data.get('exposure_level'),
followers=creator_data.get('followers', 0),
gmv=creator_data.get('gmv', 0),
items_sold=creator_data.get('items_sold', 0),
avg_video_views=creator_data.get('avg_video_views', 0),
pricing=creator_data.get('pricing'),
pricing_package=creator_data.get('pricing_package'),
collab_count=creator_data.get('collab_count'),
latest_collab=creator_data.get('latest_collab'),
profile=creator_data.get('profile'),
hashtags=creator_data.get('hashtags', ''),
trends=creator_data.get('trends', '')
)
# 如果没有找到创建一个新的CreatorProfile
try:
profile_obj = CreatorProfile.objects.create(
name=creator_name,
avatar_url=creator_data.get('avatar_url'),
email=creator_data.get('email'),
instagram=creator_data.get('instagram'),
tiktok_link=creator_data.get('tiktok_link'),
location=creator_data.get('location'),
live_schedule=creator_data.get('live_schedule'),
category=creator_data.get('category'),
e_commerce_level=creator_data.get('e_commerce_level'),
exposure_level=creator_data.get('exposure_level'),
followers=creator_data.get('followers', 0),
gmv=creator_data.get('gmv', 0),
items_sold=creator_data.get('items_sold', 0),
avg_video_views=creator_data.get('avg_video_views', 0),
pricing=creator_data.get('pricing'),
pricing_package=creator_data.get('pricing_package'),
collab_count=creator_data.get('collab_count'),
latest_collab=creator_data.get('latest_collab'),
profile=creator_data.get('profile'),
hashtags=creator_data.get('hashtags', ''),
trends=creator_data.get('trends', '')
)
except Exception as e:
logger.error(f"创建CreatorProfile时出错: {str(e)}")
continue
# 检查是否已经在当前session中存在相同creator
existing_creator = Creator.objects.filter(

View File

@ -23,12 +23,14 @@ from channels.auth import AuthMiddlewareStack
# 确保在django.setup()之后再导入
import apps.brands.routing
import apps.chat.routing
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
apps.brands.routing.websocket_urlpatterns
apps.brands.routing.websocket_urlpatterns +
apps.chat.routing.websocket_urlpatterns
)
),
})

View File

@ -49,6 +49,8 @@ INSTALLED_APPS = [
"apps.brands.apps.BrandsConfig",
'rest_framework',
'rest_framework_simplejwt',
'apps.chat.apps.ChatConfig',
'apps.common.apps.CommonConfig',
]
MIDDLEWARE = [
@ -100,7 +102,7 @@ DATABASES = {
'NAME': 'daren_detail',
'USER': 'root',
'PASSWORD': '123456',
'HOST': 'localhost',
'HOST': '192.168.31.138',
'PORT': '3306',
'OPTIONS': {
'charset': 'utf8mb4',
@ -209,9 +211,13 @@ AUTH_USER_MODEL = 'user.User'
# REST Framework 设置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [], # 默认不需要认证
'DEFAULT_PERMISSION_CLASSES': [], # 默认不需要权限
'UNAUTHENTICATED_USER': None
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'apps.user.authentication.CustomTokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# JWT 设置
@ -230,3 +236,6 @@ SIMPLE_JWT = {
'JTI_CLAIM': None, # 不在 token 中包含 JWT ID
}
API_BASE_URL = 'http://81.69.223.133:48329'
SILICON_CLOUD_API_KEY = 'sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf'

View File

@ -15,4 +15,5 @@ urlpatterns = [
path('api/discovery/', include('apps.discovery.urls')),
path('api/template/', include('apps.template.urls')),
path('api/', include('apps.brands.urls')),
path('api/chat-history/', include('apps.chat.urls')),
]