diff --git a/apps/application/flow/step_node/form_node/impl/base_form_node.py b/apps/application/flow/step_node/form_node/impl/base_form_node.py index dcf35dd3c..cedc6c8fe 100644 --- a/apps/application/flow/step_node/form_node/impl/base_form_node.py +++ b/apps/application/flow/step_node/form_node/impl/base_form_node.py @@ -17,6 +17,19 @@ from application.flow.i_step_node import NodeResult from application.flow.step_node.form_node.i_form_node import IFormNode +def get_default_option(option_list, _type, value_field): + if option_list is not None and len(option_list) > 0: + default_value_list = [o.get(value_field) for o in option_list if o.get('default')] + if len(default_value_list) == 0: + return option_list[0].get(value_field) + else: + if _type == 'MultiSelect': + return default_value_list + else: + return default_value_list[0] + return [] + + def write_context(step_variable: Dict, global_variable: Dict, node, workflow): if step_variable is not None: for key in step_variable: @@ -44,6 +57,30 @@ class BaseFormNode(IFormNode): for key in form_data: self.context[key] = form_data[key] + def reset_field(self, field): + if ['SingleSelect', 'MultiSelect', 'RadioCard'].__contains__(field.get('input_type')): + if field.get('assignment_method') == 'ref_variables': + option_list = self.workflow_manage.get_reference_field(field.get('option_list')[0], + field.get('option_list')[1:]) + field['option_list'] = option_list + field['default_value'] = get_default_option(option_list, field.get('input_type'), + field.get('value_field')) + + reset_field = ['field', 'label', 'default_value'] + for f in reset_field: + _value = field[f] + if _value is None: + continue + if isinstance(_value, str): + field[f] = self.workflow_manage.generate_prompt(_value) + elif f == 'label': + _label_value = _value.get('label') + _value['label'] = self.workflow_manage.generate_prompt(_label_value) + tooltip = _value.get('attrs').get('tooltip') + if tooltip is not None: + _value.get('attrs')['tooltip'] = self.workflow_manage.generate_prompt(tooltip) + return field + def execute(self, form_field_list, form_content_format, form_data, **kwargs) -> NodeResult: if form_data is not None: self.context['is_submit'] = True @@ -52,6 +89,7 @@ class BaseFormNode(IFormNode): self.context[key] = form_data.get(key) else: self.context['is_submit'] = False + form_field_list = [self.reset_field(field) for field in form_field_list] form_setting = {"form_field_list": form_field_list, "runtime_node_id": self.runtime_node_id, "chat_record_id": self.flow_params_serializer.data.get("chat_record_id"), "is_submit": self.context.get("is_submit", False)} @@ -60,6 +98,7 @@ class BaseFormNode(IFormNode): form_content_format = self.workflow_manage.reset_prompt(form_content_format) prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2') value = prompt_template.format(form=form, context=context) + return NodeResult( {'result': value, 'form_field_list': form_field_list, 'form_content_format': form_content_format}, {}, _write_context=write_context) diff --git a/apps/application/flow/tools.py b/apps/application/flow/tools.py index de9d4add0..60cdbb408 100644 --- a/apps/application/flow/tools.py +++ b/apps/application/flow/tools.py @@ -60,7 +60,10 @@ class Reasoning: if not self.reasoning_content_is_end: self.reasoning_content_is_end = True self.content += self.all_content - return {'content': self.all_content, 'reasoning_content': ''} + return {'content': self.all_content, + 'reasoning_content': chunk.additional_kwargs.get('reasoning_content', + '') if chunk.additional_kwargs else '' + } else: if self.reasoning_content_is_start: self.reasoning_content_chunk += chunk.content @@ -68,7 +71,9 @@ class Reasoning: self.reasoning_content_end_tag_prefix) if self.reasoning_content_is_end: self.content += chunk.content - return {'content': chunk.content, 'reasoning_content': ''} + return {'content': chunk.content, 'reasoning_content': chunk.additional_kwargs.get('reasoning_content', + '') if chunk.additional_kwargs else '' + } # 是否包含结束 if reasoning_content_end_tag_prefix_index > -1: if len(self.reasoning_content_chunk) - reasoning_content_end_tag_prefix_index >= self.reasoning_content_end_tag_len: @@ -93,7 +98,9 @@ class Reasoning: else: if self.reasoning_content_is_end: self.content += chunk.content - return {'content': chunk.content, 'reasoning_content': ''} + return {'content': chunk.content, 'reasoning_content': chunk.additional_kwargs.get('reasoning_content', + '') if chunk.additional_kwargs else '' + } else: # aaa result = {'content': '', 'reasoning_content': self.reasoning_content_chunk} diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index 4d513c346..3db6b7e07 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -170,6 +170,7 @@ class Operate(Enum): TO_CHAT = "READ+TO_CHAT" # 去对话 SETTING = "READ+SETTING" # 管理 DOWNLOAD = "READ+DOWNLOAD" # 下载 + AUTH = "READ+AUTH" class RoleGroup(Enum): @@ -335,6 +336,7 @@ Permission_Label = { Operate.DD.value: _('Dingding'), Operate.WEIXIN_PUBLIC_ACCOUNT.value: _('Weixin Public Account'), Operate.ADD_KNOWLEDGE.value: _('Add to Knowledge Base'), + Operate.AUTH.value:_('resource authorization'), Group.APPLICATION_OVERVIEW.value: _('Overview'), Group.APPLICATION_ACCESS.value: _('Application Access'), Group.APPLICATION_CHAT_USER.value: _('Dialogue users'), @@ -481,6 +483,11 @@ class PermissionConstants(Enum): parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL], resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE] ) + MODEL_RESOURCE_AUTHORIZATION = Permission( + group=Group.MODEL, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.MODEL, UserGroup.MODEL], + resource_permission_group_list=[ResourcePermissionConst.MODEL_MANGE] + ) TOOL_READ = Permission( group=Group.TOOL, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER], parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], @@ -520,6 +527,11 @@ class PermissionConstants(Enum): parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE] ) + TOOL_RESOURCE_AUTHORIZATION = Permission( + group=Group.TOOL, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], + resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE] + ) KNOWLEDGE_READ = Permission( group=Group.KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER], resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_VIEW], @@ -560,6 +572,11 @@ class PermissionConstants(Enum): resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] ) + KNOWLEDGE_RESOURCE_AUTHORIZATION = Permission( + group=Group.KNOWLEDGE, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] + ) KNOWLEDGE_DOCUMENT_READ = Permission( group=Group.KNOWLEDGE_DOCUMENT, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER], @@ -819,7 +836,11 @@ class PermissionConstants(Enum): parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE], ) - + APPLICATION_RESOURCE_AUTHORIZATION = Permission(group=Group.APPLICATION, operate=Operate.AUTH, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE], + ) APPLICATION_OVERVIEW_READ = Permission(group=Group.APPLICATION_OVERVIEW, operate=Operate.READ, role_list=[RoleConstants.ADMIN, RoleConstants.USER], parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], diff --git a/apps/common/forms/radio_button_field.py b/apps/common/forms/radio_button_field.py index aa6952303..b31572d6c 100644 --- a/apps/common/forms/radio_button_field.py +++ b/apps/common/forms/radio_button_field.py @@ -11,7 +11,7 @@ from typing import List, Dict from common.forms.base_field import BaseExecField, TriggerType -class Radio(BaseExecField): +class RadioButton(BaseExecField): """ 下拉单选 """ diff --git a/apps/common/forms/radio_card_field.py b/apps/common/forms/radio_card_field.py index b3579b84d..31c66d678 100644 --- a/apps/common/forms/radio_card_field.py +++ b/apps/common/forms/radio_card_field.py @@ -11,7 +11,7 @@ from typing import List, Dict from common.forms.base_field import BaseExecField, TriggerType -class Radio(BaseExecField): +class RadioCard(BaseExecField): """ 下拉单选 """ diff --git a/apps/knowledge/serializers/paragraph.py b/apps/knowledge/serializers/paragraph.py index 6d4e24dfb..dcc09f978 100644 --- a/apps/knowledge/serializers/paragraph.py +++ b/apps/knowledge/serializers/paragraph.py @@ -406,6 +406,15 @@ class ParagraphSerializers(serializers.Serializer): def association(self, with_valid=True, with_embedding=True): if with_valid: self.is_valid(raise_exception=True) + # 已关联则直接返回 + if QuerySet(ProblemParagraphMapping).filter( + knowledge_id=self.data.get('knowledge_id'), + document_id=self.data.get('document_id'), + paragraph_id=self.data.get('paragraph_id'), + problem_id=self.data.get('problem_id') + ).exists(): + return True + problem = QuerySet(Problem).filter(id=self.data.get("problem_id")).first() problem_paragraph_mapping = ProblemParagraphMapping(id=uuid.uuid7(), document_id=self.data.get('document_id'), diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 4f6fa10e3..63134d6df 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -8648,4 +8648,16 @@ msgid "Multiple dialects, supporting 23 dialects" msgstr "" msgid "This interface is used to recognize short audio files within 60 seconds. Supports Mandarin Chinese, English, Cantonese, Japanese, Vietnamese, Malay, Indonesian, Filipino, Thai, Portuguese, Turkish, Arabic, Hindi, French, German, and 23 Chinese dialects." +msgstr "" + +msgid "CueWord" +msgstr "" + +msgid "If not passed, the default value is What is this audio saying? Only answer the audio content" +msgstr "" + +msgid "The Qwen Omni series model supports inputting multiple modalities of data, including video, audio, images, and text, and outputting audio and text." +msgstr "" + +msgid "resource authorization" msgstr "" \ No newline at end of file diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index 62e97f258..8abdf653b 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -8774,4 +8774,16 @@ msgid "Multiple dialects, supporting 23 dialects" msgstr "多种方言,支持 23 种方言" msgid "This interface is used to recognize short audio files within 60 seconds. Supports Mandarin Chinese, English, Cantonese, Japanese, Vietnamese, Malay, Indonesian, Filipino, Thai, Portuguese, Turkish, Arabic, Hindi, French, German, and 23 Chinese dialects." -msgstr "本接口用于识别 60 秒之内的短音频文件。支持中文普通话、英语、粤语、日语、越南语、马来语、印度尼西亚语、菲律宾语、泰语、葡萄牙语、土耳其语、阿拉伯语、印地语、法语、德语及 23 种汉语方言。" \ No newline at end of file +msgstr "本接口用于识别 60 秒之内的短音频文件。支持中文普通话、英语、粤语、日语、越南语、马来语、印度尼西亚语、菲律宾语、泰语、葡萄牙语、土耳其语、阿拉伯语、印地语、法语、德语及 23 种汉语方言。" + +msgid "CueWord" +msgstr "提示词" + +msgid "If not passed, the default value is What is this audio saying? Only answer the audio content" +msgstr "如果未传递,默认值为 这段音频在说什么,只回答音频的内容" + +msgid "The Qwen Omni series model supports inputting multiple modalities of data, including video, audio, images, and text, and outputting audio and text." +msgstr "Qwen-Omni 系列模型支持输入多种模态的数据,包括视频、音频、图片、文本,并输出音频与文本" + +msgid "resource authorization" +msgstr "资源授权" \ No newline at end of file diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index 794a0f859..06ec56659 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -8774,4 +8774,16 @@ msgid "Multiple dialects, supporting 23 dialects" msgstr "多種方言,支持 23 種方言" msgid "This interface is used to recognize short audio files within 60 seconds. Supports Mandarin Chinese, English, Cantonese, Japanese, Vietnamese, Malay, Indonesian, Filipino, Thai, Portuguese, Turkish, Arabic, Hindi, French, German, and 23 Chinese dialects." -msgstr "本介面用於識別 60 秒之內的短音頻文件。支援中文普通話、英語、粵語、日語、越南語、馬來語、印度尼西亞語、菲律賓語、泰語、葡萄牙語、土耳其語、阿拉伯語、印地語、法語、德語及 23 種漢語方言。" \ No newline at end of file +msgstr "本介面用於識別 60 秒之內的短音頻文件。支援中文普通話、英語、粵語、日語、越南語、馬來語、印度尼西亞語、菲律賓語、泰語、葡萄牙語、土耳其語、阿拉伯語、印地語、法語、德語及 23 種漢語方言。" + +msgid "CueWord" +msgstr "提示詞" + +msgid "If not passed, the default value is What is this audio saying? Only answer the audio content" +msgstr "如果未傳遞,預設值為這段音訊在說什麼,只回答音訊的內容" + +msgid "The Qwen Omni series model supports inputting multiple modalities of data, including video, audio, images, and text, and outputting audio and text." +msgstr "Qwen-Omni系列模型支持輸入多種模態的數據,包括視頻、音訊、圖片、文字,並輸出音訊與文字" + +msgid "resource authorization" +msgstr "資源授權" \ No newline at end of file diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/aliyun_bai_lian_model_provider.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/aliyun_bai_lian_model_provider.py index 6f27129be..7264001b2 100644 --- a/apps/models_provider/impl/aliyun_bai_lian_model_provider/aliyun_bai_lian_model_provider.py +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/aliyun_bai_lian_model_provider.py @@ -15,6 +15,7 @@ from models_provider.impl.aliyun_bai_lian_model_provider.credential.embedding im AliyunBaiLianEmbeddingCredential from models_provider.impl.aliyun_bai_lian_model_provider.credential.image import QwenVLModelCredential from models_provider.impl.aliyun_bai_lian_model_provider.credential.llm import BaiLianLLMModelCredential +from models_provider.impl.aliyun_bai_lian_model_provider.credential.omni_stt import AliyunBaiLianOmiSTTModelCredential from models_provider.impl.aliyun_bai_lian_model_provider.credential.reranker import \ AliyunBaiLianRerankerCredential from models_provider.impl.aliyun_bai_lian_model_provider.credential.stt import AliyunBaiLianSTTModelCredential @@ -23,6 +24,7 @@ from models_provider.impl.aliyun_bai_lian_model_provider.credential.tts import A from models_provider.impl.aliyun_bai_lian_model_provider.model.embedding import AliyunBaiLianEmbedding from models_provider.impl.aliyun_bai_lian_model_provider.model.image import QwenVLChatModel from models_provider.impl.aliyun_bai_lian_model_provider.model.llm import BaiLianChatModel +from models_provider.impl.aliyun_bai_lian_model_provider.model.omni_stt import AliyunBaiLianOmiSpeechToText from models_provider.impl.aliyun_bai_lian_model_provider.model.reranker import AliyunBaiLianReranker from models_provider.impl.aliyun_bai_lian_model_provider.model.stt import AliyunBaiLianSpeechToText from models_provider.impl.aliyun_bai_lian_model_provider.model.tti import QwenTextToImageModel @@ -33,6 +35,7 @@ from django.utils.translation import gettext as _, gettext aliyun_bai_lian_model_credential = AliyunBaiLianRerankerCredential() aliyun_bai_lian_tts_model_credential = AliyunBaiLianTTSModelCredential() aliyun_bai_lian_stt_model_credential = AliyunBaiLianSTTModelCredential() +aliyun_bai_lian_omi_stt_model_credential = AliyunBaiLianOmiSTTModelCredential() aliyun_bai_lian_embedding_model_credential = AliyunBaiLianEmbeddingCredential() aliyun_bai_lian_llm_model_credential = BaiLianLLMModelCredential() qwenvl_model_credential = QwenVLModelCredential() @@ -73,7 +76,13 @@ model_info_list = [ModelInfo('gte-rerank', ModelInfo('qwen-plus', '', ModelTypeConst.LLM, aliyun_bai_lian_llm_model_credential, BaiLianChatModel), ModelInfo('qwen-max', '', ModelTypeConst.LLM, aliyun_bai_lian_llm_model_credential, - BaiLianChatModel) + BaiLianChatModel), + ModelInfo('qwen-omni-turbo', + _('The Qwen Omni series model supports inputting multiple modalities of data, including video, audio, images, and text, and outputting audio and text.'), + ModelTypeConst.STT, aliyun_bai_lian_omi_stt_model_credential, AliyunBaiLianOmiSpeechToText), + ModelInfo('qwen2.5-omni-7b', + _('The Qwen Omni series model supports inputting multiple modalities of data, including video, audio, images, and text, and outputting audio and text.'), + ModelTypeConst.STT, aliyun_bai_lian_omi_stt_model_credential, AliyunBaiLianOmiSpeechToText), ] module_info_vl_list = [ diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/omni_stt.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/omni_stt.py new file mode 100644 index 000000000..82dcb4b55 --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/omni_stt.py @@ -0,0 +1,73 @@ +# coding=utf-8 +import traceback +from typing import Dict, Any + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, PasswordInputField, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext as _ + +class AliyunBaiLianOmiSTTModelParams(BaseForm): + CueWord = forms.TextInputField( + TooltipLabel(_('CueWord'), _('If not passed, the default value is What is this audio saying? Only answer the audio content')), + required=True, + default_value='这段音频在说什么,只回答音频的内容', + ) + + +class AliyunBaiLianOmiSTTModelCredential(BaseForm, BaseModelCredential): + api_url = forms.TextInputField(_('API URL'), required=True) + api_key = forms.PasswordInputField(_('API Key'), required=True) + + def is_valid(self, + model_type: str, + model_name: str, + model_credential: Dict[str, Any], + model_params: Dict[str, Any], + provider, + raise_exception: bool = False + ) -> bool: + + model_type_list = provider.get_model_type_list() + if not any(mt.get('value') == model_type for mt in model_type_list): + raise AppApiException( + ValidCode.valid_error.value, + _('{model_type} Model type is not supported').format(model_type=model_type) + ) + + required_keys = ['api_key'] + for key in required_keys: + if key not in model_credential: + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + _('{key} is required').format(key=key) + ) + return False + + try: + model = provider.get_model(model_type, model_name, model_credential) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + _('Verification failed, please check whether the parameters are correct: {error}').format(error=str(e)) + ) + return False + return True + + def encryption_dict(self, model: Dict[str, object]) -> Dict[str, object]: + + return { + **model, + 'api_key': super().encryption(model.get('api_key', '')) + } + + + def get_model_params_setting_form(self, model_name): + + return AliyunBaiLianOmiSTTModelParams() diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/omni_stt.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/omni_stt.py new file mode 100644 index 000000000..56e060f6b --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/omni_stt.py @@ -0,0 +1,89 @@ +import base64 +import os +import traceback +from typing import Dict + +from openai import OpenAI + +from common.utils.logger import maxkb_logger +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + + +class AliyunBaiLianOmiSpeechToText(MaxKBBaseModel, BaseSpeechToText): + api_key: str + api_url: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + self.api_url = kwargs.get('api_url') + + @staticmethod + def is_cache_model(): + return False + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return AliyunBaiLianOmiSpeechToText( + model=model_name, + api_key=model_credential.get('api_key'), + api_url=model_credential.get('api_url') , + params= model_kwargs, + **model_kwargs + ) + + + def check_auth(self): + cwd = os.path.dirname(os.path.abspath(__file__)) + with open(f'{cwd}/iat_mp3_16k.mp3', 'rb') as audio_file: + self.speech_to_text(audio_file) + + + + def speech_to_text(self, audio_file): + try: + client = OpenAI( + # 若没有配置环境变量,请用阿里云百炼API Key将下行替换为:api_key="sk-xxx", + api_key=self.api_key, + base_url=self.api_url, + ) + + base64_audio = base64.b64encode(audio_file.read()).decode("utf-8") + + completion = client.chat.completions.create( + model=self.model, + messages=[ + { + "role": "user", + "content": [ + { + "type": "input_audio", + "input_audio": { + "data": f"data:;base64,{base64_audio}", + "format": "mp3", + }, + }, + {"type": "text", "text": self.params.get('CueWord')}, + ], + }, + ], + # 设置输出数据的模态,当前支持两种:["text","audio"]、["text"] + modalities=["text"], + # stream 必须设置为 True,否则会报错 + stream=True, + stream_options={"include_usage": True}, + ) + result = [] + for chunk in completion: + if chunk.choices and hasattr(chunk.choices[0].delta, 'content'): + content = chunk.choices[0].delta.content + result.append(content) + return "".join(result) + + except Exception as err: + maxkb_logger.error(f":Error: {str(err)}: {traceback.format_exc()}") diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/stt.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/stt.py index b5c9f240d..7017caf79 100644 --- a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/stt.py +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/stt.py @@ -30,8 +30,6 @@ class AliyunBaiLianSpeechToText(MaxKBBaseModel, BaseSpeechToText): optional_params['max_tokens'] = model_kwargs['max_tokens'] if 'temperature' in model_kwargs and model_kwargs['temperature'] is not None: optional_params['temperature'] = model_kwargs['temperature'] - if model_name == 'qwen-omni-turbo': - optional_params['streaming'] = True return AliyunBaiLianSpeechToText( model=model_name, api_key=model_credential.get('api_key'), diff --git a/apps/models_provider/impl/tencent_model_provider/model/stt.py b/apps/models_provider/impl/tencent_model_provider/model/stt.py index 3b5f050b0..a501fed19 100644 --- a/apps/models_provider/impl/tencent_model_provider/model/stt.py +++ b/apps/models_provider/impl/tencent_model_provider/model/stt.py @@ -1,6 +1,7 @@ import base64 import json import os +import traceback from typing import Dict from tencentcloud.asr.v20190614 import asr_client, models @@ -9,6 +10,7 @@ from tencentcloud.common.exception import TencentCloudSDKException from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile +from common.utils.logger import maxkb_logger from models_provider.base_model_provider import MaxKBBaseModel from models_provider.impl.base_stt import BaseSpeechToText diff --git a/apps/models_provider/impl/wenxin_model_provider/credential/llm.py b/apps/models_provider/impl/wenxin_model_provider/credential/llm.py index bb458fb6f..4c06ee3b4 100644 --- a/apps/models_provider/impl/wenxin_model_provider/credential/llm.py +++ b/apps/models_provider/impl/wenxin_model_provider/credential/llm.py @@ -40,16 +40,23 @@ class WenxinLLMModelParams(BaseForm): class WenxinLLMModelCredential(BaseForm, BaseModelCredential): def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, raise_exception=False): - model_type_list = provider.get_model_type_list() - if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))): - raise AppApiException(ValidCode.valid_error.value, - gettext('{model_type} Model type is not supported').format(model_type=model_type)) + # 根据api_version检查必需字段 + api_version = model_credential.get('api_version', 'v1') model = provider.get_model(model_type, model_name, model_credential, **model_params) - model_info = [model.lower() for model in model.client.models()] - if not model_info.__contains__(model_name.lower()): - raise AppApiException(ValidCode.valid_error.value, - gettext('{model_name} The model does not support').format(model_name=model_name)) - for key in ['api_key', 'secret_key']: + if api_version == 'v1': + model_type_list = provider.get_model_type_list() + if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))): + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + model_info = [model.lower() for model in model.client.models()] + if not model_info.__contains__(model_name.lower()): + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_name} The model does not support').format(model_name=model_name)) + required_keys = ['api_key', 'secret_key'] + if api_version == 'v2': + required_keys = ['api_base', 'api_key'] + + for key in required_keys: if key not in model_credential: if raise_exception: raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) @@ -64,19 +71,47 @@ class WenxinLLMModelCredential(BaseForm, BaseModelCredential): return True def encryption_dict(self, model_info: Dict[str, object]): - return {**model_info, 'secret_key': super().encryption(model_info.get('secret_key', ''))} + # 根据api_version加密不同字段 + api_version = model_info.get('api_version', 'v1') + if api_version == 'v1': + return {**model_info, 'secret_key': super().encryption(model_info.get('secret_key', ''))} + else: # v2 + return {**model_info, 'api_key': super().encryption(model_info.get('api_key', ''))} def build_model(self, model_info: Dict[str, object]): - for key in ['api_key', 'secret_key', 'model']: - if key not in model_info: - raise AppApiException(500, gettext('{key} is required').format(key=key)) - self.api_key = model_info.get('api_key') - self.secret_key = model_info.get('secret_key') + api_version = model_info.get('api_version', 'v1') + # 根据api_version检查必需字段 + if api_version == 'v1': + for key in ['api_version', 'api_key', 'secret_key', 'model']: + if key not in model_info: + raise AppApiException(500, gettext('{key} is required').format(key=key)) + self.api_key = model_info.get('api_key') + self.secret_key = model_info.get('secret_key') + else: # v2 + for key in ['api_version', 'api_base', 'api_key', 'model', ]: + if key not in model_info: + raise AppApiException(500, gettext('{key} is required').format(key=key)) + self.api_base = model_info.get('api_base') + self.api_key = model_info.get('api_key') return self - api_key = forms.PasswordInputField('API Key', required=True) + # 动态字段定义 - 根据api_version显示不同字段 + api_version = forms.Radio('API Version', required=True, text_field='label', value_field='value', + option_list=[ + {'label': 'v1', 'value': 'v1'}, + {'label': 'v2', 'value': 'v2'} + ], + default_value='v1', + provider='', + method='', ) - secret_key = forms.PasswordInputField("Secret Key", required=True) + # v2版本字段 + api_base = forms.TextInputField("API Base", required=False, relation_show_field_dict={"api_version": ["v2"]}) + + # v1版本字段 + api_key = forms.PasswordInputField('API Key', required=False) + secret_key = forms.PasswordInputField("Secret Key", required=False, + relation_show_field_dict={"api_version": ["v1"]}) def get_model_params_setting_form(self, model_name): return WenxinLLMModelParams() diff --git a/apps/models_provider/impl/wenxin_model_provider/model/llm.py b/apps/models_provider/impl/wenxin_model_provider/model/llm.py index aa79692b2..688530136 100644 --- a/apps/models_provider/impl/wenxin_model_provider/model/llm.py +++ b/apps/models_provider/impl/wenxin_model_provider/model/llm.py @@ -17,9 +17,10 @@ from langchain_core.messages import ( from langchain_core.outputs import ChatGenerationChunk from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI -class QianfanChatModel(MaxKBBaseModel, QianfanChatEndpoint): +class QianfanChatModelQianfan(MaxKBBaseModel, QianfanChatEndpoint): @staticmethod def is_cache_model(): return False @@ -27,11 +28,11 @@ class QianfanChatModel(MaxKBBaseModel, QianfanChatEndpoint): @staticmethod def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) - return QianfanChatModel(model=model_name, - qianfan_ak=model_credential.get('api_key'), - qianfan_sk=model_credential.get('secret_key'), - streaming=model_kwargs.get('streaming', False), - init_kwargs=optional_params) + return QianfanChatModelQianfan(model=model_name, + qianfan_ak=model_credential.get('api_key'), + qianfan_sk=model_credential.get('secret_key'), + streaming=model_kwargs.get('streaming', False), + init_kwargs=optional_params) usage_metadata: dict = {} @@ -74,3 +75,30 @@ class QianfanChatModel(MaxKBBaseModel, QianfanChatEndpoint): if run_manager: run_manager.on_llm_new_token(chunk.text, chunk=chunk) yield chunk + + +class QianfanChatModelOpenai(MaxKBBaseModel, BaseChatOpenAI): + @staticmethod + def is_cache_model(): + return False + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return QianfanChatModelOpenai( + model=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + extra_body=optional_params + ) + + +class QianfanChatModel(MaxKBBaseModel): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + api_version = model_credential.get('api_version', 'v1') + + if api_version == "v1": + return QianfanChatModelQianfan.new_instance(model_type, model_name, model_credential, **model_kwargs) + elif api_version == "v2": + return QianfanChatModelOpenai.new_instance(model_type, model_name, model_credential, **model_kwargs) diff --git a/apps/system_manage/views/user_resource_permission.py b/apps/system_manage/views/user_resource_permission.py index a1af437d3..2109f1dbb 100644 --- a/apps/system_manage/views/user_resource_permission.py +++ b/apps/system_manage/views/user_resource_permission.py @@ -15,7 +15,8 @@ from rest_framework.views import APIView from common import result from common.auth import TokenAuth from common.auth.authentication import has_permissions -from common.constants.permission_constants import PermissionConstants, RoleConstants, Permission, Group, Operate +from common.constants.permission_constants import RoleConstants, Permission, Group, Operate, ViewPermission, \ + CompareConstants from common.log.log import log from system_manage.api.user_resource_permission import UserResourcePermissionAPI, EditUserResourcePermissionAPI, \ ResourceUserPermissionAPI, ResourceUserPermissionPageAPI, ResourceUserPermissionEditAPI, \ @@ -89,6 +90,10 @@ class WorkSpaceUserResourcePermissionView(APIView): responses=UserResourcePermissionPageAPI.get_response(), tags=[_('Resources authorization')] # type: ignore ) + @has_permissions( + lambda r, kwargs: Permission(group=Group(kwargs.get('resource') + '_WORKSPACE_USER_RESOURCE_PERMISSION'), + operate=Operate.READ), + RoleConstants.ADMIN, RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def get(self, request: Request, workspace_id: str, user_id: str, resource: str, current_page: str, page_size: str): return result.success(UserResourcePermissionSerializer( @@ -109,6 +114,19 @@ class WorkspaceResourceUserPermissionView(APIView): responses=ResourceUserPermissionAPI.get_response(), tags=[_('Resources authorization')] # type: ignore ) + @has_permissions( + lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.AUTH, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/ROLE/WORKSPACE_MANAGE"), + lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.AUTH, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/{kwargs.get('resource')}/{kwargs.get('target')}"), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.SELF, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/{kwargs.get('resource')}/{kwargs.get('target')}")], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def get(self, request: Request, workspace_id: str, target: str, resource: str): return result.success(ResourceUserPermissionSerializer( data={'workspace_id': workspace_id, "target": target, 'auth_target_type': resource, @@ -127,6 +145,22 @@ class WorkspaceResourceUserPermissionView(APIView): responses=ResourceUserPermissionEditAPI.get_response(), tags=[_('Resources authorization')] # type: ignore ) + @log(menu='System', operate='Edit user authorization status of resource', + get_operation_object=lambda r, k: get_user_operation_object(k.get('user_id')) + ) + @has_permissions( + lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.AUTH, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/ROLE/WORKSPACE_MANAGE"), + lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.AUTH, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/{kwargs.get('resource')}/{kwargs.get('target')}"), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.SELF, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/{kwargs.get('resource')}/{kwargs.get('target')}")], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def put(self, request: Request, workspace_id: str, target: str, resource: str): return result.success(ResourceUserPermissionSerializer( data={'workspace_id': workspace_id, "target": target, 'auth_target_type': resource, }) @@ -144,6 +178,19 @@ class WorkspaceResourceUserPermissionView(APIView): responses=ResourceUserPermissionPageAPI.get_response(), tags=[_('Resources authorization')] # type: ignore ) + @has_permissions( + lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.AUTH, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/ROLE/WORKSPACE_MANAGE"), + lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.AUTH, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/{kwargs.get('resource')}/{kwargs.get('target')}"), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [lambda r, kwargs: Permission(group=Group(kwargs.get('resource')), + operate=Operate.SELF, + resource_path=f"/WORKSPACE/{kwargs.get('workspace_id')}/{kwargs.get('resource')}/{kwargs.get('target')}")], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()) def get(self, request: Request, workspace_id: str, target: str, resource: str, current_page: int, page_size: int): return result.success(ResourceUserPermissionSerializer( diff --git a/installer/Dockerfile-base b/installer/Dockerfile-base index c0c1142e5..cc9a619e3 100644 --- a/installer/Dockerfile-base +++ b/installer/Dockerfile-base @@ -1,6 +1,6 @@ -FROM python:3.11-slim-bullseye AS python-stage +FROM python:3.11-slim-bookworm AS python-stage FROM ghcr.io/1panel-dev/maxkb-vector-model:v2.0.2 AS vector-model -FROM postgres:17.5-bullseye +FROM postgres:17.6-bookworm COPY --from=python-stage /usr/local /usr/local COPY installer/*.sh /usr/bin/ COPY installer/init.sql /docker-entrypoint-initdb.d/ diff --git a/pyproject.toml b/pyproject.toml index 656286147..24d1c84ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ python-docx = "1.2.0" xlrd = "2.0.2" xlwt = "1.3.0" pymupdf = "1.26.3" -pypdf = "5.7.0" +pypdf = "6.0.0" # 音频处理 pydub = "0.25.1" diff --git a/ui/src/api/system/resource-authorization.ts b/ui/src/api/system/resource-authorization.ts index b12e72358..dc9d3b357 100644 --- a/ui/src/api/system/resource-authorization.ts +++ b/ui/src/api/system/resource-authorization.ts @@ -1,43 +1,84 @@ -import { Permission } from '@/utils/permission/type' import { Result } from '@/request/Result' import { get, put, post, del } from '@/request/index' -import type { pageRequest } from '@/api/type/common' import type { Ref } from 'vue' +import type { pageRequest } from '@/api/type/common' const prefix = '/workspace' /** - * 获取资源权限 + * 工作空间下各资源获取资源权限 + * @query 参数 + */ +const getWorkspaceResourceAuthorization: ( + workspace_id: string, + target: string, + resource: string, + page: pageRequest, + params?: any, + loading?: Ref, +) => Promise> = (workspace_id, target, resource, page, params, loading) => { + return get( + `${prefix}/${workspace_id}/resource_user_permission/resource/${target}/resource/${resource}/${page.current_page}/${page.page_size}`, + params, + loading, + ) +} + +/** + * 工作空间下各资源修改成员权限 + * @param 参数 member_id + * @param 参数 { + [ + { + "user_id": "string", + "permission": "NOT_AUTH" + } + ] + } + */ +const putWorkspaceResourceAuthorization: ( + workspace_id: string, + target: string, + resource: string, + body: any, + loading?: Ref, +) => Promise> = (workspace_id, target, resource, body, loading) => { + return put( + `${prefix}/${workspace_id}/resource_user_permission/resource/${target}/resource/${resource}`, + body, + {}, + loading, + ) +} + +/** + * 系统资源授权获取资源权限 * @query 参数 */ const getResourceAuthorization: ( workspace_id: string, user_id: string, resource: string, + page: pageRequest, + params?: any, loading?: Ref, -) => Promise> = (workspace_id, user_id, resource, loading) => { +) => Promise> = (workspace_id, user_id, resource, page, params, loading) => { return get( - `${prefix}/${workspace_id}/user_resource_permission/user/${user_id}/resource/${resource}`, - undefined, + `${prefix}/${workspace_id}/user_resource_permission/user/${user_id}/resource/${resource}/${page.current_page}/${page.page_size}`, + params, loading, ) } /** - * 修改成员权限 + * 系统资源授权修改成员权限 * @param 参数 member_id * @param 参数 { - "team_resource_permission_list": [ - { - "auth_target_type": "KNOWLEDGE", - "target_id": "string", - "auth_type": "ROLE", - "permission": { - "VIEW": true, - "MANAGE": true, - "ROLE": true - } - } - ] + [ + { + "target_id": "string", + "permission": "NOT_AUTH" + } + ] } */ const putResourceAuthorization: ( @@ -107,4 +148,6 @@ export default { getUserList, getUserMember, getSystemFolder, + getWorkspaceResourceAuthorization, + putWorkspaceResourceAuthorization } diff --git a/ui/src/components/ai-chat/index.vue b/ui/src/components/ai-chat/index.vue index 0c1b8c08b..af33fcb27 100644 --- a/ui/src/components/ai-chat/index.vue +++ b/ui/src/components/ai-chat/index.vue @@ -1,43 +1,92 @@