diff --git a/apps/application/migrations/0015_chat_execute_type.py b/apps/application/migrations/0015_chat_execute_type.py new file mode 100644 index 00000000000..e3b61cd9969 --- /dev/null +++ b/apps/application/migrations/0015_chat_execute_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.14 on 2026-07-21 08:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('application', '0014_chatrecord_workflow_context'), + ] + + operations = [ + migrations.AddField( + model_name='chat', + name='execute_type', + field=models.CharField(choices=[('ANONYMOUS_USER', '匿名用户'), ('CHAT_USER', '对话用户'), ('SYSTEM_API_KEY', '系统API_KEY'), ('APPLICATION_API_KEY', '应用API_KEY'), ('PLATFORM_USER', '平台用户')], default='CHAT', max_length=64, verbose_name='执行类型'), + ), + ] diff --git a/apps/application/models/application_chat.py b/apps/application/models/application_chat.py index 6f0a5bbcdb7..4b4f7979994 100644 --- a/apps/application/models/application_chat.py +++ b/apps/application/models/application_chat.py @@ -26,6 +26,11 @@ class ChatUserType(models.TextChoices): PLATFORM_USER = "PLATFORM_USER", "平台用户" +class ExecuteType(models.TextChoices): + DEBUG = "DEBUG" + CHAT = "CHAT" + + def default_asker(): return {'username': '游客'} @@ -37,6 +42,8 @@ class Chat(AppModelMixin): chat_user_id = models.CharField(verbose_name="对话用户id", default=None, null=True) chat_user_type = models.CharField(max_length=64, verbose_name="客户端类型", choices=ChatUserType.choices, default=ChatUserType.ANONYMOUS_USER) + execute_type = models.CharField(max_length=64, verbose_name="执行类型", choices=ChatUserType.choices, + default=ExecuteType.CHAT) is_deleted = models.BooleanField(verbose_name="逻辑删除", default=False) asker = models.JSONField(verbose_name="访问者", default=default_asker, encoder=SystemEncoder) meta = models.JSONField(verbose_name="元数据", default=dict) @@ -65,10 +72,12 @@ class VoteReasonChoices(models.TextChoices): INCOMPLETE = 'incomplete', '内容不完善' OTHER = 'other', '其他' + class ShareLinkType(models.TextChoices): PUBLIC = "PUBLIC", 'public' PRIVATE = "PRIVATE", 'private' + class ChatSourceChoices(models.TextChoices): ONLINE = "ONLINE", "线上使用" API_CALL = "API_CALL", "API调用" @@ -144,10 +153,11 @@ class Meta: models.Index(fields=['application_id', 'chat_user_id']), ] + class ChatShareLink(AppModelMixin): id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") chat = models.ForeignKey(Chat, on_delete=models.CASCADE) - application = models.ForeignKey(Application,on_delete=models.CASCADE) + application = models.ForeignKey(Application, on_delete=models.CASCADE) share_type = models.CharField(max_length=20, choices=ShareLinkType.choices, default=ShareLinkType.PUBLIC) user = models.ForeignKey(User, on_delete=models.SET_NULL, db_constraint=False, blank=True, null=True) chat_record_ids = ArrayField(base_field=models.UUIDField(max_length=128)) @@ -156,11 +166,10 @@ class Meta: db_table = "application_chat_share_link" - class ApplicationLongTermMemory(AppModelMixin): id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") application = models.ForeignKey(Application, on_delete=models.CASCADE, db_constraint=False, verbose_name="所属应用") - chat_user_id = models.CharField( max_length=128, verbose_name="对话用户id", db_index=True) + chat_user_id = models.CharField(max_length=128, verbose_name="对话用户id", db_index=True) memory = models.TextField(verbose_name="长期记忆内容", default="") class Meta: diff --git a/apps/application/serializers/common.py b/apps/application/serializers/common.py index b1d08a460c1..96357ea187f 100644 --- a/apps/application/serializers/common.py +++ b/apps/application/serializers/common.py @@ -13,7 +13,8 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from application.models import Application, ChatRecord, Chat, ApplicationVersion, ChatUserType, ApplicationTypeChoices +from application.models import Application, ChatRecord, Chat, ApplicationVersion, ChatUserType, ApplicationTypeChoices, \ + ExecuteType from application.serializers.application_chat import ChatCountSerializer from common.constants.cache_version import Cache_Version from common.database_model_manage.database_model_manage import DatabaseModelManage @@ -280,6 +281,13 @@ def set_chat(self, question): ip_address=self.ip_address, source=self.source, asker=self.get_chat_user()).save() + def save_chat(self): + Chat(id=self.chat_id, application_id=self.application_id, abstract="新建对话", + execute_type=ExecuteType.DEBUG if self.debug else ExecuteType.CHAT, + chat_user_id=self.chat_user_id, chat_user_type=self.chat_user_type, + ip_address=self.ip_address, source=self.source, + asker=self.get_chat_user()).save() + def set_chat_variable(self, chat_context): if not self.debug: chat = QuerySet(Chat).filter(id=self.chat_id).first() @@ -314,55 +322,48 @@ def append_chat_record(self, chat_record: ChatRecord): break if is_save: self.chat_record_list.append(chat_record) - - # 调试模式:把 workflow_context 存到 Redis - if self.debug and chat_record.workflow_context: - from django.core.cache import cache - from common.constants.cache_version import Cache_Version - cache_key = Cache_Version.DEBUG_WORKFLOW_CONTEXT.get_key(chat_record_id=str(chat_record.id)) - cache.set(cache_key, chat_record.workflow_context, timeout=3600) - - if not self.debug: - if not QuerySet(Chat).filter(id=self.chat_id).exists(): - Chat(id=self.chat_id, application_id=self.application_id, abstract=chat_record.problem_text[0:1024], - chat_user_id=self.chat_user_id, chat_user_type=self.chat_user_type, - ip_address=self.ip_address, source=self.source, - asker=self.get_chat_user()).save() - else: - QuerySet(Chat).filter(id=self.chat_id).update(update_time=timezone.now()) - # 插入会话记录 - QuerySet(ChatRecord).update_or_create(id=chat_record.id, - create_defaults={'id': chat_record.id, - 'chat_id': chat_record.chat_id, - "vote_status": chat_record.vote_status, - 'problem_text': chat_record.problem_text, - 'answer_text': chat_record.answer_text, - 'answer_text_list': chat_record.answer_text_list, - 'message_tokens': chat_record.message_tokens, - 'answer_tokens': chat_record.answer_tokens, - 'const': chat_record.const, - 'details': chat_record.details, - 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list, - 'run_time': chat_record.run_time, - 'source': chat_record.source, - 'ip_address': chat_record.ip_address or '', - 'index': chat_record.index}, - defaults={ - "vote_status": chat_record.vote_status, - 'problem_text': chat_record.problem_text, - 'answer_text': chat_record.answer_text, - 'answer_text_list': chat_record.answer_text_list, - 'message_tokens': chat_record.message_tokens, - 'answer_tokens': chat_record.answer_tokens, - 'const': chat_record.const, - 'details': chat_record.details, - 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list, - 'run_time': chat_record.run_time, - 'index': chat_record.index, - 'source': chat_record.source, - 'ip_address': chat_record.ip_address or '', - }) - ChatCountSerializer(data={'chat_id': self.chat_id}).update_chat() + + if not QuerySet(Chat).filter(id=self.chat_id).exists(): + Chat(id=self.chat_id, application_id=self.application_id, abstract=chat_record.problem_text[0:1024], + execute_type=ExecuteType.DEBUG if self.debug else ExecuteType.CHAT, + chat_user_id=self.chat_user_id, chat_user_type=self.chat_user_type, + ip_address=self.ip_address, source=self.source, + asker=self.get_chat_user()).save() + else: + QuerySet(Chat).filter(id=self.chat_id).update(update_time=timezone.now()) + # 插入会话记录 + QuerySet(ChatRecord).update_or_create(id=chat_record.id, + create_defaults={'id': chat_record.id, + 'chat_id': chat_record.chat_id, + "vote_status": chat_record.vote_status, + 'problem_text': chat_record.problem_text, + 'answer_text': chat_record.answer_text, + 'answer_text_list': chat_record.answer_text_list, + 'message_tokens': chat_record.message_tokens, + 'answer_tokens': chat_record.answer_tokens, + 'const': chat_record.const, + 'details': chat_record.details, + 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list, + 'run_time': chat_record.run_time, + 'source': chat_record.source, + 'ip_address': chat_record.ip_address or '', + 'index': chat_record.index}, + defaults={ + "vote_status": chat_record.vote_status, + 'problem_text': chat_record.problem_text, + 'answer_text': chat_record.answer_text, + 'answer_text_list': chat_record.answer_text_list, + 'message_tokens': chat_record.message_tokens, + 'answer_tokens': chat_record.answer_tokens, + 'const': chat_record.const, + 'details': chat_record.details, + 'improve_paragraph_id_list': chat_record.improve_paragraph_id_list, + 'run_time': chat_record.run_time, + 'index': chat_record.index, + 'source': chat_record.source, + 'ip_address': chat_record.ip_address or '', + }) + ChatCountSerializer(data={'chat_id': self.chat_id}).update_chat() def to_dict(self): diff --git a/apps/application/urls.py b/apps/application/urls.py index 1c473b63935..976fb1fbb0c 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -44,4 +44,7 @@ path('workspace//application//mcp_tools', views.McpServers.as_view()), path('workspace//application//model//prompt_generate', views.PromptGenerateView.as_view()), path('chat_message/', views.ChatView.as_view()), + path('workspace//application//historical_conversation//', views.DebugHistoricalConversation.PageView.as_view()), + path('workspace//application//historical_conversation/', views.DebugHistoricalConversation.Operate.as_view()), + path('workspace//application//historical_conversation_record///', views.DebugHistoricalConversation.RecordPageView.as_view()), ] diff --git a/apps/application/views/application_chat.py b/apps/application/views/application_chat.py index d60206ded5a..c0b0c5bb5c8 100644 --- a/apps/application/views/application_chat.py +++ b/apps/application/views/application_chat.py @@ -16,7 +16,7 @@ from application.api.application_chat import ApplicationChatQueryAPI, ApplicationChatQueryPageAPI, \ ApplicationChatExportAPI -from application.models import ChatUserType, Application +from application.models import ChatUserType, Application, ChatSourceChoices from application.serializers.application_chat import ApplicationChatQuerySerializers from chat.api.chat_api import ChatAPI, PromptGenerateAPI from chat.api.chat_authentication_api import ChatOpenAPI @@ -24,10 +24,11 @@ from common.auth import TokenAuth from common.auth.authentication import has_permissions from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants -from common.log.log import log +from common.log.log import log, _get_ip_address from common.result import result from common.utils.common import query_params_to_single_dict + def get_application_operation_object(application_id): application_model = QuerySet(model=Application).filter(id=application_id).first() if application_model is not None: @@ -36,6 +37,7 @@ def get_application_operation_object(application_id): } return {} + class ApplicationChat(APIView): authentication_classes = [TokenAuth] @@ -132,9 +134,14 @@ class OpenView(APIView): CompareConstants.AND), RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def get(self, request: Request, workspace_id: str, application_id: str): + ip_address = _get_ip_address(request) return result.success(OpenChatSerializers( data={'workspace_id': workspace_id, 'application_id': application_id, 'chat_user_id': str(uuid.uuid7()), 'chat_user_type': ChatUserType.ANONYMOUS_USER, + 'ip_address': ip_address, + 'source': { + 'type': ChatSourceChoices.ONLINE.value} + , 'debug': True}).open()) @@ -154,6 +161,7 @@ class ChatView(APIView): def post(self, request: Request, chat_id: str): return DebugChatSerializers(data={'chat_id': chat_id}).chat(request.data) + class PromptGenerateView(APIView): authentication_classes = [TokenAuth] @@ -175,5 +183,73 @@ class PromptGenerateView(APIView): RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) @log(menu='Application', operate='Generate prompt', get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id'))) - def post(self, request: Request, workspace_id: str, model_id:str, application_id: str): - return PromptGenerateSerializer(data={'workspace_id': workspace_id, 'model_id': model_id, 'application_id': application_id}).generate_prompt(instance=request.data) \ No newline at end of file + def post(self, request: Request, workspace_id: str, model_id: str, application_id: str): + return PromptGenerateSerializer(data={'workspace_id': workspace_id, 'model_id': model_id, + 'application_id': application_id}).generate_prompt(instance=request.data) + + +class DebugHistoricalConversation(APIView): + authentication_classes = [TokenAuth] + + class PageView(APIView): + authentication_classes = [TokenAuth] + + def get(self, request: Request, workspace_id: str, application_id: str, current_page: int, page_size: int): + from chat.serializers.chat_record import HistoricalConversationSerializer, page_search, HistoryChatModel + from django.db.models import QuerySet + from application.models import Chat + + queryset = QuerySet(Chat).filter( + application_id=application_id, + is_deleted=False + ).order_by('-update_time', 'id') + + return result.success( + page_search(current_page, page_size, queryset, lambda r: HistoryChatModel(r).data) + ) + + class RecordPageView(APIView): + authentication_classes = [TokenAuth] + + def get(self, request: Request, workspace_id: str, application_id: str, chat_id: str, current_page: int, + page_size: int): + from chat.serializers.chat_record import HistoricalConversationRecordSerializer + + serializer = HistoricalConversationRecordSerializer( + data={ + 'application_id': application_id, + 'chat_id': chat_id, + 'chat_user_id': str(uuid.uuid7()), + } + ) + try: + return result.success(serializer.page(current_page, page_size)) + except Exception: + from django.db.models import QuerySet + from application.models import ChatRecord + from common.db.search import page_search + from application.serializers.application_chat_record import ChatRecordSerializerModel + + queryset = QuerySet(ChatRecord).filter( + chat_id=chat_id, + ).order_by('create_time', 'id') + + return result.success( + page_search(current_page, page_size, queryset, lambda r: ChatRecordSerializerModel(r).data) + ) + + class Operate(APIView): + authentication_classes = [TokenAuth] + + def delete(self, request: Request, workspace_id: str, application_id: str, chat_id: str): + from django.db.models import QuerySet + from application.models import Chat + QuerySet(Chat).filter(id=chat_id, application_id=application_id).update(is_deleted=True) + return result.success(True) + + def put(self, request: Request, workspace_id: str, application_id: str, chat_id: str): + from django.db.models import QuerySet + from application.models import Chat + abstract = request.data.get('abstract', '') + QuerySet(Chat).filter(id=chat_id, application_id=application_id).update(abstract=abstract) + return result.success(True) diff --git a/apps/chat/serializers/chat.py b/apps/chat/serializers/chat.py index 94479bff669..898190cf214 100644 --- a/apps/chat/serializers/chat.py +++ b/apps/chat/serializers/chat.py @@ -745,9 +745,11 @@ def open_work_flow(self, application): source = self.data.get("source") debug = self.data.get("debug") chat_id = str(uuid.uuid7()) - ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], - [], - application_id, debug).set_cache() + chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, [], + [], + application_id, debug) + chat_info.save_chat() + chat_info.set_cache() return chat_id def open_simple(self, application): @@ -763,13 +765,15 @@ def open_simple(self, application): target_type='KNOWLEDGE')] chat_id = str(uuid.uuid7()) - ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, knowledge_id_list, - [str(document.id) for document in - QuerySet(Document).filter( - knowledge_id__in=knowledge_id_list, - is_active=False)], - application_id, - debug=debug).set_cache() + chat_info = ChatInfo(chat_id, chat_user_id, chat_user_type, ip_address, source, knowledge_id_list, + [str(document.id) for document in + QuerySet(Document).filter( + knowledge_id__in=knowledge_id_list, + is_active=False)], + application_id, + debug=debug) + chat_info.save_chat() + chat_info.set_cache() return chat_id diff --git a/ui/src/api/application/chat-log.ts b/ui/src/api/application/chat-log.ts index 2c14a48da4a..ba6042cf514 100644 --- a/ui/src/api/application/chat-log.ts +++ b/ui/src/api/application/chat-log.ts @@ -43,7 +43,7 @@ const postChatLogAddKnowledge: ( } */ const getChatLog: ( - application_id: String, + application_id: string, page: pageRequest, param: any, loading?: Ref, @@ -61,8 +61,8 @@ const getChatLog: ( * application_id, chart_id,order_asc */ const getChatRecordLog: ( - application_id: String, - chart_id: String, + application_id: string, + chart_id: string, page: pageRequest, loading?: Ref, order_asc?: boolean, @@ -108,11 +108,11 @@ const getMarkChatRecord: ( } */ const putChatRecordLog: ( - application_id: String, - chart_id: String, - chart_record_id: String, - knowledge_id: String, - document_id: String, + application_id: string, + chart_id: string, + chart_record_id: string, + knowledge_id: string, + document_id: string, data: any, loading?: Ref, ) => Promise> = ( @@ -138,12 +138,12 @@ const putChatRecordLog: ( * application_id, chart_id, chart_record_id, knowledge_id, document_id,paragraph_id */ const delMarkChatRecord: ( - application_id: String, - chart_id: String, - chart_record_id: String, - knowledge_id: String, - document_id: String, - paragraph_id: String, + application_id: string, + chart_id: string, + chart_record_id: string, + knowledge_id: string, + document_id: string, + paragraph_id: string, loading?: Ref, ) => Promise> = ( application_id, diff --git a/ui/src/api/system-resource-management/chat-log.ts b/ui/src/api/system-resource-management/chat-log.ts index 1200ab2fe19..adbf0133953 100644 --- a/ui/src/api/system-resource-management/chat-log.ts +++ b/ui/src/api/system-resource-management/chat-log.ts @@ -30,7 +30,7 @@ const postChatLogAddKnowledge: ( } */ const getChatLog: ( - application_id: String, + application_id: string, page: pageRequest, param: any, loading?: Ref, @@ -48,8 +48,8 @@ const getChatLog: ( * application_id, chart_id,order_asc */ const getChatRecordLog: ( - application_id: String, - chart_id: String, + application_id: string, + chart_id: string, page: pageRequest, loading?: Ref, order_asc?: boolean, @@ -90,11 +90,11 @@ const getMarkChatRecord: ( } */ const putChatRecordLog: ( - application_id: String, - chart_id: String, - chart_record_id: String, - knowledge_id: String, - document_id: String, + application_id: string, + chart_id: string, + chart_record_id: string, + knowledge_id: string, + document_id: string, data: any, loading?: Ref, ) => Promise> = ( @@ -120,12 +120,12 @@ const putChatRecordLog: ( * application_id, chart_id, chart_record_id, knowledge_id, document_id,paragraph_id */ const delMarkChatRecord: ( - application_id: String, - chart_id: String, - chart_record_id: String, - knowledge_id: String, - document_id: String, - paragraph_id: String, + application_id: string, + chart_id: string, + chart_record_id: string, + knowledge_id: string, + document_id: string, + paragraph_id: string, loading?: Ref, ) => Promise> = ( application_id, diff --git a/ui/src/api/system-shared/tool.ts b/ui/src/api/system-shared/tool.ts index c8a12eada9e..dcc1d1f7cc0 100644 --- a/ui/src/api/system-shared/tool.ts +++ b/ui/src/api/system-shared/tool.ts @@ -96,7 +96,7 @@ const getToolById: (tool_id: string, loading?: Ref) => Promise) => Promise> = ( +const delTool: (tool_id: string, loading?: Ref) => Promise> = ( tool_id, loading, ) => { diff --git a/ui/src/components/conversation/api/index.ts b/ui/src/components/conversation/api/index.ts index 382b6bc97bb..a2adae0ffbd 100644 --- a/ui/src/components/conversation/api/index.ts +++ b/ui/src/components/conversation/api/index.ts @@ -1,5 +1,5 @@ -import { get as adminGet, postStream as adminPostStream, del as adminDel, put as adminPut } from '@/request/index' -import { get as chatGet, postStream as chatPostStream, del as chatDel, put as chatPut } from '@/request/chat/index' +import { get as adminGet, postStream as adminPostStream, del as adminDel, put as adminPut, post as adminPost } from '@/request/index' +import { get as chatGet, postStream as chatPostStream, del as chatDel, put as chatPut, post as chatPost } from '@/request/chat/index' import useStore from '@/stores' const adminPrefix = (window.MaxKB?.prefix || '/admin') + '/api' @@ -14,17 +14,52 @@ export const debugApi = { chat: (chatId: string, data: any) => adminPostStream(`${adminPrefix}/chat_message/${chatId}`, data), - history: (page: number, size: number) => - adminGet(`/historical_conversation/${page}/${size}`), + history: (page: number, size: number, applicationId?: string) => { + const { user } = useStore() + const wsId = user.getWorkspaceId() + if (applicationId) { + return adminGet(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${page}/${size}`) + } + return adminGet(`/workspace/${wsId}/historical_conversation/${page}/${size}`) + }, - records: (chatId: string, page: number, size: number) => - adminGet(`/historical_conversation_record/${chatId}/${page}/${size}`), + records: (chatId: string, page: number, size: number, applicationId?: string) => { + const { user } = useStore() + const wsId = user.getWorkspaceId() + if (applicationId) { + return adminGet(`/workspace/${wsId}/application/${applicationId}/historical_conversation_record/${chatId}/${page}/${size}`) + } + return adminGet(`/workspace/${wsId}/historical_conversation_record/${chatId}/${page}/${size}`) + }, - deleteChat: (chatId: string) => - adminDel(`/historical_conversation/${chatId}`), + deleteChat: (chatId: string, applicationId?: string) => { + const { user } = useStore() + const wsId = user.getWorkspaceId() + if (applicationId) { + return adminDel(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${chatId}`) + } + return adminDel(`/workspace/${wsId}/historical_conversation/${chatId}`) + }, - modifyChat: (chatId: string, data: any) => - adminPut(`/historical_conversation/${chatId}`, data) + modifyChat: (chatId: string, data: any, applicationId?: string) => { + const { user } = useStore() + const wsId = user.getWorkspaceId() + if (applicationId) { + return adminPut(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${chatId}`, data) + } + return adminPut(`/workspace/${wsId}/historical_conversation/${chatId}`, data) + }, + + uploadFile: (file: File, chatId: string) => { + const fd = new FormData() + fd.append('file', file) + fd.append('source_id', chatId) + fd.append('source_type', 'CHAT') + return adminPost('/oss/file', fd) + }, + + speechToText: (data: any) => + adminPost(`/speech_to_text`, data), } export const chatApi = { @@ -44,7 +79,18 @@ export const chatApi = { chatDel(`/historical_conversation/${chatId}`), modifyChat: (chatId: string, data: any) => - chatPut(`/historical_conversation/${chatId}`, data) + chatPut(`/historical_conversation/${chatId}`, data), + + uploadFile: (file: File, chatId: string) => { + const fd = new FormData() + fd.append('file', file) + fd.append('source_id', chatId) + fd.append('source_type', 'CHAT') + return chatPost('/oss/file', fd) + }, + + speechToText: (data: any) => + chatPost(`/speech_to_text`, data), } export type ChatType = 'CHAT' | 'DEBUG' diff --git a/ui/src/components/conversation/chat-panel/index.vue b/ui/src/components/conversation/chat-panel/index.vue index af280128128..cb92ca3d128 100644 --- a/ui/src/components/conversation/chat-panel/index.vue +++ b/ui/src/components/conversation/chat-panel/index.vue @@ -1,208 +1,502 @@