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 @@ + + + + + +
-
+