From c4361b158a268cb3e5b68ca4366b574dda242eba Mon Sep 17 00:00:00 2001 From: tanlianwang Date: Tue, 31 Mar 2026 19:03:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=8F=8D=E9=A6=88=E5=8A=9F=E8=83=BD=E5=92=8C=E7=BD=AE?= =?UTF-8?q?=E4=BF=A1=E5=BA=A6=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增对话记录反馈提交功能,支持用户提交反馈内容 - 添加多语言支持包括英文、简体中文、繁体中文的反馈相关文案 - 在AI聊天界面添加反馈按钮和对话框组件 - 实现反馈类型选择和内容输入验证功能 - 添加置信度分数显示功能,在答案内容区域展示综合评分 - 更新应用发布流程,支持内部发布和公开发布的模式选择 - 优化系统资源管理中的应用访问控制逻辑 - 扩展应用聊天记录数据结构,增加反馈内容字段 - 实现反馈列表和详情展示功能,支持历史对话中的反馈查看 --- .../serializers/application_chat.py | 44 +++++- .../serializers/application_chat_record.py | 51 ++++++- apps/application/urls.py | 1 + .../views/application_chat_record.py | 24 +++ apps/chat/urls.py | 1 + apps/chat/views/chat_record.py | 19 +++ static/admin/index.html | 2 +- ui/src/api/application/chat-log.ts | 25 ++++ ui/src/api/chat/chat.ts | 10 ++ .../system-resource-management/chat-log.ts | 17 +++ ui/src/api/type/application.ts | 1 + .../component/answer-content/index.vue | 40 ++++- .../operation-button/ChatOperationButton.vue | 54 +++++++ .../operation-button/LogOperationButton.vue | 22 +++ .../component/operation-button/index.vue | 1 + ui/src/components/app-icon/index.ts | 37 +++++ ui/src/components/model-select/index.vue | 5 +- ui/src/locales/lang/en-US/ai-chat.ts | 1 + .../locales/lang/en-US/views/application.ts | 7 + ui/src/locales/lang/en-US/views/chat-log.ts | 19 ++- ui/src/locales/lang/zh-CN/ai-chat.ts | 1 + .../locales/lang/zh-CN/views/application.ts | 7 + ui/src/locales/lang/zh-CN/views/chat-log.ts | 19 ++- ui/src/locales/lang/zh-Hant/ai-chat.ts | 1 + .../locales/lang/zh-Hant/views/application.ts | 7 + ui/src/locales/lang/zh-Hant/views/chat-log.ts | 19 ++- ui/src/stores/modules/application.ts | 2 +- .../views/application/ApplicationSetting.vue | 99 ++++++++++++- ui/src/views/application/index.vue | 14 +- .../chat-log/component/FeedbackDialog.vue | 139 ++++++++++++++++++ ui/src/views/chat-log/index.vue | 66 +++++++++ .../model/component/SelectProviderDialog.vue | 2 + ui/src/views/model/index.vue | 2 + .../paragraph/component/ParagraphCard.vue | 32 +++- .../ApplicationResourceIndex.vue | 14 +- 35 files changed, 791 insertions(+), 14 deletions(-) create mode 100644 ui/src/views/chat-log/component/FeedbackDialog.vue diff --git a/apps/application/serializers/application_chat.py b/apps/application/serializers/application_chat.py index 7fda52675..9ac61205f 100644 --- a/apps/application/serializers/application_chat.py +++ b/apps/application/serializers/application_chat.py @@ -42,6 +42,10 @@ class ApplicationChatResponseSerializers(serializers.Serializer): star_num = serializers.IntegerField(required=True, label=_("Number of Likes")) trample_num = serializers.IntegerField(required=True, label=_("Number of thumbs-downs")) mark_sum = serializers.IntegerField(required=True, label=_("Number of tags")) + feedback_count = serializers.IntegerField(required=False, label=_("Feedback count")) + feedback_content = serializers.CharField(required=False, allow_null=True, allow_blank=True, + label=_("Feedback content")) + feedback_list = serializers.ListField(required=False, label=_("Feedback list")) class ApplicationChatRecordExportRequest(serializers.Serializer): @@ -136,11 +140,45 @@ class ApplicationChatQuerySerializers(serializers.Serializer): def list(self, with_valid=True): if with_valid: self.is_valid(raise_exception=True) - return native_search(self.get_query_set(), select_string=get_file_content( + result = native_search(self.get_query_set(), select_string=get_file_content( os.path.join(PROJECT_DIR, "apps", "application", 'sql', ('list_application_chat_ee.sql' if ['PE', 'EE'].__contains__( edition) else 'list_application_chat.sql'))), with_table_name=False) + return self.append_feedback_content(result) + + @staticmethod + def get_feedback_items(details): + if not isinstance(details, dict): + return [] + feedback = details.get('feedback') + if isinstance(feedback, list): + return feedback + if isinstance(feedback, dict): + items = feedback.get('items') + return items if isinstance(items, list) else [] + return [] + + @classmethod + def append_feedback_content(cls, records): + if not records: + return records + chat_id_list = [row.get('id') for row in records if row.get('id') is not None] + feedback_map = {} + chat_records = QuerySet(ChatRecord).filter(chat_id__in=chat_id_list).order_by('create_time') + for chat_record in chat_records: + feedback_items = cls.get_feedback_items(chat_record.details) + if not feedback_items: + continue + feedback_map[str(chat_record.chat_id)] = feedback_items + + for row in records: + feedback_items = feedback_map.get(str(row.get('id')), []) + latest_feedback = feedback_items[-1] if feedback_items else {} + row['feedback_count'] = len(feedback_items) + row['feedback_content'] = latest_feedback.get('content') if isinstance(latest_feedback, dict) else '' + row['feedback_list'] = feedback_items + return records @staticmethod def paragraph_list_to_string(paragraph_list): @@ -239,11 +277,13 @@ class ApplicationChatQuerySerializers(serializers.Serializer): def page(self, current_page: int, page_size: int, with_valid=True): if with_valid: self.is_valid(raise_exception=True) - return native_page_search(current_page, page_size, self.get_query_set(), select_string=get_file_content( + page = native_page_search(current_page, page_size, self.get_query_set(), select_string=get_file_content( os.path.join(PROJECT_DIR, "apps", "application", 'sql', ('list_application_chat_ee.sql' if ['PE', 'EE'].__contains__( edition) else 'list_application_chat.sql'))), with_table_name=False) + page['records'] = self.append_feedback_content(page.get('records')) + return page class ChatCountSerializer(serializers.Serializer): diff --git a/apps/application/serializers/application_chat_record.py b/apps/application/serializers/application_chat_record.py index 08f0e5e93..5da29bbc2 100644 --- a/apps/application/serializers/application_chat_record.py +++ b/apps/application/serializers/application_chat_record.py @@ -10,6 +10,7 @@ from functools import reduce from typing import Dict import uuid_utils.compat as uuid +from django.utils import timezone from django.db import transaction from django.db.models import QuerySet from django.db.models.aggregates import Max, Min @@ -45,6 +46,8 @@ class ChatRecordOperateSerializer(serializers.Serializer): workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) application_id = serializers.UUIDField(required=True, label=_("Application ID")) chat_record_id = serializers.UUIDField(required=True, label=_("Conversation record id")) + feedback_type = serializers.CharField(required=False, allow_blank=True, label=_("Feedback type")) + content = serializers.CharField(required=False, allow_blank=True, label=_("Feedback content")) def is_valid(self, *, debug=False, raise_exception=False): super().is_valid(raise_exception=True) @@ -85,6 +88,49 @@ class ChatRecordOperateSerializer(serializers.Serializer): return ApplicationChatRecordQuerySerializers.reset_chat_record( chat_record, True if debug else show_source, True if debug else show_exec) + def feedback(self, request=None, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + chat_record = self.get_chat_record() + if chat_record is None: + raise AppApiException(500, gettext('Conversation record does not exist')) + + feedback_type = self.data.get('feedback_type') + content = self.data.get('content') + + if not feedback_type: + raise AppApiException(500, gettext('Feedback type is required')) + if not content: + raise AppApiException(500, gettext('Feedback content is required')) + + feedback_items = [] + feedback_details = chat_record.details.get('feedback') + if isinstance(feedback_details, list): + feedback_items = feedback_details + elif isinstance(feedback_details, dict): + feedback_items = feedback_details.get('items') or [] + + feedback_data = { + 'feedback_type': feedback_type, + 'content': content, + 'create_time': timezone.now().isoformat() + } + chat_record.details['feedback'] = { + 'type': 'feedback-node', + 'items': [*feedback_items, feedback_data] + } + chat_record.save() + + application_access_token = QuerySet(ApplicationAccessToken).filter( + application_id=self.data.get('application_id')).first() + show_source = False + show_exec = False + if application_access_token is not None: + show_exec = application_access_token.show_exec + show_source = application_access_token.show_source + return ApplicationChatRecordQuerySerializers.reset_chat_record( + chat_record, show_source, show_exec) + class ApplicationChatRecordQuerySerializers(serializers.Serializer): workspace_id = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_("Workspace ID")) @@ -114,12 +160,14 @@ class ApplicationChatRecordQuerySerializers(serializers.Serializer): def reset_chat_record(chat_record, show_source, show_exec): knowledge_list = [] paragraph_list = [] - if 'search_step' in chat_record.details and chat_record.details.get('search_step').get( + if 'search_step' in chat_record.details and isinstance(chat_record.details.get('search_step'), dict) and chat_record.details.get('search_step').get( 'paragraph_list') is not None: paragraph_list = chat_record.details.get('search_step').get( 'paragraph_list') for item in chat_record.details.values(): + if not isinstance(item, dict): + continue if item.get('type') == 'search-knowledge-node' and item.get('show_knowledge', False): paragraph_list = paragraph_list + (item.get( 'paragraph_list') or []) @@ -151,6 +199,7 @@ class ApplicationChatRecordQuerySerializers(serializers.Serializer): show_source_dict = {'knowledge_list': knowledge_list, 'paragraph_list': paragraph_list, } show_exec_dict = {'execution_details': [chat_record.details[key] for key in chat_record.details if + isinstance(chat_record.details[key], dict) and chat_record.details[key].get('type') != 'feedback-node' and (True if show_exec else chat_record.details[key].get( 'type') == 'start-node')]} return { diff --git a/apps/application/urls.py b/apps/application/urls.py index 34ded9fe0..335a8a995 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ path('workspace//application//chat//', views.ApplicationChat.Page.as_view()), path('workspace//application//chat//chat_record', views.ApplicationChatRecord.as_view()), path('workspace//application//chat//chat_record/', views.ApplicationChatRecordOperateAPI.as_view()), + path('workspace//application//chat//chat_record//feedback', views.ApplicationChatRecordOperateAPI.as_view()), path('workspace//application//chat//chat_record//', views.ApplicationChatRecord.Page.as_view()), path('workspace//application//chat//chat_record//improve', views.ApplicationChatRecordImprove.as_view()), path('workspace//application//chat//chat_record//knowledge//document//improve', views.ApplicationChatRecordImproveParagraph.as_view()), diff --git a/apps/application/views/application_chat_record.py b/apps/application/views/application_chat_record.py index 0d59146b2..cd085b5ef 100644 --- a/apps/application/views/application_chat_record.py +++ b/apps/application/views/application_chat_record.py @@ -107,6 +107,30 @@ class ApplicationChatRecordOperateAPI(APIView): 'chat_id': chat_id, 'chat_record_id': chat_record_id}).one(True)) + @extend_schema( + methods=['POST'], + description=_('Submit conversation feedback'), + summary=_('Submit conversation feedback'), + operation_id=_('Submit conversation feedback'), # type: ignore + tags=[_('Application/Conversation Log')] # type: ignore + ) + @has_permissions(PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_application_permission(), + PermissionConstants.APPLICATION_CHAT_LOG_READ.get_workspace_permission_workspace_manage_role(), + PermissionConstants.APPLICATION_READ.get_workspace_application_permission(), + PermissionConstants.APPLICATION_READ.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) + def post(self, request: Request, workspace_id: str, application_id: str, chat_id: str, chat_record_id: str): + return result.success(ChatRecordOperateSerializer( + data={ + 'workspace_id': workspace_id, + 'application_id': application_id, + 'chat_id': chat_id, + 'chat_record_id': chat_record_id, + **request.data}).feedback(request=request)) + class ApplicationChatRecordAddKnowledge(APIView): authentication_classes = [TokenAuth] diff --git a/apps/chat/urls.py b/apps/chat/urls.py index 2ba61084f..ad27635bf 100644 --- a/apps/chat/urls.py +++ b/apps/chat/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ name='application/chat_completions'), path('vote/chat//chat_record/', views.VoteView.as_view(), name='vote'), path('historical_conversation', views.HistoricalConversationView.as_view(), name='historical_conversation'), + path('historical_conversation//record//feedback', views.ChatRecordView.as_view(), name='conversation_feedback'), path('historical_conversation//record/',views.ChatRecordView.as_view(),name='conversation_details'), path('historical_conversation//', views.HistoricalConversationView.PageView.as_view(), name='historical_conversation'), path('historical_conversation/clear',views.HistoricalConversationView.BatchDelete.as_view(), name='historical_conversation_clear'), diff --git a/apps/chat/views/chat_record.py b/apps/chat/views/chat_record.py index a2d80dcce..12d3f9bb8 100644 --- a/apps/chat/views/chat_record.py +++ b/apps/chat/views/chat_record.py @@ -198,3 +198,22 @@ class ChatRecordView(APIView): 'application_id': request.auth.application_id, 'chat_user_id': request.auth.chat_user_id, }).one(False)) + + @extend_schema( + methods=['POST'], + description=_("Submit conversation feedback"), + summary=_("Submit conversation feedback"), + operation_id=_("Submit conversation feedback"), # type: ignore + parameters=PageHistoricalConversationRecordAPI.get_parameters(), + responses=PageHistoricalConversationRecordAPI.get_response(), + tags=[_('Chat')] # type: ignore + ) + def post(self, request: Request, chat_id: str, chat_record_id: str): + return result.success(ChatRecordOperateSerializer( + data={ + 'chat_id': chat_id, + 'chat_record_id': chat_record_id, + 'application_id': request.auth.application_id, + 'chat_user_id': request.auth.chat_user_id, + **request.data + }).feedback()) diff --git a/static/admin/index.html b/static/admin/index.html index 0f61d58d3..596243c4a 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -3,4 +3,4 @@ prefix: '/admin', chatPrefix: '/chat', } - })()
\ No newline at end of file + })()
\ No newline at end of file diff --git a/ui/src/api/application/chat-log.ts b/ui/src/api/application/chat-log.ts index ba6042cf5..8ef016ea8 100644 --- a/ui/src/api/application/chat-log.ts +++ b/ui/src/api/application/chat-log.ts @@ -198,6 +198,30 @@ const getChatRecordDetails: ( loading, ) } + +/** + * 提交对话记录反馈 + * @param application_id + * @param chat_id + * @param chat_record_id + * @param data + * @param loading + */ +const postChatRecordFeedback: ( + application_id: string, + chat_id: string, + chat_record_id: string, + data: any, + loading?: Ref, +) => Promise> = (application_id, chat_id, chat_record_id, data, loading) => { + return post( + `${prefix.value}/${application_id}/chat/${chat_id}/chat_record/${chat_record_id}/feedback`, + data, + undefined, + loading, + ) +} + export default { postChatLogAddKnowledge, getChatLog, @@ -207,4 +231,5 @@ export default { delMarkChatRecord, postExportChatLog, getChatRecordDetails, + postChatRecordFeedback, } diff --git a/ui/src/api/chat/chat.ts b/ui/src/api/chat/chat.ts index dde551501..092de45ac 100644 --- a/ui/src/api/chat/chat.ts +++ b/ui/src/api/chat/chat.ts @@ -250,6 +250,15 @@ const getChatRecord: ( ) => Promise> = (chat_id, chat_record_id, loading) => { return get(`historical_conversation/${chat_id}/record/${chat_record_id}`, {}, loading) } + +const postChatRecordFeedback: ( + chat_id: string, + chat_record_id: string, + data: any, + loading?: Ref, +) => Promise> = (chat_id, chat_record_id, data, loading) => { + return post(`historical_conversation/${chat_id}/record/${chat_record_id}/feedback`, data, undefined, loading) +} /** * 文本转语音 */ @@ -356,6 +365,7 @@ export default { resetCurrentPassword, getChatUserProfile, getChatRecord, + postChatRecordFeedback, textToSpeech, speechToText, deleteChat, diff --git a/ui/src/api/system-resource-management/chat-log.ts b/ui/src/api/system-resource-management/chat-log.ts index adbf01339..a941aff3a 100644 --- a/ui/src/api/system-resource-management/chat-log.ts +++ b/ui/src/api/system-resource-management/chat-log.ts @@ -180,6 +180,22 @@ const getChatRecordDetails: ( loading, ) } + +const postChatRecordFeedback: ( + application_id: string, + chat_id: string, + chat_record_id: string, + data: any, + loading?: Ref, +) => Promise> = (application_id, chat_id, chat_record_id, data, loading) => { + return post( + `${prefix}/${application_id}/chat/${chat_id}/chat_record/${chat_record_id}/feedback`, + data, + undefined, + loading, + ) +} + export default { postChatLogAddKnowledge, getChatLog, @@ -189,4 +205,5 @@ export default { delMarkChatRecord, postExportChatLog, getChatRecordDetails, + postChatRecordFeedback, } diff --git a/ui/src/api/type/application.ts b/ui/src/api/type/application.ts index 4961e1002..fac3cc7d7 100644 --- a/ui/src/api/type/application.ts +++ b/ui/src/api/type/application.ts @@ -57,6 +57,7 @@ interface chatType { id: string problem_text: string answer_text: string + comprehensive_score?: number | string | null buffer: Array answer_text_list: Array< Array<{ diff --git a/ui/src/components/ai-chat/component/answer-content/index.vue b/ui/src/components/ai-chat/component/answer-content/index.vue index e2bfe8ad3..c47a581d0 100644 --- a/ui/src/components/ai-chat/component/answer-content/index.vue +++ b/ui/src/components/ai-chat/component/answer-content/index.vue @@ -54,6 +54,17 @@ +
+ {{ $t('chat.comprehensiveScore') }} + {{ formattedComprehensiveScore }} +
+ diff --git a/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue b/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue index 209fdb614..5aff435dd 100644 --- a/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue +++ b/ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue @@ -93,7 +93,15 @@ + + + + +
@@ -101,12 +109,14 @@ + diff --git a/ui/src/views/chat-log/index.vue b/ui/src/views/chat-log/index.vue index 0d6b89e70..c484af832 100644 --- a/ui/src/views/chat-log/index.vue +++ b/ui/src/views/chat-log/index.vue @@ -163,6 +163,14 @@ + + + + + +
-
+