From 946de675ffd46eed0baa221fbef2c8d87f0181ec Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Thu, 17 Apr 2025 18:01:33 +0800 Subject: [PATCH] feat: add model setting --- apps/common/cache/file_cache.py | 86 +++ apps/common/cache/mem_cache.py | 47 ++ apps/common/config/embedding_config.py | 66 ++ apps/common/config/tokenizer_manage_config.py | 24 + apps/common/constants/permission_constants.py | 7 + apps/common/forms/__init__.py | 24 + apps/common/forms/array_object_card.py | 33 + apps/common/forms/base_field.py | 157 +++++ apps/common/forms/base_form.py | 30 + apps/common/forms/label/__init__.py | 10 + apps/common/forms/label/base_label.py | 28 + apps/common/forms/label/tooltip_label.py | 14 + apps/common/forms/multi_select.py | 38 ++ apps/common/forms/object_card.py | 33 + apps/common/forms/password_input.py | 26 + apps/common/forms/radio_button_field.py | 38 ++ apps/common/forms/radio_card_field.py | 38 ++ apps/common/forms/radio_field.py | 38 ++ apps/common/forms/single_select_field.py | 39 ++ apps/common/forms/slider_field.py | 65 ++ apps/common/forms/switch_field.py | 33 + apps/common/forms/tab_card.py | 33 + apps/common/forms/table_checkbox.py | 33 + apps/common/forms/table_radio.py | 33 + apps/common/forms/text_input_field.py | 28 + apps/common/mixins/app_model_mixin.py | 18 + apps/common/utils/common.py | 160 +++++ apps/common/utils/rsa_util.py | 140 +++++ apps/locales/zh_CN/LC_MESSAGES/django.po | 2 + apps/maxkb/settings/base.py | 3 +- apps/maxkb/urls.py | 3 +- apps/models_provider/__init__.py | 0 apps/models_provider/admin.py | 3 + apps/models_provider/api/__init__.py | 1 + apps/models_provider/api/model.py | 20 + apps/models_provider/api/provide.py | 85 +++ apps/models_provider/apps.py | 6 + apps/models_provider/base_model_provider.py | 255 ++++++++ .../constants/model_provider_constants.py | 48 ++ .../__init__.py | 8 + .../aliyun_bai_lian_model_provider.py | 100 +++ .../credential/embedding.py | 74 +++ .../credential/image.py | 71 +++ .../credential/llm.py | 94 +++ .../credential/reranker.py | 85 +++ .../credential/stt.py | 92 +++ .../credential/tti.py | 147 +++++ .../credential/tts.py | 139 +++++ .../icon/aliyun_bai_lian_icon_svg | 1 + .../model/embedding.py | 66 ++ .../model/iat_mp3_16k.mp3 | Bin 0 -> 17876 bytes .../model/image.py | 23 + .../model/llm.py | 24 + .../model/reranker.py | 20 + .../model/stt.py | 75 +++ .../model/tti.py | 59 ++ .../model/tts.py | 57 ++ .../impl/anthropic_model_provider/__init__.py | 8 + .../anthropic_model_provider.py | 62 ++ .../credential/image.py | 53 ++ .../credential/llm.py | 78 +++ .../icon/anthropic_icon_svg | 1 + .../anthropic_model_provider/model/image.py | 26 + .../anthropic_model_provider/model/llm.py | 53 ++ .../aws_bedrock_model_provider/__init__.py | 2 + .../aws_bedrock_model_provider.py | 154 +++++ .../credential/embedding.py | 53 ++ .../credential/llm.py | 76 +++ .../icon/bedrock_icon_svg | 1 + .../model/embedding.py | 60 ++ .../aws_bedrock_model_provider/model/llm.py | 88 +++ .../impl/azure_model_provider/__init__.py | 8 + .../azure_model_provider.py | 116 ++++ .../credential/embedding.py | 57 ++ .../azure_model_provider/credential/image.py | 75 +++ .../azure_model_provider/credential/llm.py | 96 +++ .../azure_model_provider/credential/stt.py | 50 ++ .../azure_model_provider/credential/tti.py | 87 +++ .../azure_model_provider/credential/tts.py | 68 ++ .../azure_model_provider/icon/azure_icon_svg | 1 + .../model/azure_chat_model.py | 51 ++ .../azure_model_provider/model/embedding.py | 25 + .../impl/azure_model_provider/model/image.py | 42 ++ .../impl/azure_model_provider/model/stt.py | 62 ++ .../impl/azure_model_provider/model/tti.py | 61 ++ .../impl/azure_model_provider/model/tts.py | 69 +++ .../models_provider/impl/base_chat_open_ai.py | 168 +++++ apps/models_provider/impl/base_stt.py | 14 + apps/models_provider/impl/base_tti.py | 14 + apps/models_provider/impl/base_tts.py | 14 + .../impl/deepseek_model_provider/__init__.py | 8 + .../deepseek_model_provider/credential/llm.py | 77 +++ .../deepseek_model_provider.py | 47 ++ .../icon/deepseek_icon_svg | 6 + .../impl/deepseek_model_provider/model/llm.py | 31 + .../impl/gemini_model_provider/__init__.py | 8 + .../credential/embedding.py | 52 ++ .../gemini_model_provider/credential/image.py | 71 +++ .../gemini_model_provider/credential/llm.py | 78 +++ .../gemini_model_provider/credential/stt.py | 48 ++ .../gemini_model_provider.py | 97 +++ .../icon/gemini_icon_svg | 2 + .../gemini_model_provider/model/embedding.py | 22 + .../impl/gemini_model_provider/model/image.py | 24 + .../impl/gemini_model_provider/model/llm.py | 90 +++ .../impl/gemini_model_provider/model/stt.py | 57 ++ .../impl/kimi_model_provider/__init__.py | 8 + .../kimi_model_provider/credential/llm.py | 77 +++ .../kimi_model_provider/icon/kimi_icon_svg | 1 + .../kimi_model_provider.py | 42 ++ .../impl/kimi_model_provider/model/llm.py | 31 + .../impl/local_model_provider/__init__.py | 8 + .../credential/embedding.py | 53 ++ .../credential/reranker.py | 54 ++ .../local_model_provider/icon/local_icon_svg | 1 + .../local_model_provider.py | 41 ++ .../local_model_provider/model/embedding.py | 62 ++ .../local_model_provider/model/reranker.py | 101 +++ .../impl/ollama_model_provider/__init__.py | 8 + .../credential/embedding.py | 49 ++ .../ollama_model_provider/credential/image.py | 56 ++ .../ollama_model_provider/credential/llm.py | 70 +++ .../credential/reranker.py | 66 ++ .../icon/ollama_icon_svg | 3 + .../ollama_model_provider/model/embedding.py | 48 ++ .../impl/ollama_model_provider/model/image.py | 32 + .../impl/ollama_model_provider/model/llm.py | 48 ++ .../ollama_model_provider/model/reranker.py | 49 ++ .../ollama_model_provider.py | 279 +++++++++ .../impl/openai_model_provider/__init__.py | 8 + .../credential/embedding.py | 53 ++ .../openai_model_provider/credential/image.py | 74 +++ .../openai_model_provider/credential/llm.py | 80 +++ .../openai_model_provider/credential/stt.py | 49 ++ .../openai_model_provider/credential/tti.py | 90 +++ .../openai_model_provider/credential/tts.py | 68 ++ .../icon/openai_icon_svg | 1 + .../openai_model_provider/model/embedding.py | 39 ++ .../impl/openai_model_provider/model/image.py | 20 + .../impl/openai_model_provider/model/llm.py | 58 ++ .../impl/openai_model_provider/model/stt.py | 59 ++ .../impl/openai_model_provider/model/tti.py | 58 ++ .../impl/openai_model_provider/model/tts.py | 64 ++ .../openai_model_provider.py | 147 +++++ .../impl/qwen_model_provider/__init__.py | 8 + .../qwen_model_provider/credential/image.py | 78 +++ .../qwen_model_provider/credential/llm.py | 76 +++ .../qwen_model_provider/credential/tti.py | 98 +++ .../qwen_model_provider/icon/qwen_icon_svg | 1 + .../impl/qwen_model_provider/model/image.py | 26 + .../impl/qwen_model_provider/model/llm.py | 31 + .../impl/qwen_model_provider/model/tti.py | 59 ++ .../qwen_model_provider.py | 65 ++ .../siliconCloud_model_provider/__init__.py | 8 + .../credential/embedding.py | 53 ++ .../credential/image.py | 74 +++ .../credential/llm.py | 79 +++ .../credential/reranker.py | 53 ++ .../credential/stt.py | 49 ++ .../credential/tti.py | 90 +++ .../credential/tts.py | 48 ++ .../icon/siliconCloud_icon_svg | 5 + .../model/embedding.py | 23 + .../model/image.py | 20 + .../siliconCloud_model_provider/model/llm.py | 38 ++ .../model/reranker.py | 74 +++ .../siliconCloud_model_provider/model/stt.py | 59 ++ .../siliconCloud_model_provider/model/tti.py | 58 ++ .../siliconCloud_model_provider/model/tts.py | 87 +++ .../siliconCloud_model_provider.py | 146 +++++ .../tencent_cloud_model_provider/__init__.py | 8 + .../credential/llm.py | 79 +++ .../icon/tencent_cloud_icon_svg | 15 + .../tencent_cloud_model_provider/model/llm.py | 53 ++ .../tencent_cloud_model_provider.py | 61 ++ .../impl/tencent_model_provider/__init__.py | 2 + .../credential/embedding.py | 41 ++ .../credential/image.py | 78 +++ .../tencent_model_provider/credential/llm.py | 70 +++ .../tencent_model_provider/credential/tti.py | 116 ++++ .../icon/tencent_icon_svg | 5 + .../tencent_model_provider/model/embedding.py | 41 ++ .../tencent_model_provider/model/hunyuan.py | 280 +++++++++ .../tencent_model_provider/model/image.py | 20 + .../impl/tencent_model_provider/model/llm.py | 45 ++ .../impl/tencent_model_provider/model/tti.py | 92 +++ .../tencent_model_provider.py | 127 ++++ .../impl/vllm_model_provider/__init__.py | 1 + .../credential/embedding.py | 53 ++ .../vllm_model_provider/credential/image.py | 72 +++ .../vllm_model_provider/credential/llm.py | 73 +++ .../vllm_model_provider/icon/vllm_icon_svg | 5 + .../vllm_model_provider/model/embedding.py | 23 + .../impl/vllm_model_provider/model/image.py | 38 ++ .../impl/vllm_model_provider/model/llm.py | 50 ++ .../vllm_model_provider.py | 84 +++ .../__init__.py | 2 + .../credential/embedding.py | 53 ++ .../credential/image.py | 72 +++ .../credential/llm.py | 79 +++ .../credential/stt.py | 52 ++ .../credential/tti.py | 68 ++ .../credential/tts.py | 78 +++ .../icon/volcanic_engine_icon_svg | 5 + .../model/embedding.py | 16 + .../model/iat_mp3_16k.mp3 | Bin 0 -> 17876 bytes .../model/image.py | 20 + .../model/llm.py | 21 + .../model/stt.py | 343 ++++++++++ .../model/tti.py | 172 +++++ .../model/tts.py | 182 ++++++ .../volcanic_engine_model_provider.py | 116 ++++ .../impl/wenxin_model_provider/__init__.py | 8 + .../credential/embedding.py | 49 ++ .../wenxin_model_provider/credential/llm.py | 82 +++ .../wenxin_model_provider/icon/azure_icon_svg | 5 + .../wenxin_model_provider/model/embedding.py | 23 + .../impl/wenxin_model_provider/model/llm.py | 76 +++ .../wenxin_model_provider.py | 68 ++ .../impl/xf_model_provider/__init__.py | 8 + .../xf_model_provider/credential/embedding.py | 50 ++ .../xf_model_provider/credential/image.py | 60 ++ .../xf_model_provider/credential/img_1.png | Bin 0 -> 363045 bytes .../impl/xf_model_provider/credential/llm.py | 101 +++ .../impl/xf_model_provider/credential/stt.py | 51 ++ .../impl/xf_model_provider/credential/tts.py | 75 +++ .../impl/xf_model_provider/icon/xf_icon_svg | 1 + .../impl/xf_model_provider/model/embedding.py | 49 ++ .../xf_model_provider/model/iat_mp3_16k.mp3 | Bin 0 -> 17876 bytes .../impl/xf_model_provider/model/image.py | 96 +++ .../impl/xf_model_provider/model/img_1.png | Bin 0 -> 363045 bytes .../impl/xf_model_provider/model/llm.py | 78 +++ .../impl/xf_model_provider/model/stt.py | 171 +++++ .../impl/xf_model_provider/model/tts.py | 150 +++++ .../xf_model_provider/xf_model_provider.py | 68 ++ .../xinference_model_provider/__init__.py | 1 + .../credential/embedding.py | 44 ++ .../credential/image.py | 70 +++ .../credential/llm.py | 67 ++ .../credential/reranker.py | 51 ++ .../credential/stt.py | 47 ++ .../credential/tti.py | 87 +++ .../credential/tts.py | 66 ++ .../icon/xinference_icon_svg | 5 + .../model/embedding.py | 91 +++ .../xinference_model_provider/model/image.py | 35 ++ .../xinference_model_provider/model/llm.py | 50 ++ .../model/reranker.py | 54 ++ .../xinference_model_provider/model/stt.py | 57 ++ .../xinference_model_provider/model/tti.py | 63 ++ .../xinference_model_provider/model/tts.py | 61 ++ .../xinference_model_provider.py | 585 ++++++++++++++++++ .../impl/zhipu_model_provider/__init__.py | 0 .../zhipu_model_provider/credential/image.py | 71 +++ .../zhipu_model_provider/credential/llm.py | 76 +++ .../zhipu_model_provider/credential/tti.py | 69 +++ .../icon/zhipuai_icon_svg | 1 + .../impl/zhipu_model_provider/model/image.py | 20 + .../impl/zhipu_model_provider/model/llm.py | 107 ++++ .../impl/zhipu_model_provider/model/tti.py | 69 +++ .../zhipu_model_provider.py | 77 +++ .../migrations/0001_initial.py | 39 ++ apps/models_provider/migrations/__init__.py | 0 apps/models_provider/models/__init__.py | 10 + .../models/model_management.py | 50 ++ apps/models_provider/serializers/__init__.py | 1 + apps/models_provider/serializers/model.py | 181 ++++++ apps/models_provider/tests.py | 3 + apps/models_provider/tools.py | 125 ++++ apps/models_provider/urls.py | 21 + apps/models_provider/views/__init__.py | 4 + apps/models_provider/views/model.py | 35 ++ apps/models_provider/views/provide.py | 68 ++ .../migrations/0002_systemsetting.py | 25 + apps/system_manage/models/__init__.py | 1 + apps/system_manage/models/system_setting.py | 32 + pyproject.toml | 24 +- 277 files changed, 15472 insertions(+), 3 deletions(-) create mode 100644 apps/common/cache/file_cache.py create mode 100644 apps/common/cache/mem_cache.py create mode 100644 apps/common/config/embedding_config.py create mode 100644 apps/common/config/tokenizer_manage_config.py create mode 100644 apps/common/forms/__init__.py create mode 100644 apps/common/forms/array_object_card.py create mode 100644 apps/common/forms/base_field.py create mode 100644 apps/common/forms/base_form.py create mode 100644 apps/common/forms/label/__init__.py create mode 100644 apps/common/forms/label/base_label.py create mode 100644 apps/common/forms/label/tooltip_label.py create mode 100644 apps/common/forms/multi_select.py create mode 100644 apps/common/forms/object_card.py create mode 100644 apps/common/forms/password_input.py create mode 100644 apps/common/forms/radio_button_field.py create mode 100644 apps/common/forms/radio_card_field.py create mode 100644 apps/common/forms/radio_field.py create mode 100644 apps/common/forms/single_select_field.py create mode 100644 apps/common/forms/slider_field.py create mode 100644 apps/common/forms/switch_field.py create mode 100644 apps/common/forms/tab_card.py create mode 100644 apps/common/forms/table_checkbox.py create mode 100644 apps/common/forms/table_radio.py create mode 100644 apps/common/forms/text_input_field.py create mode 100644 apps/common/mixins/app_model_mixin.py create mode 100644 apps/common/utils/rsa_util.py create mode 100644 apps/models_provider/__init__.py create mode 100644 apps/models_provider/admin.py create mode 100644 apps/models_provider/api/__init__.py create mode 100644 apps/models_provider/api/model.py create mode 100644 apps/models_provider/api/provide.py create mode 100644 apps/models_provider/apps.py create mode 100644 apps/models_provider/base_model_provider.py create mode 100644 apps/models_provider/constants/model_provider_constants.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/__init__.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/aliyun_bai_lian_model_provider.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/reranker.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/icon/aliyun_bai_lian_icon_svg create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/iat_mp3_16k.mp3 create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/image.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/reranker.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/anthropic_model_provider/__init__.py create mode 100644 apps/models_provider/impl/anthropic_model_provider/anthropic_model_provider.py create mode 100644 apps/models_provider/impl/anthropic_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/anthropic_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/anthropic_model_provider/icon/anthropic_icon_svg create mode 100644 apps/models_provider/impl/anthropic_model_provider/model/image.py create mode 100644 apps/models_provider/impl/anthropic_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/__init__.py create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/aws_bedrock_model_provider.py create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/icon/bedrock_icon_svg create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/aws_bedrock_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/azure_model_provider/__init__.py create mode 100644 apps/models_provider/impl/azure_model_provider/azure_model_provider.py create mode 100644 apps/models_provider/impl/azure_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/azure_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/azure_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/azure_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/azure_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/azure_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/azure_model_provider/icon/azure_icon_svg create mode 100644 apps/models_provider/impl/azure_model_provider/model/azure_chat_model.py create mode 100644 apps/models_provider/impl/azure_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/azure_model_provider/model/image.py create mode 100644 apps/models_provider/impl/azure_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/azure_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/azure_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/base_chat_open_ai.py create mode 100644 apps/models_provider/impl/base_stt.py create mode 100644 apps/models_provider/impl/base_tti.py create mode 100644 apps/models_provider/impl/base_tts.py create mode 100644 apps/models_provider/impl/deepseek_model_provider/__init__.py create mode 100644 apps/models_provider/impl/deepseek_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/deepseek_model_provider/deepseek_model_provider.py create mode 100644 apps/models_provider/impl/deepseek_model_provider/icon/deepseek_icon_svg create mode 100644 apps/models_provider/impl/deepseek_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/gemini_model_provider/__init__.py create mode 100644 apps/models_provider/impl/gemini_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/gemini_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/gemini_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/gemini_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/gemini_model_provider/gemini_model_provider.py create mode 100644 apps/models_provider/impl/gemini_model_provider/icon/gemini_icon_svg create mode 100644 apps/models_provider/impl/gemini_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/gemini_model_provider/model/image.py create mode 100644 apps/models_provider/impl/gemini_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/gemini_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/kimi_model_provider/__init__.py create mode 100644 apps/models_provider/impl/kimi_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/kimi_model_provider/icon/kimi_icon_svg create mode 100644 apps/models_provider/impl/kimi_model_provider/kimi_model_provider.py create mode 100644 apps/models_provider/impl/kimi_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/local_model_provider/__init__.py create mode 100644 apps/models_provider/impl/local_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/local_model_provider/credential/reranker.py create mode 100644 apps/models_provider/impl/local_model_provider/icon/local_icon_svg create mode 100644 apps/models_provider/impl/local_model_provider/local_model_provider.py create mode 100644 apps/models_provider/impl/local_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/local_model_provider/model/reranker.py create mode 100644 apps/models_provider/impl/ollama_model_provider/__init__.py create mode 100644 apps/models_provider/impl/ollama_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/ollama_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/ollama_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/ollama_model_provider/credential/reranker.py create mode 100644 apps/models_provider/impl/ollama_model_provider/icon/ollama_icon_svg create mode 100644 apps/models_provider/impl/ollama_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/ollama_model_provider/model/image.py create mode 100644 apps/models_provider/impl/ollama_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/ollama_model_provider/model/reranker.py create mode 100644 apps/models_provider/impl/ollama_model_provider/ollama_model_provider.py create mode 100644 apps/models_provider/impl/openai_model_provider/__init__.py create mode 100644 apps/models_provider/impl/openai_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/openai_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/openai_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/openai_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/openai_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/openai_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/openai_model_provider/icon/openai_icon_svg create mode 100644 apps/models_provider/impl/openai_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/openai_model_provider/model/image.py create mode 100644 apps/models_provider/impl/openai_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/openai_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/openai_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/openai_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/openai_model_provider/openai_model_provider.py create mode 100644 apps/models_provider/impl/qwen_model_provider/__init__.py create mode 100644 apps/models_provider/impl/qwen_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/qwen_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/qwen_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/qwen_model_provider/icon/qwen_icon_svg create mode 100644 apps/models_provider/impl/qwen_model_provider/model/image.py create mode 100644 apps/models_provider/impl/qwen_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/qwen_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/qwen_model_provider/qwen_model_provider.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/__init__.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/reranker.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/icon/siliconCloud_icon_svg create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/image.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/reranker.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/siliconCloud_model_provider/siliconCloud_model_provider.py create mode 100644 apps/models_provider/impl/tencent_cloud_model_provider/__init__.py create mode 100644 apps/models_provider/impl/tencent_cloud_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/tencent_cloud_model_provider/icon/tencent_cloud_icon_svg create mode 100644 apps/models_provider/impl/tencent_cloud_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/tencent_cloud_model_provider/tencent_cloud_model_provider.py create mode 100644 apps/models_provider/impl/tencent_model_provider/__init__.py create mode 100644 apps/models_provider/impl/tencent_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/tencent_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/tencent_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/tencent_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/tencent_model_provider/icon/tencent_icon_svg create mode 100644 apps/models_provider/impl/tencent_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/tencent_model_provider/model/hunyuan.py create mode 100644 apps/models_provider/impl/tencent_model_provider/model/image.py create mode 100644 apps/models_provider/impl/tencent_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/tencent_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/tencent_model_provider/tencent_model_provider.py create mode 100644 apps/models_provider/impl/vllm_model_provider/__init__.py create mode 100644 apps/models_provider/impl/vllm_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/vllm_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/vllm_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/vllm_model_provider/icon/vllm_icon_svg create mode 100644 apps/models_provider/impl/vllm_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/vllm_model_provider/model/image.py create mode 100644 apps/models_provider/impl/vllm_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/vllm_model_provider/vllm_model_provider.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/__init__.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/icon/volcanic_engine_icon_svg create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/iat_mp3_16k.mp3 create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/image.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/volcanic_engine_model_provider/volcanic_engine_model_provider.py create mode 100644 apps/models_provider/impl/wenxin_model_provider/__init__.py create mode 100644 apps/models_provider/impl/wenxin_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/wenxin_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/wenxin_model_provider/icon/azure_icon_svg create mode 100644 apps/models_provider/impl/wenxin_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/wenxin_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/wenxin_model_provider/wenxin_model_provider.py create mode 100644 apps/models_provider/impl/xf_model_provider/__init__.py create mode 100644 apps/models_provider/impl/xf_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/xf_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/xf_model_provider/credential/img_1.png create mode 100644 apps/models_provider/impl/xf_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/xf_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/xf_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/xf_model_provider/icon/xf_icon_svg create mode 100644 apps/models_provider/impl/xf_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/xf_model_provider/model/iat_mp3_16k.mp3 create mode 100644 apps/models_provider/impl/xf_model_provider/model/image.py create mode 100644 apps/models_provider/impl/xf_model_provider/model/img_1.png create mode 100644 apps/models_provider/impl/xf_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/xf_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/xf_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/xf_model_provider/xf_model_provider.py create mode 100644 apps/models_provider/impl/xinference_model_provider/__init__.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/embedding.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/reranker.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/stt.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/xinference_model_provider/credential/tts.py create mode 100644 apps/models_provider/impl/xinference_model_provider/icon/xinference_icon_svg create mode 100644 apps/models_provider/impl/xinference_model_provider/model/embedding.py create mode 100644 apps/models_provider/impl/xinference_model_provider/model/image.py create mode 100644 apps/models_provider/impl/xinference_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/xinference_model_provider/model/reranker.py create mode 100644 apps/models_provider/impl/xinference_model_provider/model/stt.py create mode 100644 apps/models_provider/impl/xinference_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/xinference_model_provider/model/tts.py create mode 100644 apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/__init__.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/credential/image.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/credential/llm.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/credential/tti.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/icon/zhipuai_icon_svg create mode 100644 apps/models_provider/impl/zhipu_model_provider/model/image.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/model/llm.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/model/tti.py create mode 100644 apps/models_provider/impl/zhipu_model_provider/zhipu_model_provider.py create mode 100644 apps/models_provider/migrations/0001_initial.py create mode 100644 apps/models_provider/migrations/__init__.py create mode 100644 apps/models_provider/models/__init__.py create mode 100644 apps/models_provider/models/model_management.py create mode 100644 apps/models_provider/serializers/__init__.py create mode 100644 apps/models_provider/serializers/model.py create mode 100644 apps/models_provider/tests.py create mode 100644 apps/models_provider/tools.py create mode 100644 apps/models_provider/urls.py create mode 100644 apps/models_provider/views/__init__.py create mode 100644 apps/models_provider/views/model.py create mode 100644 apps/models_provider/views/provide.py create mode 100644 apps/system_manage/migrations/0002_systemsetting.py create mode 100644 apps/system_manage/models/system_setting.py diff --git a/apps/common/cache/file_cache.py b/apps/common/cache/file_cache.py new file mode 100644 index 000000000..45b5a7349 --- /dev/null +++ b/apps/common/cache/file_cache.py @@ -0,0 +1,86 @@ +# coding=utf-8 +""" + @project: qabot + @Author:虎 + @file: file_cache.py + @date:2023/9/11 15:58 + @desc: 文件缓存 +""" +import datetime +import math +import os +import time + +from diskcache import Cache +from django.core.cache.backends.base import BaseCache + + +class FileCache(BaseCache): + def __init__(self, dir, params): + super().__init__(params) + self._dir = os.path.abspath(dir) + self._createdir() + self.cache = Cache(self._dir) + + def _createdir(self): + old_umask = os.umask(0o077) + try: + os.makedirs(self._dir, 0o700, exist_ok=True) + finally: + os.umask(old_umask) + + def add(self, key, value, timeout=None, version=None): + expire = timeout if isinstance(timeout, int) or isinstance(timeout, + float) or timeout is None else timeout.total_seconds() + return self.cache.add(self.get_key(key, version), value=value, expire=expire) + + def set(self, key, value, timeout=None, version=None): + expire = timeout if isinstance(timeout, int) or isinstance(timeout, + float) or timeout is None else timeout.total_seconds() + return self.cache.set(self.get_key(key, version), value=value, expire=expire) + + def get(self, key, default=None, version=None): + return self.cache.get(self.get_key(key, version), default=default) + + @staticmethod + def get_key(key, version): + if version is None: + return f"default:{key}" + return f"{version}:{key}" + + def delete(self, key, version=None): + return self.cache.delete(self.get_key(key, version)) + + def touch(self, key, timeout=None, version=None): + expire = timeout if isinstance(timeout, int) or isinstance(timeout, + float) else timeout.total_seconds() + + return self.cache.touch(self.get_key(key, version), expire=expire) + + def ttl(self, key, version=None): + """ + 获取key的剩余时间 + :param key: key + :return: 剩余时间 + @param version: + """ + value, expire_time = self.cache.get(self.get_key(key, version), expire_time=True) + if value is None: + return None + return datetime.timedelta(seconds=math.ceil(expire_time - time.time())) + + def clear_by_application_id(self, application_id): + delete_keys = [] + for key in self.cache.iterkeys(): + value = self.cache.get(key) + if (hasattr(value, + 'application') and value.application is not None and value.application.id is not None and + str( + value.application.id) == application_id): + delete_keys.append(key) + for key in delete_keys: + self.cache.delete(key) + + def clear_timeout_data(self): + for key in self.cache.iterkeys(): + self.get(key) diff --git a/apps/common/cache/mem_cache.py b/apps/common/cache/mem_cache.py new file mode 100644 index 000000000..5afb1e562 --- /dev/null +++ b/apps/common/cache/mem_cache.py @@ -0,0 +1,47 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: mem_cache.py + @date:2024/3/6 11:20 + @desc: +""" +from django.core.cache.backends.base import DEFAULT_TIMEOUT +from django.core.cache.backends.locmem import LocMemCache + + +class MemCache(LocMemCache): + def __init__(self, name, params): + super().__init__(name, params) + + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): + key = self.make_and_validate_key(key, version=version) + pickled = value + with self._lock: + self._set(key, pickled, timeout) + + def get(self, key, default=None, version=None): + key = self.make_and_validate_key(key, version=version) + with self._lock: + if self._has_expired(key): + self._delete(key) + return default + pickled = self._cache[key] + self._cache.move_to_end(key, last=False) + return pickled + + def clear_by_application_id(self, application_id): + delete_keys = [] + for key in self._cache.keys(): + value = self._cache.get(key) + if (hasattr(value, + 'application') and value.application is not None and value.application.id is not None and + str( + value.application.id) == application_id): + delete_keys.append(key) + for key in delete_keys: + self._delete(key) + + def clear_timeout_data(self): + for key in self._cache.keys(): + self.get(key) diff --git a/apps/common/config/embedding_config.py b/apps/common/config/embedding_config.py new file mode 100644 index 000000000..98c391e96 --- /dev/null +++ b/apps/common/config/embedding_config.py @@ -0,0 +1,66 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: embedding_config.py + @date:2023/10/23 16:03 + @desc: +""" +import threading +import time + +from common.cache.mem_cache import MemCache + +lock = threading.Lock() + + +class ModelManage: + cache = MemCache('model', {}) + up_clear_time = time.time() + + @staticmethod + def get_model(_id, get_model): + # 获取锁 + lock.acquire() + try: + model_instance = ModelManage.cache.get(_id) + if model_instance is None or not model_instance.is_cache_model(): + model_instance = get_model(_id) + ModelManage.cache.set(_id, model_instance, timeout=60 * 30) + return model_instance + # 续期 + ModelManage.cache.touch(_id, timeout=60 * 30) + ModelManage.clear_timeout_cache() + return model_instance + finally: + # 释放锁 + lock.release() + + @staticmethod + def clear_timeout_cache(): + if time.time() - ModelManage.up_clear_time > 60: + ModelManage.cache.clear_timeout_data() + + @staticmethod + def delete_key(_id): + if ModelManage.cache.has_key(_id): + ModelManage.cache.delete(_id) + + +class VectorStore: + from embedding.vector.pg_vector import PGVector + from embedding.vector.base_vector import BaseVectorStore + instance_map = { + 'pg_vector': PGVector, + } + instance = None + + @staticmethod + def get_embedding_vector() -> BaseVectorStore: + from embedding.vector.pg_vector import PGVector + if VectorStore.instance is None: + from maxkb.const import CONFIG + vector_store_class = VectorStore.instance_map.get(CONFIG.get("VECTOR_STORE_NAME"), + PGVector) + VectorStore.instance = vector_store_class() + return VectorStore.instance diff --git a/apps/common/config/tokenizer_manage_config.py b/apps/common/config/tokenizer_manage_config.py new file mode 100644 index 000000000..b590d3a00 --- /dev/null +++ b/apps/common/config/tokenizer_manage_config.py @@ -0,0 +1,24 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: tokenizer_manage_config.py + @date:2024/4/28 10:17 + @desc: +""" + + +class TokenizerManage: + tokenizer = None + + @staticmethod + def get_tokenizer(): + from transformers import BertTokenizer + if TokenizerManage.tokenizer is None: + TokenizerManage.tokenizer = BertTokenizer.from_pretrained( + 'bert-base-cased', + cache_dir="/opt/maxkb/model/tokenizer", + local_files_only=True, + resume_download=False, + force_download=False) + return TokenizerManage.tokenizer diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index a2a5a94cf..2ba1a3940 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -111,6 +111,13 @@ class PermissionConstants(Enum): USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN]) TOOL_CREATE = Permission(group=Group.USER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, RoleConstants.USER]) + MODEL_CREATE = Permission(group=Group.USER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN, + RoleConstants.USER]) + MODEL_READ = Permission(group=Group.USER, operate=Operate.READ, role_list=[RoleConstants.ADMIN, + RoleConstants.USER]) + MODEL_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER]) + MODEL_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER]) def get_workspace_application_permission(self): return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate, diff --git a/apps/common/forms/__init__.py b/apps/common/forms/__init__.py new file mode 100644 index 000000000..609542193 --- /dev/null +++ b/apps/common/forms/__init__.py @@ -0,0 +1,24 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py + @date:2023/10/31 17:56 + @desc: +""" +from .array_object_card import * +from .base_field import * +from .base_form import * +from .multi_select import * +from .object_card import * +from .password_input import * +from .radio_field import * +from .single_select_field import * +from .tab_card import * +from .table_radio import * +from .text_input_field import * +from .radio_button_field import * +from .table_checkbox import * +from .radio_card_field import * +from .label import * +from .slider_field import * diff --git a/apps/common/forms/array_object_card.py b/apps/common/forms/array_object_card.py new file mode 100644 index 000000000..2dc71aaaf --- /dev/null +++ b/apps/common/forms/array_object_card.py @@ -0,0 +1,33 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: array_object_card.py + @date:2023/10/31 18:03 + @desc: +""" +from typing import Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class ArrayCard(BaseExecField): + """ + 收集List[Object] + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("ArrayObjectCard", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) diff --git a/apps/common/forms/base_field.py b/apps/common/forms/base_field.py new file mode 100644 index 000000000..b0cf0f202 --- /dev/null +++ b/apps/common/forms/base_field.py @@ -0,0 +1,157 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: base_field.py + @date:2023/10/31 18:07 + @desc: +""" +from enum import Enum +from typing import List, Dict + +from common.exception.app_exception import AppApiException +from common.forms.label.base_label import BaseLabel +from django.utils.translation import gettext_lazy as _ + + +class TriggerType(Enum): + # 执行函数获取 OptionList数据 + OPTION_LIST = 'OPTION_LIST' + # 执行函数获取子表单 + CHILD_FORMS = 'CHILD_FORMS' + + +class BaseField: + def __init__(self, + input_type: str, + label: str or BaseLabel, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + """ + + :param input_type: 字段 + :param label: 提示 + :param default_value: 默认值 + :param relation_show_field_dict: {field:field_value_list} 表示在 field有值 ,并且值在field_value_list中才显示 + :param relation_trigger_field_dict: {field:field_value_list} 表示在 field有值 ,并且值在field_value_list中才 执行函数获取 数据 + :param trigger_type: 执行器类型 OPTION_LIST请求Option_list数据 CHILD_FORMS请求子表单 + :param attrs: 前端attr数据 + :param props_info: 其他额外信息 + """ + if props_info is None: + props_info = {} + if attrs is None: + attrs = {} + self.label = label + self.attrs = attrs + self.props_info = props_info + self.default_value = default_value + self.input_type = input_type + self.relation_show_field_dict = {} if relation_show_field_dict is None else relation_show_field_dict + self.relation_trigger_field_dict = [] if relation_trigger_field_dict is None else relation_trigger_field_dict + self.required = required + self.trigger_type = trigger_type + + def is_valid(self, value): + field_label = self.label.label if hasattr(self.label, 'to_dict') else self.label + if self.required and value is None: + raise AppApiException(500, + _('The field {field_label} is required').format(field_label=field_label)) + + def to_dict(self, **kwargs): + return { + 'input_type': self.input_type, + 'label': self.label.to_dict(**kwargs) if hasattr(self.label, 'to_dict') else self.label, + 'required': self.required, + 'default_value': self.default_value, + 'relation_show_field_dict': self.relation_show_field_dict, + 'relation_trigger_field_dict': self.relation_trigger_field_dict, + 'trigger_type': self.trigger_type.value, + 'attrs': self.attrs, + 'props_info': self.props_info, + **kwargs + } + + +class BaseDefaultOptionField(BaseField): + def __init__(self, input_type: str, + label: str, + text_field: str, + value_field: str, + option_list: List[dict], + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict[str, object] = None, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + """ + + :param input_type: 字段 + :param label: label + :param text_field: 文本字段 + :param value_field: 值字段 + :param option_list: 可选列表 + :param required: 是否必填 + :param default_value: 默认值 + :param relation_show_field_dict: {field:field_value_list} 表示在 field有值 ,并且值在field_value_list中才显示 + :param attrs: 前端attr数据 + :param props_info: 其他额外信息 + """ + super().__init__(input_type, label, required, default_value, relation_show_field_dict, + {}, TriggerType.OPTION_LIST, attrs, props_info) + self.text_field = text_field + self.value_field = value_field + self.option_list = option_list + + def to_dict(self, **kwargs): + return {**super().to_dict(**kwargs), 'text_field': self.text_field, 'value_field': self.value_field, + 'option_list': self.option_list} + + +class BaseExecField(BaseField): + def __init__(self, + input_type: str, + label: str, + text_field: str, + value_field: str, + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + """ + + :param input_type: 字段 + :param label: 提示 + :param text_field: 文本字段 + :param value_field: 值字段 + :param provider: 指定供应商 + :param method: 执行供应商函数 method + :param required: 是否必填 + :param default_value: 默认值 + :param relation_show_field_dict: {field:field_value_list} 表示在 field有值 ,并且值在field_value_list中才显示 + :param relation_trigger_field_dict: {field:field_value_list} 表示在 field有值 ,并且值在field_value_list中才 执行函数获取 数据 + :param trigger_type: 执行器类型 OPTION_LIST请求Option_list数据 CHILD_FORMS请求子表单 + :param attrs: 前端attr数据 + :param props_info: 其他额外信息 + """ + super().__init__(input_type, label, required, default_value, relation_show_field_dict, + relation_trigger_field_dict, + trigger_type, attrs, props_info) + self.text_field = text_field + self.value_field = value_field + self.provider = provider + self.method = method + + def to_dict(self, **kwargs): + return {**super().to_dict(**kwargs), 'text_field': self.text_field, 'value_field': self.value_field, + 'provider': self.provider, 'method': self.method} diff --git a/apps/common/forms/base_form.py b/apps/common/forms/base_form.py new file mode 100644 index 000000000..5ef92c5c1 --- /dev/null +++ b/apps/common/forms/base_form.py @@ -0,0 +1,30 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: base_form.py + @date:2023/11/1 16:04 + @desc: +""" +from typing import Dict + +from common.forms import BaseField + + +class BaseForm: + def to_form_list(self, **kwargs): + return [{**self.__getattribute__(key).to_dict(**kwargs), 'field': key} for key in + list(filter(lambda key: isinstance(self.__getattribute__(key), BaseField), + [attr for attr in vars(self.__class__) if not attr.startswith("__")]))] + + def valid_form(self, form_data): + field_keys = list(filter(lambda key: isinstance(self.__getattribute__(key), BaseField), + [attr for attr in vars(self.__class__) if not attr.startswith("__")])) + for field_key in field_keys: + self.__getattribute__(field_key).is_valid(form_data.get(field_key)) + + def get_default_form_data(self): + return {key: self.__getattribute__(key).default_value for key in + [attr for attr in vars(self.__class__) if not attr.startswith("__")] if + isinstance(self.__getattribute__(key), BaseField) and self.__getattribute__( + key).default_value is not None} diff --git a/apps/common/forms/label/__init__.py b/apps/common/forms/label/__init__.py new file mode 100644 index 000000000..81c1b3298 --- /dev/null +++ b/apps/common/forms/label/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py.py + @date:2024/8/22 17:19 + @desc: +""" +from .base_label import * +from .tooltip_label import * diff --git a/apps/common/forms/label/base_label.py b/apps/common/forms/label/base_label.py new file mode 100644 index 000000000..59e4d3722 --- /dev/null +++ b/apps/common/forms/label/base_label.py @@ -0,0 +1,28 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: base_label.py + @date:2024/8/22 17:11 + @desc: +""" + + +class BaseLabel: + def __init__(self, + input_type: str, + label: str, + attrs=None, + props_info=None): + self.input_type = input_type + self.label = label + self.attrs = attrs + self.props_info = props_info + + def to_dict(self, **kwargs): + return { + 'input_type': self.input_type, + 'label': self.label, + 'attrs': {} if self.attrs is None else self.attrs, + 'props_info': {} if self.props_info is None else self.props_info, + } diff --git a/apps/common/forms/label/tooltip_label.py b/apps/common/forms/label/tooltip_label.py new file mode 100644 index 000000000..885345daf --- /dev/null +++ b/apps/common/forms/label/tooltip_label.py @@ -0,0 +1,14 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: tooltip_label.py + @date:2024/8/22 17:19 + @desc: +""" +from common.forms.label.base_label import BaseLabel + + +class TooltipLabel(BaseLabel): + def __init__(self, label, tooltip): + super().__init__('TooltipLabel', label, attrs={'tooltip': tooltip}, props_info={}) diff --git a/apps/common/forms/multi_select.py b/apps/common/forms/multi_select.py new file mode 100644 index 000000000..791c8e974 --- /dev/null +++ b/apps/common/forms/multi_select.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: multi_select.py + @date:2023/10/31 18:00 + @desc: +""" +from typing import List, Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class MultiSelect(BaseExecField): + """ + 下拉单选 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + option_list: List[str:object], + provider: str = None, + method: str = None, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("MultiSelect", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) + self.option_list = option_list + + def to_dict(self): + return {**super().to_dict(), 'option_list': self.option_list} diff --git a/apps/common/forms/object_card.py b/apps/common/forms/object_card.py new file mode 100644 index 000000000..ddb192ef9 --- /dev/null +++ b/apps/common/forms/object_card.py @@ -0,0 +1,33 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: object_card.py + @date:2023/10/31 18:02 + @desc: +""" +from typing import Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class ObjectCard(BaseExecField): + """ + 收集对象子表卡片 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("ObjectCard", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) diff --git a/apps/common/forms/password_input.py b/apps/common/forms/password_input.py new file mode 100644 index 000000000..e7c7923bb --- /dev/null +++ b/apps/common/forms/password_input.py @@ -0,0 +1,26 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: password_input.py + @date:2023/11/1 14:48 + @desc: +""" +from typing import Dict + +from common.forms import BaseField, TriggerType + + +class PasswordInputField(BaseField): + """ + 文本输入框 + """ + + def __init__(self, label: str, + required: bool = False, + default_value=None, + relation_show_field_dict: Dict = None, + attrs=None, props_info=None): + super().__init__('PasswordInput', label, required, default_value, relation_show_field_dict, + {}, + TriggerType.OPTION_LIST, attrs, props_info) diff --git a/apps/common/forms/radio_button_field.py b/apps/common/forms/radio_button_field.py new file mode 100644 index 000000000..aa6952303 --- /dev/null +++ b/apps/common/forms/radio_button_field.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: radio_field.py + @date:2023/10/31 17:59 + @desc: +""" +from typing import List, Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class Radio(BaseExecField): + """ + 下拉单选 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + option_list: List[str:object], + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("RadioButton", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) + self.option_list = option_list + + def to_dict(self): + return {**super().to_dict(), 'option_list': self.option_list} diff --git a/apps/common/forms/radio_card_field.py b/apps/common/forms/radio_card_field.py new file mode 100644 index 000000000..b3579b84d --- /dev/null +++ b/apps/common/forms/radio_card_field.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: radio_field.py + @date:2023/10/31 17:59 + @desc: +""" +from typing import List, Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class Radio(BaseExecField): + """ + 下拉单选 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + option_list: List[str:object], + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("RadioCard", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) + self.option_list = option_list + + def to_dict(self): + return {**super().to_dict(), 'option_list': self.option_list} diff --git a/apps/common/forms/radio_field.py b/apps/common/forms/radio_field.py new file mode 100644 index 000000000..94a016d9d --- /dev/null +++ b/apps/common/forms/radio_field.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: radio_field.py + @date:2023/10/31 17:59 + @desc: +""" +from typing import List, Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class Radio(BaseExecField): + """ + 下拉单选 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + option_list: List[str:object], + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("Radio", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) + self.option_list = option_list + + def to_dict(self): + return {**super().to_dict(), 'option_list': self.option_list} diff --git a/apps/common/forms/single_select_field.py b/apps/common/forms/single_select_field.py new file mode 100644 index 000000000..21bd5de57 --- /dev/null +++ b/apps/common/forms/single_select_field.py @@ -0,0 +1,39 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: single_select_field.py + @date:2023/10/31 18:00 + @desc: +""" +from typing import List, Dict + +from common.forms import BaseLabel +from common.forms.base_field import TriggerType, BaseExecField + + +class SingleSelect(BaseExecField): + """ + 下拉单选 + """ + + def __init__(self, + label: str or BaseLabel, + text_field: str, + value_field: str, + option_list: List[str:object], + provider: str = None, + method: str = None, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("SingleSelect", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) + self.option_list = option_list + + def to_dict(self): + return {**super().to_dict(), 'option_list': self.option_list} diff --git a/apps/common/forms/slider_field.py b/apps/common/forms/slider_field.py new file mode 100644 index 000000000..3919891fd --- /dev/null +++ b/apps/common/forms/slider_field.py @@ -0,0 +1,65 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: slider_field.py + @date:2024/8/22 17:06 + @desc: +""" +from typing import Dict + +from common.exception.app_exception import AppApiException +from common.forms import BaseField, TriggerType, BaseLabel +from django.utils.translation import gettext_lazy as _ + + +class SliderField(BaseField): + """ + 滑块输入框 + """ + + def __init__(self, label: str or BaseLabel, + _min, + _max, + _step, + precision, + required: bool = False, + default_value=None, + relation_show_field_dict: Dict = None, + attrs=None, props_info=None): + """ + @param label: 提示 + @param _min: 最小值 + @param _max: 最大值 + @param _step: 步长 + @param precision: 保留多少小数 + @param required: 是否必填 + @param default_value: 默认值 + @param relation_show_field_dict: + @param attrs: + @param props_info: + """ + _attrs = {'min': _min, 'max': _max, 'step': _step, + 'precision': precision, 'show-input-controls': False, 'show-input': True} + if attrs is not None: + _attrs.update(attrs) + super().__init__('Slider', label, required, default_value, relation_show_field_dict, + {}, + TriggerType.OPTION_LIST, _attrs, props_info) + + def is_valid(self, value): + super().is_valid(value) + field_label = self.label.label if hasattr(self.label, 'to_dict') else self.label + if value is not None: + if value < self.attrs.get('min'): + raise AppApiException(500, + _("The {field_label} cannot be less than {min}").format(field_label=field_label, + min=self.attrs.get( + 'min'))) + + if value > self.attrs.get('max'): + raise AppApiException(500, + _("The {field_label} cannot be greater than {max}").format( + field_label=field_label, + max=self.attrs.get( + 'max'))) diff --git a/apps/common/forms/switch_field.py b/apps/common/forms/switch_field.py new file mode 100644 index 000000000..9fa176bee --- /dev/null +++ b/apps/common/forms/switch_field.py @@ -0,0 +1,33 @@ +""" + @project: MaxKB + @Author:虎 + @file: switch_field.py + @date:2024/10/13 19:43 + @desc: +""" +from typing import Dict +from common.forms import BaseField, TriggerType, BaseLabel + + +class SwitchField(BaseField): + """ + 滑块输入框 + """ + + def __init__(self, label: str or BaseLabel, + required: bool = False, + default_value=None, + relation_show_field_dict: Dict = None, + + attrs=None, props_info=None): + """ + @param required: 是否必填 + @param default_value: 默认值 + @param relation_show_field_dict: + @param attrs: + @param props_info: + """ + + super().__init__('Switch', label, required, default_value, relation_show_field_dict, + {}, + TriggerType.OPTION_LIST, attrs, props_info) diff --git a/apps/common/forms/tab_card.py b/apps/common/forms/tab_card.py new file mode 100644 index 000000000..7907714bd --- /dev/null +++ b/apps/common/forms/tab_card.py @@ -0,0 +1,33 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: tab_card.py + @date:2023/10/31 18:03 + @desc: +""" +from typing import Dict + +from common.forms.base_field import BaseExecField, TriggerType + + +class TabCard(BaseExecField): + """ + 收集 Tab类型数据 tab1:{},tab2:{} + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("TabCard", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) diff --git a/apps/common/forms/table_checkbox.py b/apps/common/forms/table_checkbox.py new file mode 100644 index 000000000..e01f14d31 --- /dev/null +++ b/apps/common/forms/table_checkbox.py @@ -0,0 +1,33 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: table_radio.py + @date:2023/10/31 18:01 + @desc: +""" +from typing import Dict + +from common.forms.base_field import TriggerType, BaseExecField + + +class TableRadio(BaseExecField): + """ + table 单选 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("TableCheckbox", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) diff --git a/apps/common/forms/table_radio.py b/apps/common/forms/table_radio.py new file mode 100644 index 000000000..3b4c2bfb0 --- /dev/null +++ b/apps/common/forms/table_radio.py @@ -0,0 +1,33 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: table_radio.py + @date:2023/10/31 18:01 + @desc: +""" +from typing import Dict + +from common.forms.base_field import TriggerType, BaseExecField + + +class TableRadio(BaseExecField): + """ + table 单选 + """ + + def __init__(self, + label: str, + text_field: str, + value_field: str, + provider: str, + method: str, + required: bool = False, + default_value: object = None, + relation_show_field_dict: Dict = None, + relation_trigger_field_dict: Dict = None, + trigger_type: TriggerType = TriggerType.OPTION_LIST, + attrs: Dict[str, object] = None, + props_info: Dict[str, object] = None): + super().__init__("TableRadio", label, text_field, value_field, provider, method, required, default_value, + relation_show_field_dict, relation_trigger_field_dict, trigger_type, attrs, props_info) diff --git a/apps/common/forms/text_input_field.py b/apps/common/forms/text_input_field.py new file mode 100644 index 000000000..2b8b2ce04 --- /dev/null +++ b/apps/common/forms/text_input_field.py @@ -0,0 +1,28 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: text_input_field.py + @date:2023/10/31 17:58 + @desc: +""" +from typing import Dict + +from common.forms import BaseLabel +from common.forms.base_field import BaseField, TriggerType + + +class TextInputField(BaseField): + """ + 文本输入框 + """ + + def __init__(self, label: str or BaseLabel, + required: bool = False, + default_value=None, + relation_show_field_dict: Dict = None, + + attrs=None, props_info=None): + super().__init__('TextInput', label, required, default_value, relation_show_field_dict, + {}, + TriggerType.OPTION_LIST, attrs, props_info) diff --git a/apps/common/mixins/app_model_mixin.py b/apps/common/mixins/app_model_mixin.py new file mode 100644 index 000000000..412dbae00 --- /dev/null +++ b/apps/common/mixins/app_model_mixin.py @@ -0,0 +1,18 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: app_model_mixin.py + @date:2023/9/21 9:41 + @desc: +""" +from django.db import models + + +class AppModelMixin(models.Model): + create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True) + update_time = models.DateTimeField(verbose_name="修改时间", auto_now=True) + + class Meta: + abstract = True + ordering = ['create_time'] diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 7cd6be01d..a28b4b94d 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -7,8 +7,18 @@ @desc: """ import hashlib +import io +import mimetypes +import re +import shutil from typing import List +from django.core.files.uploadedfile import InMemoryUploadedFile +from django.utils.translation import gettext as _ +from pydub import AudioSegment + +from ..exception.app_exception import AppApiException + def password_encrypt(row_password): """ @@ -36,3 +46,153 @@ def group_by(list_source: List, key): array.append(e) result[k] = array return result + + +def encryption(message: str): + """ + 加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890 + :param message: + :return: + """ + max_pre_len = 8 + max_post_len = 4 + message_len = len(message) + pre_len = int(message_len / 5 * 2) + post_len = int(message_len / 5 * 1) + pre_str = "".join([message[index] for index in + range(0, max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int(pre_len))]) + end_str = "".join( + [message[index] for index in + range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len), message_len)]) + content = "***************" + return pre_str + content + end_str + + +def _remove_empty_lines(text): + if not isinstance(text, str): + raise AppApiException(500, _('Text-to-speech node, the text content must be of string type')) + if not text: + raise AppApiException(500, _('Text-to-speech node, the text content cannot be empty')) + result = '\n'.join(line for line in text.split('\n') if line.strip()) + return markdown_to_plain_text(result) + + +def markdown_to_plain_text(md: str) -> str: + # 移除图片 ![alt](url) + text = re.sub(r'!\[.*?\]\(.*?\)', '', md) + # 移除链接 [text](url) + text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) + # 移除 Markdown 标题符号 (#, ##, ###) + text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) + # 移除加粗 **text** 或 __text__ + text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) + text = re.sub(r'__(.*?)__', r'\1', text) + # 移除斜体 *text* 或 _text_ + text = re.sub(r'\*(.*?)\*', r'\1', text) + text = re.sub(r'_(.*?)_', r'\1', text) + # 移除行内代码 `code` + text = re.sub(r'`(.*?)`', r'\1', text) + # 移除代码块 ```code``` + text = re.sub(r'```[\s\S]*?```', '', text) + # 移除多余的换行符 + text = re.sub(r'\n{2,}', '\n', text) + # 使用正则表达式去除所有 HTML 标签 + text = re.sub(r'<[^>]+>', '', text) + # 去除多余的空白字符(包括换行符、制表符等) + text = re.sub(r'\s+', ' ', text) + # 去除表单渲染 + re.sub(r'[\s\S]*?<\/form_rander>', '', text) + # 去除首尾空格 + text = text.strip() + return text + + +def get_file_content(path): + with open(path, "r", encoding='utf-8') as file: + content = file.read() + return content + + + +def bytes_to_uploaded_file(file_bytes, file_name="file.txt"): + content_type, _ = mimetypes.guess_type(file_name) + if content_type is None: + # 如果未能识别,设置为默认的二进制文件类型 + content_type = "application/octet-stream" + # 创建一个内存中的字节流对象 + file_stream = io.BytesIO(file_bytes) + + # 获取文件大小 + file_size = len(file_bytes) + + # 创建 InMemoryUploadedFile 对象 + uploaded_file = InMemoryUploadedFile( + file=file_stream, + field_name=None, + name=file_name, + content_type=content_type, + size=file_size, + charset=None, + ) + return uploaded_file + + +def any_to_amr(any_path, amr_path): + """ + 把任意格式转成amr文件 + """ + if any_path.endswith(".amr"): + shutil.copy2(any_path, amr_path) + return + if any_path.endswith(".sil") or any_path.endswith(".silk") or any_path.endswith(".slk"): + raise NotImplementedError("Not support file type: {}".format(any_path)) + audio = AudioSegment.from_file(any_path) + audio = audio.set_frame_rate(8000) # only support 8000 + audio.export(amr_path, format="amr") + return audio.duration_seconds * 1000 + + +def any_to_mp3(any_path, mp3_path): + """ + 把任意格式转成mp3文件 + """ + if any_path.endswith(".mp3"): + shutil.copy2(any_path, mp3_path) + return + if any_path.endswith(".sil") or any_path.endswith(".silk") or any_path.endswith(".slk"): + sil_to_wav(any_path, any_path) + any_path = mp3_path + audio = AudioSegment.from_file(any_path) + audio = audio.set_frame_rate(16000) + audio.export(mp3_path, format="mp3") + + +def sil_to_wav(silk_path, wav_path, rate: int = 24000): + """ + silk 文件转 wav + """ + try: + import pysilk + except ImportError: + raise AppApiException("import pysilk failed, wechaty voice message will not be supported.") + wav_data = pysilk.decode_file(silk_path, to_wav=True, sample_rate=rate) + with open(wav_path, "wb") as f: + f.write(wav_data) + + +def split_and_transcribe(file_path, model, max_segment_length_ms=59000, audio_format="mp3"): + audio_data = AudioSegment.from_file(file_path, format=audio_format) + audio_length_ms = len(audio_data) + + if audio_length_ms <= max_segment_length_ms: + return model.speech_to_text(io.BytesIO(audio_data.export(format=audio_format).read())) + + full_text = [] + for start_ms in range(0, audio_length_ms, max_segment_length_ms): + end_ms = min(audio_length_ms, start_ms + max_segment_length_ms) + segment = audio_data[start_ms:end_ms] + text = model.speech_to_text(io.BytesIO(segment.export(format=audio_format).read())) + if isinstance(text, str): + full_text.append(text) + return ' '.join(full_text) + diff --git a/apps/common/utils/rsa_util.py b/apps/common/utils/rsa_util.py new file mode 100644 index 000000000..3e20f5949 --- /dev/null +++ b/apps/common/utils/rsa_util.py @@ -0,0 +1,140 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: rsa_util.py + @date:2023/11/3 11:13 + @desc: +""" +import base64 +import threading + +from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher +from Crypto.PublicKey import RSA +from django.core import cache +from django.db.models import QuerySet + +from system_manage.models import SystemSetting, SettingType + +lock = threading.Lock() +rsa_cache = cache.caches['default'] +cache_key = "rsa_key" +# 对密钥加密的密码 +secret_code = "mac_kb_password" + + +def generate(): + """ + 生成 私钥秘钥对 + :return:{key:'公钥',value:'私钥'} + """ + # 生成一个 2048 位的密钥 + key = RSA.generate(2048) + + # 获取私钥 + encrypted_key = key.export_key(passphrase=secret_code, pkcs=8, + protection="scryptAndAES128-CBC") + return {'key': key.publickey().export_key(), 'value': encrypted_key} + + +def get_key_pair(): + rsa_value = rsa_cache.get(cache_key) + if rsa_value is None: + lock.acquire() + rsa_value = rsa_cache.get(cache_key) + if rsa_value is not None: + return rsa_value + try: + rsa_value = get_key_pair_by_sql() + rsa_cache.set(cache_key, rsa_value) + finally: + lock.release() + return rsa_value + + +def get_key_pair_by_sql(): + system_setting = QuerySet(SystemSetting).filter(type=SettingType.RSA.value).first() + if system_setting is None: + kv = generate() + system_setting = SystemSetting(type=SettingType.RSA.value, + meta={'key': kv.get('key').decode(), 'value': kv.get('value').decode()}) + system_setting.save() + return system_setting.meta + + +def encrypt(msg, public_key: str | None = None): + """ + 加密 + :param msg: 加密数据 + :param public_key: 公钥 + :return: 加密后的数据 + """ + if public_key is None: + public_key = get_key_pair().get('key') + cipher = PKCS1_cipher.new(RSA.importKey(public_key)) + encrypt_msg = cipher.encrypt(msg.encode("utf-8")) + return base64.b64encode(encrypt_msg).decode() + + +def decrypt(msg, pri_key: str | None = None): + """ + 解密 + :param msg: 需要解密的数据 + :param pri_key: 私钥 + :return: 解密后数据 + """ + if pri_key is None: + pri_key = get_key_pair().get('value') + cipher = PKCS1_cipher.new(RSA.importKey(pri_key, passphrase=secret_code)) + decrypt_data = cipher.decrypt(base64.b64decode(msg), 0) + return decrypt_data.decode("utf-8") + + +def rsa_long_encrypt(message, public_key: str | None = None, length=200): + """ + 超长文本加密 + + :param message: 需要加密的字符串 + :param public_key 公钥 + :param length: 1024bit的证书用100, 2048bit的证书用 200 + :return: 加密后的数据 + """ + # 读取公钥 + if public_key is None: + public_key = get_key_pair().get('key') + cipher = PKCS1_cipher.new(RSA.importKey(extern_key=public_key, + passphrase=secret_code)) + # 处理:Plaintext is too long. 分段加密 + if len(message) <= length: + # 对编码的数据进行加密,并通过base64进行编码 + result = base64.b64encode(cipher.encrypt(message.encode('utf-8'))) + else: + rsa_text = [] + # 对编码后的数据进行切片,原因:加密长度不能过长 + for i in range(0, len(message), length): + cont = message[i:i + length] + # 对切片后的数据进行加密,并新增到text后面 + rsa_text.append(cipher.encrypt(cont.encode('utf-8'))) + # 加密完进行拼接 + cipher_text = b''.join(rsa_text) + # base64进行编码 + result = base64.b64encode(cipher_text) + return result.decode() + + +def rsa_long_decrypt(message, pri_key: str | None = None, length=256): + """ + 超长文本解密,默认不加密 + :param message: 需要解密的数据 + :param pri_key: 秘钥 + :param length : 1024bit的证书用128,2048bit证书用256位 + :return: 解密后的数据 + """ + if pri_key is None: + pri_key = get_key_pair().get('value') + cipher = PKCS1_cipher.new(RSA.importKey(pri_key, passphrase=secret_code)) + base64_de = base64.b64decode(message) + res = [] + for i in range(0, len(base64_de), length): + res.append(cipher.decrypt(base64_de[i:i + length], 0)) + return b"".join(res).decode() diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index 3e3f18828..3d81fc99c 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -102,3 +102,5 @@ msgstr "用户管理" #: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25 msgid "Get current user information" msgstr "获取当前用户信息" + + diff --git a/apps/maxkb/settings/base.py b/apps/maxkb/settings/base.py index 1c601d49b..24fe69d92 100644 --- a/apps/maxkb/settings/base.py +++ b/apps/maxkb/settings/base.py @@ -41,7 +41,8 @@ INSTALLED_APPS = [ 'users.apps.UsersConfig', 'tools.apps.ToolConfig', 'common', - 'system_manage' + 'system_manage', + 'models_provider', ] MIDDLEWARE = [ diff --git a/apps/maxkb/urls.py b/apps/maxkb/urls.py index 7f06be935..341d8d3c9 100644 --- a/apps/maxkb/urls.py +++ b/apps/maxkb/urls.py @@ -22,7 +22,8 @@ from maxkb import settings urlpatterns = [ path("api/", include("users.urls")), - path("api/", include("tools.urls")) + path("api/", include("tools.urls")), + path("api/", include("models_provider.urls")), ] urlpatterns += [ path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由,下面两个ui也是根据这个配置文件来生成的 diff --git a/apps/models_provider/__init__.py b/apps/models_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/models_provider/admin.py b/apps/models_provider/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/models_provider/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/models_provider/api/__init__.py b/apps/models_provider/api/__init__.py new file mode 100644 index 000000000..9bad5790a --- /dev/null +++ b/apps/models_provider/api/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/apps/models_provider/api/model.py b/apps/models_provider/api/model.py new file mode 100644 index 000000000..870324a09 --- /dev/null +++ b/apps/models_provider/api/model.py @@ -0,0 +1,20 @@ +# coding=utf-8 + +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer +from models_provider.serializers.model import ModelCreateRequest, ModelModelSerializer + + +class ModelCreateResponse(ResultSerializer): + def get_data(self): + return ModelModelSerializer() + + +class ModelCreateAPI(APIMixin): + @staticmethod + def get_request(): + return ModelCreateRequest + + @staticmethod + def get_response(): + return ModelCreateResponse diff --git a/apps/models_provider/api/provide.py b/apps/models_provider/api/provide.py new file mode 100644 index 000000000..5b036386d --- /dev/null +++ b/apps/models_provider/api/provide.py @@ -0,0 +1,85 @@ +# coding=utf-8 +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer +from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ + + +class ProvideResponse(ResultSerializer): + def get_data(self): + return ProvideSerializer() + + +class ProvideSerializer(serializers.Serializer): + name = serializers.CharField(required=True, max_length=64, label=_("model name")) + provider = serializers.CharField(required=True, label=_("provider")) + icon = serializers.CharField(required=True, label=_("icon")) + + +class ProvideListSerializer(serializers.Serializer): + key = serializers.CharField(required=True, max_length=64, label=_("model name")) + value = serializers.CharField(required=True, label=_("value")) + + +class ModelListSerializer(serializers.Serializer): + name = serializers.CharField(required=True, label=_("model name")) + model_type = serializers.CharField(required=True, label=_("model type")) + desc = serializers.CharField(required=True, label=_("model name")) + + +class ProvideApi(APIMixin): + class ModelList(APIMixin): + @staticmethod + def get_query_params_api(): + return [OpenApiParameter( + # 参数的名称是done + name="model_type", + # 对参数的备注 + description="model_type", + # 指定参数的类型 + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + # 指定必须给 + required=False, + ), OpenApiParameter( + # 参数的名称是done + name="provider", + # 对参数的备注 + description="provider", + # 指定参数的类型 + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + # 指定必须给 + required=True, + ) + ] + + @staticmethod + def get_response(): + return serializers.ListSerializer(child=ModelListSerializer()) + + @staticmethod + def get_response(): + return ProvideResponse + + class ModelTypeList(APIMixin): + @staticmethod + def get_query_params_api(): + return [OpenApiParameter( + # 参数的名称是done + name="provider", + # 对参数的备注 + description="provider", + # 指定参数的类型 + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + # 指定必须给 + required=True, + )] + + @staticmethod + def get_response(): + return serializers.ListSerializer(child=ProvideListSerializer()) diff --git a/apps/models_provider/apps.py b/apps/models_provider/apps.py new file mode 100644 index 000000000..196c94d73 --- /dev/null +++ b/apps/models_provider/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ModelsProviderConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'models_provider' diff --git a/apps/models_provider/base_model_provider.py b/apps/models_provider/base_model_provider.py new file mode 100644 index 000000000..7f1dcffb9 --- /dev/null +++ b/apps/models_provider/base_model_provider.py @@ -0,0 +1,255 @@ +# coding=utf-8 + +from abc import ABC, abstractmethod +from enum import Enum +from functools import reduce +from typing import Dict, Iterator, Type, List + +from pydantic import BaseModel + +from common.exception.app_exception import AppApiException +from django.utils.translation import gettext_lazy as _ + +from common.utils.common import encryption + + +class DownModelChunkStatus(Enum): + success = "success" + error = "error" + pulling = "pulling" + unknown = 'unknown' + + +class ValidCode(Enum): + valid_error = 500 + model_not_fount = 404 + + +class DownModelChunk: + def __init__(self, status: DownModelChunkStatus, digest: str, progress: int, details: str, index: int): + self.details = details + self.status = status + self.digest = digest + self.progress = progress + self.index = index + + def to_dict(self): + return { + "details": self.details, + "status": self.status.value, + "digest": self.digest, + "progress": self.progress, + "index": self.index + } + + +class IModelProvider(ABC): + @abstractmethod + def get_model_info_manage(self): + pass + + @abstractmethod + def get_model_provide_info(self): + pass + + def get_model_type_list(self): + return self.get_model_info_manage().get_model_type_list() + + def get_model_list(self, model_type): + if model_type is None: + raise AppApiException(500, _('Model type cannot be empty')) + return self.get_model_info_manage().get_model_list_by_model_type(model_type) + + def get_model_credential(self, model_type, model_name): + model_info = self.get_model_info_manage().get_model_info(model_type, model_name) + return model_info.model_credential + + def get_model_params(self, model_type, model_name): + model_info = self.get_model_info_manage().get_model_info(model_type, model_name) + return model_info.model_credential + + def is_valid_credential(self, model_type, model_name, model_credential: Dict[str, object], + model_params: Dict[str, object], raise_exception=False): + model_info = self.get_model_info_manage().get_model_info(model_type, model_name) + return model_info.model_credential.is_valid(model_type, model_name, model_credential, model_params, self, + raise_exception=raise_exception) + + def get_model(self, model_type, model_name, model_credential: Dict[str, object], **model_kwargs) -> BaseModel: + model_info = self.get_model_info_manage().get_model_info(model_type, model_name) + return model_info.model_class.new_instance(model_type, model_name, model_credential, **model_kwargs) + + def get_dialogue_number(self): + return 3 + + def down_model(self, model_type: str, model_name, model_credential: Dict[str, object]) -> Iterator[DownModelChunk]: + raise AppApiException(500, _('The current platform does not support downloading models')) + + +class MaxKBBaseModel(ABC): + @staticmethod + @abstractmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + pass + + @staticmethod + def is_cache_model(): + return True + + @staticmethod + def filter_optional_params(model_kwargs): + optional_params = {} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming', 'show_ref_label']: + optional_params[key] = value + return optional_params + + +class BaseModelCredential(ABC): + + @abstractmethod + def is_valid(self, model_type: str, model_name, model: Dict[str, object], model_params, provider, + raise_exception=True): + pass + + @abstractmethod + def encryption_dict(self, model_info: Dict[str, object]): + """ + :param model_info: 模型数据 + :return: 加密后数据 + """ + pass + + def get_model_params_setting_form(self, model_name): + """ + 模型参数设置表单 + :return: + """ + pass + + @staticmethod + def encryption(message: str): + """ + 加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890 + :param message: + :return: + """ + return encryption(message) + + +class ModelTypeConst(Enum): + LLM = {'code': 'LLM', 'message': _('LLM')} + EMBEDDING = {'code': 'EMBEDDING', 'message': _('Embedding Model')} + STT = {'code': 'STT', 'message': _('Speech2Text')} + TTS = {'code': 'TTS', 'message': _('TTS')} + IMAGE = {'code': 'IMAGE', 'message': _('Vision Model')} + TTI = {'code': 'TTI', 'message': _('Image Generation')} + RERANKER = {'code': 'RERANKER', 'message': _('Rerank')} + + +class ModelInfo: + def __init__(self, name: str, desc: str, model_type: ModelTypeConst, model_credential: BaseModelCredential, + model_class: Type[MaxKBBaseModel], + **keywords): + self.name = name + self.desc = desc + self.model_type = model_type.name + self.model_credential = model_credential + self.model_class = model_class + if keywords is not None: + for key in keywords.keys(): + self.__setattr__(key, keywords.get(key)) + + def get_name(self): + """ + 获取模型名称 + :return: 模型名称 + """ + return self.name + + def get_desc(self): + """ + 获取模型描述 + :return: 模型描述 + """ + return self.desc + + def get_model_type(self): + return self.model_type + + def get_model_class(self): + return self.model_class + + def to_dict(self): + return reduce(lambda x, y: {**x, **y}, + [{attr: self.__getattribute__(attr)} for attr in vars(self) if + not attr.startswith("__") and not attr == 'model_credential' and not attr == 'model_class'], {}) + + +class ModelInfoManage: + def __init__(self): + self.model_dict = {} + self.model_list = [] + self.default_model_list = [] + self.default_model_dict = {} + + def append_model_info(self, model_info: ModelInfo): + self.model_list.append(model_info) + model_type_dict = self.model_dict.get(model_info.model_type) + if model_type_dict is None: + self.model_dict[model_info.model_type] = {model_info.name: model_info} + else: + model_type_dict[model_info.name] = model_info + + def append_default_model_info(self, model_info: ModelInfo): + self.default_model_list.append(model_info) + self.default_model_dict[model_info.model_type] = model_info + + def get_model_list(self): + return [model.to_dict() for model in self.model_list] + + def get_model_list_by_model_type(self, model_type): + return [model.to_dict() for model in self.model_list if model.model_type == model_type] + + def get_model_type_list(self): + return [{'key': _type.value.get('message'), 'value': _type.value.get('code')} for _type in ModelTypeConst if + len([model for model in self.model_list if model.model_type == _type.name]) > 0] + + def get_model_info(self, model_type, model_name) -> ModelInfo: + model_info = self.model_dict.get(model_type, {}).get(model_name, self.default_model_dict.get(model_type)) + if model_info is None: + raise AppApiException(500, _('The model does not support')) + return model_info + + class builder: + def __init__(self): + self.modelInfoManage = ModelInfoManage() + + def append_model_info(self, model_info: ModelInfo): + self.modelInfoManage.append_model_info(model_info) + return self + + def append_model_info_list(self, model_info_list: List[ModelInfo]): + for model_info in model_info_list: + self.modelInfoManage.append_model_info(model_info) + return self + + def append_default_model_info(self, model_info: ModelInfo): + self.modelInfoManage.append_default_model_info(model_info) + return self + + def build(self): + return self.modelInfoManage + + +class ModelProvideInfo: + def __init__(self, provider: str, name: str, icon: str): + self.provider = provider + + self.name = name + + self.icon = icon + + def to_dict(self): + return reduce(lambda x, y: {**x, **y}, + [{attr: self.__getattribute__(attr)} for attr in vars(self) if + not attr.startswith("__")], {}) diff --git a/apps/models_provider/constants/model_provider_constants.py b/apps/models_provider/constants/model_provider_constants.py new file mode 100644 index 000000000..07d683b8a --- /dev/null +++ b/apps/models_provider/constants/model_provider_constants.py @@ -0,0 +1,48 @@ +# coding=utf-8 +from enum import Enum + +from models_provider.impl.aliyun_bai_lian_model_provider.aliyun_bai_lian_model_provider import \ + AliyunBaiLianModelProvider +from models_provider.impl.anthropic_model_provider.anthropic_model_provider import AnthropicModelProvider +from models_provider.impl.aws_bedrock_model_provider.aws_bedrock_model_provider import BedrockModelProvider +from models_provider.impl.azure_model_provider.azure_model_provider import AzureModelProvider +from models_provider.impl.deepseek_model_provider.deepseek_model_provider import DeepSeekModelProvider +from models_provider.impl.gemini_model_provider.gemini_model_provider import GeminiModelProvider +from models_provider.impl.kimi_model_provider.kimi_model_provider import KimiModelProvider +from models_provider.impl.local_model_provider.local_model_provider import LocalModelProvider +from models_provider.impl.ollama_model_provider.ollama_model_provider import OllamaModelProvider +from models_provider.impl.openai_model_provider.openai_model_provider import OpenAIModelProvider +from models_provider.impl.qwen_model_provider.qwen_model_provider import QwenModelProvider +from models_provider.impl.siliconCloud_model_provider.siliconCloud_model_provider import SiliconCloudModelProvider +from models_provider.impl.tencent_cloud_model_provider.tencent_cloud_model_provider import TencentCloudModelProvider +from models_provider.impl.tencent_model_provider.tencent_model_provider import TencentModelProvider +from models_provider.impl.vllm_model_provider.vllm_model_provider import VllmModelProvider +from models_provider.impl.volcanic_engine_model_provider.volcanic_engine_model_provider import \ + VolcanicEngineModelProvider +from models_provider.impl.wenxin_model_provider.wenxin_model_provider import WenxinModelProvider +from models_provider.impl.xf_model_provider.xf_model_provider import XunFeiModelProvider +from models_provider.impl.xinference_model_provider.xinference_model_provider import XinferenceModelProvider +from models_provider.impl.zhipu_model_provider.zhipu_model_provider import ZhiPuModelProvider + + +class ModelProvideConstants(Enum): + model_azure_provider = AzureModelProvider() + model_wenxin_provider = WenxinModelProvider() + model_ollama_provider = OllamaModelProvider() + model_openai_provider = OpenAIModelProvider() + model_kimi_provider = KimiModelProvider() + model_qwen_provider = QwenModelProvider() + model_zhipu_provider = ZhiPuModelProvider() + model_xf_provider = XunFeiModelProvider() + model_deepseek_provider = DeepSeekModelProvider() + model_gemini_provider = GeminiModelProvider() + model_volcanic_engine_provider = VolcanicEngineModelProvider() + model_tencent_provider = TencentModelProvider() + model_tencent_cloud_provider = TencentCloudModelProvider() + model_aws_bedrock_provider = BedrockModelProvider() + model_local_provider = LocalModelProvider() + model_xinference_provider = XinferenceModelProvider() + model_vllm_provider = VllmModelProvider() + aliyun_bai_lian_model_provider = AliyunBaiLianModelProvider() + model_anthropic_provider = AnthropicModelProvider() + model_siliconCloud_provider = SiliconCloudModelProvider() diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/__init__.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/__init__.py new file mode 100644 index 000000000..3c10c5535 --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py + @date:2024/9/9 17:42 + @desc: +""" 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 new file mode 100644 index 000000000..590c6c42e --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/aliyun_bai_lian_model_provider.py @@ -0,0 +1,100 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: aliyun_bai_lian_model_provider.py + @date:2024/9/9 17:43 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import ModelProvideInfo, ModelTypeConst, ModelInfo, IModelProvider, \ + ModelInfoManage +from models_provider.impl.aliyun_bai_lian_model_provider.credential.embedding import \ + 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.reranker import \ + AliyunBaiLianRerankerCredential +from models_provider.impl.aliyun_bai_lian_model_provider.credential.stt import AliyunBaiLianSTTModelCredential +from models_provider.impl.aliyun_bai_lian_model_provider.credential.tti import QwenTextToImageModelCredential +from models_provider.impl.aliyun_bai_lian_model_provider.credential.tts import AliyunBaiLianTTSModelCredential +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.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 +from models_provider.impl.aliyun_bai_lian_model_provider.model.tts import AliyunBaiLianTextToSpeech +from maxkb.conf import PROJECT_DIR +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_embedding_model_credential = AliyunBaiLianEmbeddingCredential() +aliyun_bai_lian_llm_model_credential = BaiLianLLMModelCredential() +qwenvl_model_credential = QwenVLModelCredential() +qwentti_model_credential = QwenTextToImageModelCredential() + +model_info_list = [ModelInfo('gte-rerank', + _('With the GTE-Rerank text sorting series model developed by Alibaba Tongyi Lab, developers can integrate high-quality text retrieval and sorting through the LlamaIndex framework.'), + ModelTypeConst.RERANKER, aliyun_bai_lian_model_credential, AliyunBaiLianReranker), + ModelInfo('paraformer-realtime-v2', + _('Chinese (including various dialects such as Cantonese), English, Japanese, and Korean support free switching between multiple languages.'), + ModelTypeConst.STT, aliyun_bai_lian_stt_model_credential, AliyunBaiLianSpeechToText), + ModelInfo('cosyvoice-v1', + _('CosyVoice is based on a new generation of large generative speech models, which can predict emotions, intonation, rhythm, etc. based on context, and has better anthropomorphic effects.'), + ModelTypeConst.TTS, aliyun_bai_lian_tts_model_credential, AliyunBaiLianTextToSpeech), + ModelInfo('text-embedding-v1', + _("Universal text vector is Tongyi Lab's multi-language text unified vector model based on the LLM base. It provides high-level vector services for multiple mainstream languages around the world and helps developers quickly convert text data into high-quality vector data."), + ModelTypeConst.EMBEDDING, aliyun_bai_lian_embedding_model_credential, + AliyunBaiLianEmbedding), + ModelInfo('qwen-turbo', '', ModelTypeConst.LLM, aliyun_bai_lian_llm_model_credential, + BaiLianChatModel), + ModelInfo('qwen-plus', '', ModelTypeConst.LLM, aliyun_bai_lian_llm_model_credential, + BaiLianChatModel), + ModelInfo('qwen-max', '', ModelTypeConst.LLM, aliyun_bai_lian_llm_model_credential, + BaiLianChatModel) + ] + +module_info_vl_list = [ + ModelInfo('qwen-vl-max', '', ModelTypeConst.IMAGE, qwenvl_model_credential, QwenVLChatModel), + ModelInfo('qwen-vl-max-0809', '', ModelTypeConst.IMAGE, qwenvl_model_credential, QwenVLChatModel), + ModelInfo('qwen-vl-plus-0809', '', ModelTypeConst.IMAGE, qwenvl_model_credential, QwenVLChatModel), +] +module_info_tti_list = [ + ModelInfo('wanx-v1', + _('Tongyi Wanxiang - a large image model for text generation, supports bilingual input in Chinese and English, and supports the input of reference pictures for reference content or reference style migration. Key styles include but are not limited to watercolor, oil painting, Chinese painting, sketch, flat illustration, two-dimensional, and 3D. Cartoon.'), + ModelTypeConst.TTI, qwentti_model_credential, QwenTextToImageModel), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_model_info_list(module_info_vl_list) + .append_default_model_info(module_info_vl_list[0]) + .append_model_info_list(module_info_tti_list) + .append_default_model_info(module_info_tti_list[0]) + .append_default_model_info(model_info_list[1]) + .append_default_model_info(model_info_list[2]) + .append_default_model_info(model_info_list[3]) + .append_default_model_info(model_info_list[4]) + .append_default_model_info(model_info_list[0]) + .build() +) + + +class AliyunBaiLianModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='aliyun_bai_lian_model_provider', name=gettext('Alibaba Cloud Bailian'), + icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', + 'aliyun_bai_lian_model_provider', + 'icon', + 'aliyun_bai_lian_icon_svg'))) diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/embedding.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/embedding.py new file mode 100644 index 000000000..95da1e03b --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/embedding.py @@ -0,0 +1,74 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/10/16 17:01 + @desc: +""" +import traceback +from typing import Dict, Any + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.aliyun_bai_lian_model_provider.model.embedding import AliyunBaiLianEmbedding + + +class AliyunBaiLianEmbeddingCredential(BaseForm, BaseModelCredential): + + def is_valid( + self, + model_type: str, + model_name: str, + model_credential: Dict[str, Any], + model_params: Dict[str, Any], + provider: Any, + 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, + f"{model_type} Model type is not supported" + ) + required_keys = ['dashscope_api_key'] + missing_keys = [key for key in required_keys if key not in model_credential] + if missing_keys: + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + f"{', '.join(missing_keys)} is required" + ) + return False + + try: + model: AliyunBaiLianEmbedding = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_("Hello")) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + f"Verification failed, please check whether the parameters are correct: {e}" + ) + return False + + return True + + def encryption_dict(self, model: Dict[str, Any]) -> Dict[str, Any]: + """ + 加密敏感信息 + """ + api_key = model.get('dashscope_api_key', '') + return {**model, 'dashscope_api_key': super().encryption(api_key)} + + dashscope_api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/image.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/image.py new file mode 100644 index 000000000..903424fe1 --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/image.py @@ -0,0 +1,71 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:41 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QwenVLModelCredential(BaseForm, BaseModelCredential): + + def is_valid( + self, + model_type: str, + model_name: str, + model_credential: Dict[str, object], + model_params: dict, + 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, + gettext('{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, + gettext('{key} is required').format(key=key) + ) + return False + + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + gettext('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', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py new file mode 100644 index 000000000..409833b05 --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/llm.py @@ -0,0 +1,94 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from langchain_core.messages import HumanMessage +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class BaiLianLLMModelParams(BaseForm): + temperature = forms.SliderField( + TooltipLabel( + _('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic.') + ), + required=True, + default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2 + ) + + max_tokens = forms.SliderField( + TooltipLabel( + _('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate.') + ), + required=True, + default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0 + ) + + +class BaiLianLLMModelCredential(BaseForm, BaseModelCredential): + + api_base = 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, object], + model_params: dict, + 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, + gettext('{model_type} Model type is not supported').format(model_type=model_type) + ) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + gettext('{key} is required').format(key=key) + ) + return False + + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + gettext('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: str) -> BaiLianLLMModelParams: + return BaiLianLLMModelParams() diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/reranker.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/reranker.py new file mode 100644 index 000000000..f6131492a --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/reranker.py @@ -0,0 +1,85 @@ +# coding=utf-8 + +import traceback +from typing import Dict, Any + +from django.utils.translation import gettext as _ +from langchain_core.documents import Document + +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, PasswordInputField +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.aliyun_bai_lian_model_provider.model.reranker import AliyunBaiLianReranker + + +class AliyunBaiLianRerankerCredential(BaseForm, BaseModelCredential): + """ + Credential class for the Aliyun BaiLian Reranker model. + Provides validation and encryption for the model credentials. + """ + + dashscope_api_key = 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: + """ + Validate the model credentials. + + :param model_type: Type of the model (e.g., 'RERANKER'). + :param model_name: Name of the model. + :param model_credential: Dictionary containing the model credentials. + :param model_params: Parameters for the model. + :param provider: Model provider instance. + :param raise_exception: Whether to raise an exception on validation failure. + :return: Boolean indicating whether the credentials are valid. + """ + if model_type != 'RERANKER': + raise AppApiException( + ValidCode.valid_error.value, + _('{model_type} Model type is not supported').format(model_type=model_type) + ) + + required_keys = ['dashscope_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: AliyunBaiLianReranker = provider.get_model(model_type, model_name, model_credential) + model.compress_documents([Document(page_content=_('Hello'))], _('Hello')) + 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]: + """ + Encrypt sensitive fields in the model dictionary. + + :param model: Dictionary containing model details. + :return: Dictionary with encrypted sensitive fields. + """ + return { + **model, + 'dashscope_api_key': super().encryption(model.get('dashscope_api_key', '')) + } diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/stt.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/stt.py new file mode 100644 index 000000000..a071f66a7 --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/stt.py @@ -0,0 +1,92 @@ +# coding=utf-8 + +import traceback +from typing import Dict, Any + +from django.utils.translation import gettext as _ +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, PasswordInputField +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AliyunBaiLianSTTModelCredential(BaseForm, BaseModelCredential): + """ + Credential class for the Aliyun BaiLian STT (Speech-to-Text) model. + Provides validation and encryption for the model credentials. + """ + + api_key = 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: + """ + Validate the model credentials. + + :param model_type: Type of the model (e.g., 'STT'). + :param model_name: Name of the model. + :param model_credential: Dictionary containing the model credentials. + :param model_params: Parameters for the model. + :param provider: Model provider instance. + :param raise_exception: Whether to raise an exception on validation failure. + :return: Boolean indicating whether the credentials are valid. + """ + 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) + model.check_auth() + 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]: + """ + Encrypt sensitive fields in the model dictionary. + + :param model: Dictionary containing model details. + :return: Dictionary with encrypted sensitive fields. + """ + return { + **model, + 'api_key': super().encryption(model.get('api_key', '')) + } + + def get_model_params_setting_form(self, model_name: str): + """ + Get the parameter setting form for the specified model. + + :param model_name: Name of the model. + :return: Parameter setting form (not implemented). + """ + pass diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tti.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tti.py new file mode 100644 index 000000000..d256d6ce8 --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tti.py @@ -0,0 +1,147 @@ +# coding=utf-8 + +import traceback +from typing import Dict, Any + +from django.utils.translation import gettext_lazy as _, gettext + +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, PasswordInputField, SingleSelect, SliderField, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QwenModelParams(BaseForm): + """ + Parameters class for the Qwen Text-to-Image model. + Defines fields such as image size, number of images, and style. + """ + + size = SingleSelect( + TooltipLabel(_('Image size'), _('Specify the size of the generated image, such as: 1024x1024')), + required=True, + default_value='1024*1024', + option_list=[ + {'value': '1024*1024', 'label': '1024*1024'}, + {'value': '720*1280', 'label': '720*1280'}, + {'value': '768*1152', 'label': '768*1152'}, + {'value': '1280*720', 'label': '1280*720'}, + ], + text_field='label', + value_field='value' + ) + + n = SliderField( + TooltipLabel(_('Number of pictures'), _('Specify the number of generated images')), + required=True, + default_value=1, + _min=1, + _max=4, + _step=1, + precision=0 + ) + + style = SingleSelect( + TooltipLabel(_('Style'), _('Specify the style of generated images')), + required=True, + default_value='', + option_list=[ + {'value': '', 'label': _('Default value, the image style is randomly output by the model')}, + {'value': '', 'label': _('photography')}, + {'value': '', 'label': _('Portraits')}, + {'value': '<3d cartoon>', 'label': _('3D cartoon')}, + {'value': '', 'label': _('animation')}, + {'value': '', 'label': _('painting')}, + {'value': '', 'label': _('watercolor')}, + {'value': '', 'label': _('sketch')}, + {'value': '', 'label': _('Chinese painting')}, + {'value': '', 'label': _('flat illustration')}, + ], + text_field='label', + value_field='value' + ) + + +class QwenTextToImageModelCredential(BaseForm, BaseModelCredential): + """ + Credential class for the Qwen Text-to-Image model. + Provides validation and encryption for the model credentials. + """ + + api_key = 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: + """ + Validate the model credentials. + + :param model_type: Type of the model (e.g., 'TEXT_TO_IMAGE'). + :param model_name: Name of the model. + :param model_credential: Dictionary containing the model credentials. + :param model_params: Parameters for the model. + :param provider: Model provider instance. + :param raise_exception: Whether to raise an exception on validation failure. + :return: Boolean indicating whether the credentials are valid. + """ + 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, + gettext('{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, + gettext('{key} is required').format(key=key) + ) + return False + + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + gettext( + '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]: + """ + Encrypt sensitive fields in the model dictionary. + + :param model: Dictionary containing model details. + :return: Dictionary with encrypted sensitive fields. + """ + return { + **model, + 'api_key': super().encryption(model.get('api_key', '')) + } + + def get_model_params_setting_form(self, model_name: str): + """ + Get the parameter setting form for the specified model. + + :param model_name: Name of the model. + :return: Parameter setting form. + """ + return QwenModelParams() diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tts.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tts.py new file mode 100644 index 000000000..681b670fc --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/credential/tts.py @@ -0,0 +1,139 @@ +# coding=utf-8 + +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, PasswordInputField, SingleSelect, SliderField, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AliyunBaiLianTTSModelGeneralParams(BaseForm): + """ + Parameters class for the Aliyun BaiLian TTS (Text-to-Speech) model. + Defines fields such as voice and speech rate. + """ + + voice = SingleSelect( + TooltipLabel(_('Timbre'), _('Chinese sounds can support mixed scenes of Chinese and English')), + required=True, + default_value='longxiaochun', + text_field='value', + value_field='value', + option_list=[ + {'text': _('Long Xiaochun'), 'value': 'longxiaochun'}, + {'text': _('Long Xiaoxia'), 'value': 'longxiaoxia'}, + {'text': _('Long Xiaochen'), 'value': 'longxiaocheng'}, + {'text': _('Long Xiaobai'), 'value': 'longxiaobai'}, + {'text': _('Long Laotie'), 'value': 'longlaotie'}, + {'text': _('Long Shu'), 'value': 'longshu'}, + {'text': _('Long Shuo'), 'value': 'longshuo'}, + {'text': _('Long Jing'), 'value': 'longjing'}, + {'text': _('Long Miao'), 'value': 'longmiao'}, + {'text': _('Long Yue'), 'value': 'longyue'}, + {'text': _('Long Yuan'), 'value': 'longyuan'}, + {'text': _('Long Fei'), 'value': 'longfei'}, + {'text': _('Long Jielidou'), 'value': 'longjielidou'}, + {'text': _('Long Tong'), 'value': 'longtong'}, + {'text': _('Long Xiang'), 'value': 'longxiang'}, + {'text': 'Stella', 'value': 'loongstella'}, + {'text': 'Bella', 'value': 'loongbella'}, + ] + ) + + speech_rate = SliderField( + TooltipLabel(_('Speaking speed'), _('[0.5, 2], the default is 1, usually one decimal place is enough')), + required=True, + default_value=1, + _min=0.5, + _max=2, + _step=0.1, + precision=1 + ) + + +class AliyunBaiLianTTSModelCredential(BaseForm, BaseModelCredential): + """ + Credential class for the Aliyun BaiLian TTS (Text-to-Speech) model. + Provides validation and encryption for the model credentials. + """ + + api_key = PasswordInputField("API Key", required=True) + + def is_valid( + self, + model_type: str, + model_name: str, + model_credential: Dict[str, object], + model_params, + provider, + raise_exception: bool = False + ) -> bool: + """ + Validate the model credentials. + + :param model_type: Type of the model (e.g., 'TTS'). + :param model_name: Name of the model. + :param model_credential: Dictionary containing the model credentials. + :param model_params: Parameters for the model. + :param provider: Model provider instance. + :param raise_exception: Whether to raise an exception on validation failure. + :return: Boolean indicating whether the credentials are valid. + """ + 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, + gettext('{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, + gettext('{key} is required').format(key=key) + ) + return False + + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException( + ValidCode.valid_error.value, + gettext( + '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]: + """ + Encrypt sensitive fields in the model dictionary. + + :param model: Dictionary containing model details. + :return: Dictionary with encrypted sensitive fields. + """ + return { + **model, + 'api_key': super().encryption(model.get('api_key', '')) + } + + def get_model_params_setting_form(self, model_name: str): + """ + Get the parameter setting form for the specified model. + + :param model_name: Name of the model. + :return: Parameter setting form. + """ + return AliyunBaiLianTTSModelGeneralParams() diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/icon/aliyun_bai_lian_icon_svg b/apps/models_provider/impl/aliyun_bai_lian_model_provider/icon/aliyun_bai_lian_icon_svg new file mode 100644 index 000000000..0678828dd --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/icon/aliyun_bai_lian_icon_svg @@ -0,0 +1 @@ +【icon】阿里百炼大模型 \ No newline at end of file diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/embedding.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/embedding.py new file mode 100644 index 000000000..0316782dd --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/embedding.py @@ -0,0 +1,66 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/10/16 16:34 + @desc: +""" +from functools import reduce +from typing import Dict, List + +from langchain_community.embeddings import DashScopeEmbeddings +from langchain_community.embeddings.dashscope import embed_with_retry + +from models_provider.base_model_provider import MaxKBBaseModel + + +def proxy_embed_documents(texts: List[str], step_size, embed_documents): + value = [embed_documents(texts[start_index:start_index + step_size]) for start_index in + range(0, len(texts), step_size)] + return reduce(lambda x, y: [*x, *y], value, []) + + +class AliyunBaiLianEmbedding(MaxKBBaseModel, DashScopeEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return AliyunBaiLianEmbedding( + model=model_name, + dashscope_api_key=model_credential.get('dashscope_api_key') + ) + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + if self.model == 'text-embedding-v3': + return proxy_embed_documents(texts, 6, self._embed_documents) + return self._embed_documents(texts) + + def _embed_documents(self, texts: List[str]) -> List[List[float]]: + """Call out to DashScope's embedding endpoint for embedding search docs. + + Args: + texts: The list of texts to embed. + chunk_size: The chunk size of embeddings. If None, will use the chunk size + specified by the class. + + Returns: + List of embeddings, one for each text. + """ + embeddings = embed_with_retry( + self, input=texts, text_type="document", model=self.model + ) + embedding_list = [item["embedding"] for item in embeddings] + return embedding_list + + def embed_query(self, text: str) -> List[float]: + """Call out to DashScope's embedding endpoint for embedding query text. + + Args: + text: The text to embed. + + Returns: + Embedding for the text. + """ + embedding = embed_with_retry( + self, input=[text], text_type="document", model=self.model + )[0]["embedding"] + return embedding diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/iat_mp3_16k.mp3 b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/iat_mp3_16k.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..75e744c8ff5188208a3797561efb2e096d4fa015 GIT binary patch literal 17876 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', 'n': 1}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + chat_tong_yi = QwenTextToImageModel( + model_name=model_name, + api_key=model_credential.get('api_key'), + **optional_params, + ) + return chat_tong_yi + + def is_cache_model(self): + return False + + def check_auth(self): + chat = ChatTongyi(api_key=self.api_key, model_name='qwen-max') + chat.invoke([HumanMessage([{"type": "text", "text": gettext('Hello')}])]) + + def generate_image(self, prompt: str, negative_prompt: str = None): + # api_base='https://dashscope.aliyuncs.com/compatible-mode/v1', + rsp = ImageSynthesis.call(api_key=self.api_key, + model=self.model_name, + prompt=prompt, + negative_prompt=negative_prompt, + **self.params) + file_urls = [] + if rsp.status_code == HTTPStatus.OK: + for result in rsp.output.results: + file_urls.append(result.url) + else: + print('sync_call Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + return file_urls diff --git a/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py new file mode 100644 index 000000000..a42d1824c --- /dev/null +++ b/apps/models_provider/impl/aliyun_bai_lian_model_provider/model/tts.py @@ -0,0 +1,57 @@ +from typing import Dict + +import dashscope + +from django.utils.translation import gettext as _ + +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech + + +class AliyunBaiLianTextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + api_key: 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') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'voice': 'longxiaochun', 'speech_rate': 1.0}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + + return AliyunBaiLianTextToSpeech( + model=model_name, + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + self.text_to_speech(_('Hello')) + + def text_to_speech(self, text): + dashscope.api_key = self.api_key + text = _remove_empty_lines(text) + if 'sambert' in self.model: + from dashscope.audio.tts import SpeechSynthesizer + audio = SpeechSynthesizer.call(model=self.model, text=text, **self.params).get_audio_data() + else: + from dashscope.audio.tts_v2 import SpeechSynthesizer + synthesizer = SpeechSynthesizer(model=self.model, **self.params) + audio = synthesizer.call(text) + if audio is None: + raise Exception('Failed to generate audio') + if type(audio) == str: + print(audio) + raise Exception(audio) + return audio + + def is_cache_model(self): + return False diff --git a/apps/models_provider/impl/anthropic_model_provider/__init__.py b/apps/models_provider/impl/anthropic_model_provider/__init__.py new file mode 100644 index 000000000..2dc4ab10d --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/3/28 16:25 + @desc: +""" diff --git a/apps/models_provider/impl/anthropic_model_provider/anthropic_model_provider.py b/apps/models_provider/impl/anthropic_model_provider/anthropic_model_provider.py new file mode 100644 index 000000000..33094337e --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/anthropic_model_provider.py @@ -0,0 +1,62 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: openai_model_provider.py + @date:2024/3/28 16:26 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, \ + ModelTypeConst, ModelInfoManage +from models_provider.impl.anthropic_model_provider.credential.image import AnthropicImageModelCredential +from models_provider.impl.anthropic_model_provider.credential.llm import AnthropicLLMModelCredential +from models_provider.impl.anthropic_model_provider.model.image import AnthropicImage +from models_provider.impl.anthropic_model_provider.model.llm import AnthropicChatModel +from maxkb.conf import PROJECT_DIR + +openai_llm_model_credential = AnthropicLLMModelCredential() +openai_image_model_credential = AnthropicImageModelCredential() + +model_info_list = [ + ModelInfo('claude-3-opus-20240229', '', ModelTypeConst.LLM, + openai_llm_model_credential, AnthropicChatModel + ), + ModelInfo('claude-3-sonnet-20240229', '', ModelTypeConst.LLM, openai_llm_model_credential, + AnthropicChatModel), + ModelInfo('claude-3-haiku-20240307', '', ModelTypeConst.LLM, openai_llm_model_credential, + AnthropicChatModel), + ModelInfo('claude-3-5-sonnet-20240620', '', ModelTypeConst.LLM, openai_llm_model_credential, + AnthropicChatModel), + ModelInfo('claude-3-5-haiku-20241022', '', ModelTypeConst.LLM, openai_llm_model_credential, + AnthropicChatModel), + ModelInfo('claude-3-5-sonnet-20241022', '', ModelTypeConst.LLM, openai_llm_model_credential, + AnthropicChatModel), +] + +image_model_info = [ + ModelInfo('claude-3-5-sonnet-20241022', '', ModelTypeConst.IMAGE, openai_image_model_credential, + AnthropicImage), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info(model_info_list[0]) + .append_model_info_list(image_model_info) + .append_default_model_info(image_model_info[0]) + .build() +) + + +class AnthropicModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_anthropic_provider', name='Anthropic', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'anthropic_model_provider', 'icon', + 'anthropic_icon_svg'))) diff --git a/apps/models_provider/impl/anthropic_model_provider/credential/image.py b/apps/models_provider/impl/anthropic_model_provider/credential/image.py new file mode 100644 index 000000000..50bec62ce --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/credential/image.py @@ -0,0 +1,53 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AnthropicImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField(_('API URL'), required=True) + api_key = forms.PasswordInputField(_('API Key'), required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext("Hello")}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/anthropic_model_provider/credential/llm.py b/apps/models_provider/impl/anthropic_model_provider/credential/llm.py new file mode 100644 index 000000000..38e0b3cf5 --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/credential/llm.py @@ -0,0 +1,78 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:32 + @desc: +""" +import traceback +from typing import Dict + +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext_lazy as _, gettext + + +class AnthropicLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class AnthropicLLMModelCredential(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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField(_('API URL'), required=True) + api_key = forms.PasswordInputField(_('API Key'), required=True) + + def get_model_params_setting_form(self, model_name): + return AnthropicLLMModelParams() diff --git a/apps/models_provider/impl/anthropic_model_provider/icon/anthropic_icon_svg b/apps/models_provider/impl/anthropic_model_provider/icon/anthropic_icon_svg new file mode 100644 index 000000000..342d40be8 --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/icon/anthropic_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/anthropic_model_provider/model/image.py b/apps/models_provider/impl/anthropic_model_provider/model/image.py new file mode 100644 index 000000000..f4a3935d6 --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/model/image.py @@ -0,0 +1,26 @@ +from typing import Dict + +from langchain_anthropic import ChatAnthropic + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class AnthropicImage(MaxKBBaseModel, ChatAnthropic): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return AnthropicImage( + model=model_name, + anthropic_api_url=model_credential.get('api_base'), + anthropic_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/anthropic_model_provider/model/llm.py b/apps/models_provider/impl/anthropic_model_provider/model/llm.py new file mode 100644 index 000000000..1a297c46e --- /dev/null +++ b/apps/models_provider/impl/anthropic_model_provider/model/llm.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/4/18 15:28 + @desc: +""" +from typing import List, Dict + +from langchain_anthropic import ChatAnthropic +from langchain_core.messages import BaseMessage, get_buffer_string + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class AnthropicChatModel(MaxKBBaseModel, ChatAnthropic): + + @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) + azure_chat_open_ai = AnthropicChatModel( + model=model_name, + anthropic_api_url=model_credential.get('api_base'), + anthropic_api_key=model_credential.get('api_key'), + **optional_params, + custom_get_token_ids=custom_get_token_ids + ) + return azure_chat_open_ai + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + try: + return super().get_num_tokens_from_messages(messages) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + + def get_num_tokens(self, text: str) -> int: + try: + return super().get_num_tokens(text) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/__init__.py b/apps/models_provider/impl/aws_bedrock_model_provider/__init__.py new file mode 100644 index 000000000..8cb7f459e --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/aws_bedrock_model_provider.py b/apps/models_provider/impl/aws_bedrock_model_provider/aws_bedrock_model_provider.py new file mode 100644 index 000000000..57c52fc9c --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/aws_bedrock_model_provider.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +from common.utils.common import get_file_content +from models_provider.base_model_provider import ( + IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, ModelInfoManage +) +from models_provider.impl.aws_bedrock_model_provider.credential.embedding import BedrockEmbeddingCredential +from models_provider.impl.aws_bedrock_model_provider.credential.llm import BedrockLLMModelCredential +from models_provider.impl.aws_bedrock_model_provider.model.embedding import BedrockEmbeddingModel +from models_provider.impl.aws_bedrock_model_provider.model.llm import BedrockModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + + +def _create_model_info(model_name, description, model_type, credential_class, model_class): + return ModelInfo( + name=model_name, + desc=description, + model_type=model_type, + model_credential=credential_class(), + model_class=model_class + ) + + +def _get_aws_bedrock_icon_path(): + return os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'aws_bedrock_model_provider', + 'icon', 'bedrock_icon_svg') + + +def _initialize_model_info(): + model_info_list = [ + _create_model_info( + 'anthropic.claude-v2:1', + _('An update to Claude 2 that doubles the context window and improves reliability, hallucination rates, and evidence-based accuracy in long documents and RAG contexts.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'anthropic.claude-v2', + _('Anthropic is a powerful model that can handle a variety of tasks, from complex dialogue and creative content generation to detailed command obedience.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'anthropic.claude-3-haiku-20240307-v1:0', + _("The Claude 3 Haiku is Anthropic's fastest and most compact model, with near-instant responsiveness. The model can answer simple queries and requests quickly. Customers will be able to build seamless AI experiences that mimic human interactions. Claude 3 Haiku can process images and return text output, and provides 200K context windows."), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'anthropic.claude-3-sonnet-20240229-v1:0', + _("The Claude 3 Sonnet model from Anthropic strikes the ideal balance between intelligence and speed, especially when it comes to handling enterprise workloads. This model offers maximum utility while being priced lower than competing products, and it's been engineered to be a solid choice for deploying AI at scale."), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'anthropic.claude-3-5-sonnet-20240620-v1:0', + _('The Claude 3.5 Sonnet raises the industry standard for intelligence, outperforming competing models and the Claude 3 Opus in extensive evaluations, with the speed and cost-effectiveness of our mid-range models.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'anthropic.claude-instant-v1', + _('A faster, more affordable but still very powerful model that can handle a range of tasks including casual conversation, text analysis, summarization and document question answering.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'amazon.titan-text-premier-v1:0', + _("Titan Text Premier is the most powerful and advanced model in the Titan Text series, designed to deliver exceptional performance for a variety of enterprise applications. With its cutting-edge features, it delivers greater accuracy and outstanding results, making it an excellent choice for organizations looking for a top-notch text processing solution."), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel + ), + _create_model_info( + 'amazon.titan-text-lite-v1', + _('Amazon Titan Text Lite is a lightweight, efficient model ideal for fine-tuning English-language tasks, including summarization and copywriting, where customers require smaller, more cost-effective, and highly customizable models.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel), + _create_model_info( + 'amazon.titan-text-express-v1', + _('Amazon Titan Text Express has context lengths of up to 8,000 tokens, making it ideal for a variety of high-level general language tasks, such as open-ended text generation and conversational chat, as well as support in retrieval-augmented generation (RAG). At launch, the model is optimized for English, but other languages are supported.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel), + _create_model_info( + 'mistral.mistral-7b-instruct-v0:2', + _('7B dense converter for rapid deployment and easy customization. Small in size yet powerful in a variety of use cases. Supports English and code, as well as 32k context windows.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel), + _create_model_info( + 'mistral.mistral-large-2402-v1:0', + _('Advanced Mistral AI large-scale language model capable of handling any language task, including complex multilingual reasoning, text understanding, transformation, and code generation.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel), + _create_model_info( + 'meta.llama3-70b-instruct-v1:0', + _('Ideal for content creation, conversational AI, language understanding, R&D, and enterprise applications'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel), + _create_model_info( + 'meta.llama3-8b-instruct-v1:0', + _('Ideal for limited computing power and resources, edge devices, and faster training times.'), + ModelTypeConst.LLM, + BedrockLLMModelCredential, + BedrockModel), + ] + embedded_model_info_list = [ + _create_model_info( + 'amazon.titan-embed-text-v1', + _('Titan Embed Text is the largest embedding model in the Amazon Titan Embed series and can handle various text embedding tasks, such as text classification, text similarity calculation, etc.'), + ModelTypeConst.EMBEDDING, + BedrockEmbeddingCredential, + BedrockEmbeddingModel + ), + ] + + model_info_manage = ModelInfoManage.builder() \ + .append_model_info_list(model_info_list) \ + .append_default_model_info(model_info_list[0]) \ + .append_model_info_list(embedded_model_info_list) \ + .append_default_model_info(embedded_model_info_list[0]) \ + .build() + + return model_info_manage + + +class BedrockModelProvider(IModelProvider): + def __init__(self): + self._model_info_manage = _initialize_model_info() + + def get_model_info_manage(self): + return self._model_info_manage + + def get_model_provide_info(self): + icon_path = _get_aws_bedrock_icon_path() + icon_data = get_file_content(icon_path) + return ModelProvideInfo( + provider='model_aws_bedrock_provider', + name='Amazon Bedrock', + icon=icon_data + ) diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/credential/embedding.py b/apps/models_provider/impl/aws_bedrock_model_provider/credential/embedding.py new file mode 100644 index 000000000..6a7192a92 --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/credential/embedding.py @@ -0,0 +1,53 @@ +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.aws_bedrock_model_provider.model.embedding import BedrockEmbeddingModel + + +class BedrockEmbeddingCredential(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(mt.get('value') == model_type for mt in model_type_list): + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + _('{model_type} Model type is not supported').format(model_type=model_type)) + return False + + required_keys = ['region_name', 'access_key_id', 'secret_access_key'] + if not all(key in model_credential for key in required_keys): + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + _('The following fields are required: {keys}').format( + keys=", ".join(required_keys))) + return False + + try: + model: BedrockEmbeddingModel = provider.get_model(model_type, model_name, model_credential) + aa = model.embed_query(_('Hello')) + print(aa) + except AppApiException: + raise + except Exception as e: + traceback.print_exc() + 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]): + return {**model, 'secret_access_key': super().encryption(model.get('secret_access_key', ''))} + + region_name = forms.TextInputField('Region Name', required=True) + access_key_id = forms.TextInputField('Access Key ID', required=True) + secret_access_key = forms.PasswordInputField('Secret Access Key', required=True) diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/credential/llm.py b/apps/models_provider/impl/aws_bedrock_model_provider/credential/llm.py new file mode 100644 index 000000000..358e66309 --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/credential/llm.py @@ -0,0 +1,76 @@ +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import ValidCode, BaseModelCredential + + +class BedrockLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class BedrockLLMModelCredential(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(mt.get('value') == model_type for mt in model_type_list): + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + return False + + required_keys = ['region_name', 'access_key_id', 'secret_access_key'] + if not all(key in model_credential for key in required_keys): + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('The following fields are required: {keys}').format( + keys=", ".join(required_keys))) + return False + + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except AppApiException: + raise + except Exception as e: + traceback.print_exc() + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + '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]): + return {**model, 'secret_access_key': super().encryption(model.get('secret_access_key', ''))} + + region_name = forms.TextInputField('Region Name', required=True) + access_key_id = forms.TextInputField('Access Key ID', required=True) + secret_access_key = forms.PasswordInputField('Secret Access Key', required=True) + base_url = forms.TextInputField('Proxy URL', required=False) + + def get_model_params_setting_form(self, model_name): + return BedrockLLMModelParams() diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/icon/bedrock_icon_svg b/apps/models_provider/impl/aws_bedrock_model_provider/icon/bedrock_icon_svg new file mode 100644 index 000000000..5f176a7d2 --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/icon/bedrock_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/model/embedding.py b/apps/models_provider/impl/aws_bedrock_model_provider/model/embedding.py new file mode 100644 index 000000000..f837b12cb --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/model/embedding.py @@ -0,0 +1,60 @@ +from langchain_community.embeddings import BedrockEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel +from typing import Dict, List + +from models_provider.impl.aws_bedrock_model_provider.model.llm import _update_aws_credentials + + +class BedrockEmbeddingModel(MaxKBBaseModel, BedrockEmbeddings): + def __init__(self, model_id: str, region_name: str, credentials_profile_name: str, + **kwargs): + super().__init__(model_id=model_id, region_name=region_name, + credentials_profile_name=credentials_profile_name, **kwargs) + + @classmethod + def new_instance(cls, model_type: str, model_name: str, model_credential: Dict[str, str], + **model_kwargs) -> 'BedrockModel': + _update_aws_credentials(model_credential['access_key_id'], model_credential['access_key_id'], + model_credential['secret_access_key']) + return cls( + model_id=model_name, + region_name=model_credential['region_name'], + credentials_profile_name=model_credential['access_key_id'], + ) + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Compute doc embeddings using a Bedrock model. + + Args: + texts: The list of texts to embed + + Returns: + List of embeddings, one for each text. + """ + results = [] + for text in texts: + response = self._embedding_func(text) + + if self.normalize: + response = self._normalize_vector(response) + + results.append(response) + + return results + + def embed_query(self, text: str) -> List[float]: + """Compute query embeddings using a Bedrock model. + + Args: + text: The text to embed. + + Returns: + Embeddings for the text. + """ + embedding = self._embedding_func(text) + + if self.normalize: + return self._normalize_vector(embedding) + + return embedding diff --git a/apps/models_provider/impl/aws_bedrock_model_provider/model/llm.py b/apps/models_provider/impl/aws_bedrock_model_provider/model/llm.py new file mode 100644 index 000000000..7b2b7668b --- /dev/null +++ b/apps/models_provider/impl/aws_bedrock_model_provider/model/llm.py @@ -0,0 +1,88 @@ +import os +import re +from typing import Dict + +from botocore.config import Config +from langchain_community.chat_models import BedrockChat + +from models_provider.base_model_provider import MaxKBBaseModel + + +def get_max_tokens_keyword(model_name): + """ + 根据模型名称返回正确的 max_tokens 关键字。 + + :param model_name: 模型名称字符串 + :return: 对应的 max_tokens 关键字字符串 + """ + maxTokens = ["ai21.j2-ultra-v1", "ai21.j2-mid-v1"] + # max_tokens_to_sample = ["anthropic.claude-v2:1", "anthropic.claude-v2", "anthropic.claude-instant-v1"] + maxTokenCount = ["amazon.titan-text-lite-v1", "amazon.titan-text-express-v1"] + max_new_tokens = [ + "us.meta.llama3-2-1b-instruct-v1:0", "us.meta.llama3-2-3b-instruct-v1:0", "us.meta.llama3-2-11b-instruct-v1:0", + "us.meta.llama3-2-90b-instruct-v1:0"] + if model_name in maxTokens: + return 'maxTokens' + elif model_name in maxTokenCount: + return 'maxTokenCount' + elif model_name in max_new_tokens: + return 'max_new_tokens' + else: + return 'max_tokens' + + +class BedrockModel(MaxKBBaseModel, BedrockChat): + + @staticmethod + def is_cache_model(): + return False + + def __init__(self, model_id: str, region_name: str, credentials_profile_name: str, + streaming: bool = False, config: Config = None, **kwargs): + super().__init__(model_id=model_id, region_name=region_name, + credentials_profile_name=credentials_profile_name, streaming=streaming, config=config, + **kwargs) + + @classmethod + def new_instance(cls, model_type: str, model_name: str, model_credential: Dict[str, str], + **model_kwargs) -> 'BedrockModel': + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + + config = {} + # 判断model_kwargs是否包含 base_url 且不为空 + if 'base_url' in model_credential and model_credential['base_url']: + proxy_url = model_credential['base_url'] + config = Config( + proxies={ + 'http': proxy_url, + 'https': proxy_url + }, + connect_timeout=60, + read_timeout=60 + ) + _update_aws_credentials(model_credential['access_key_id'], model_credential['access_key_id'], + model_credential['secret_access_key']) + + return cls( + model_id=model_name, + region_name=model_credential['region_name'], + credentials_profile_name=model_credential['access_key_id'], + streaming=model_kwargs.pop('streaming', True), + model_kwargs=optional_params, + config=config + ) + + +def _update_aws_credentials(profile_name, access_key_id, secret_access_key): + credentials_path = os.path.join(os.path.expanduser("~"), ".aws", "credentials") + os.makedirs(os.path.dirname(credentials_path), exist_ok=True) + + content = open(credentials_path, 'r').read() if os.path.exists(credentials_path) else '' + pattern = rf'\n*\[{profile_name}\]\n*(aws_access_key_id = .*)\n*(aws_secret_access_key = .*)\n*' + content = re.sub(pattern, '', content, flags=re.DOTALL) + + if not re.search(rf'\[{profile_name}\]', content): + content += f"\n[{profile_name}]\naws_access_key_id = {access_key_id}\naws_secret_access_key = {secret_access_key}\n" + + with open(credentials_path, 'w') as file: + file.write(content) diff --git a/apps/models_provider/impl/azure_model_provider/__init__.py b/apps/models_provider/impl/azure_model_provider/__init__.py new file mode 100644 index 000000000..53b7001e5 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2023/10/31 17:16 + @desc: +""" diff --git a/apps/models_provider/impl/azure_model_provider/azure_model_provider.py b/apps/models_provider/impl/azure_model_provider/azure_model_provider.py new file mode 100644 index 000000000..39cf44ced --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/azure_model_provider.py @@ -0,0 +1,116 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: azure_model_provider.py + @date:2023/10/31 16:19 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, \ + ModelTypeConst, ModelInfoManage +from models_provider.impl.azure_model_provider.credential.embedding import AzureOpenAIEmbeddingCredential +from models_provider.impl.azure_model_provider.credential.image import AzureOpenAIImageModelCredential +from models_provider.impl.azure_model_provider.credential.llm import AzureLLMModelCredential +from models_provider.impl.azure_model_provider.credential.stt import AzureOpenAISTTModelCredential +from models_provider.impl.azure_model_provider.credential.tti import AzureOpenAITextToImageModelCredential +from models_provider.impl.azure_model_provider.credential.tts import AzureOpenAITTSModelCredential +from models_provider.impl.azure_model_provider.model.azure_chat_model import AzureChatModel +from models_provider.impl.azure_model_provider.model.embedding import AzureOpenAIEmbeddingModel +from models_provider.impl.azure_model_provider.model.image import AzureOpenAIImage +from models_provider.impl.azure_model_provider.model.stt import AzureOpenAISpeechToText +from models_provider.impl.azure_model_provider.model.tti import AzureOpenAITextToImage +from models_provider.impl.azure_model_provider.model.tts import AzureOpenAITextToSpeech +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext_lazy as _ + +base_azure_llm_model_credential = AzureLLMModelCredential() +base_azure_embedding_model_credential = AzureOpenAIEmbeddingCredential() +base_azure_image_model_credential = AzureOpenAIImageModelCredential() +base_azure_tti_model_credential = AzureOpenAITextToImageModelCredential() +base_azure_tts_model_credential = AzureOpenAITTSModelCredential() +base_azure_stt_model_credential = AzureOpenAISTTModelCredential() + +default_model_info = [ + ModelInfo('Azure OpenAI', '', ModelTypeConst.LLM, + base_azure_llm_model_credential, AzureChatModel, api_version='2024-02-15-preview' + ), + ModelInfo('gpt-4', '', ModelTypeConst.LLM, + base_azure_llm_model_credential, AzureChatModel, api_version='2024-02-15-preview' + ), + ModelInfo('gpt-4o', '', ModelTypeConst.LLM, + base_azure_llm_model_credential, AzureChatModel, api_version='2024-02-15-preview' + ), + ModelInfo('gpt-4o-mini', '', ModelTypeConst.LLM, + base_azure_llm_model_credential, AzureChatModel, api_version='2024-02-15-preview' + ), +] + +embedding_model_info = [ + ModelInfo('text-embedding-3-large', '', ModelTypeConst.EMBEDDING, + base_azure_embedding_model_credential, AzureOpenAIEmbeddingModel, api_version='2023-05-15' + ), + ModelInfo('text-embedding-3-small', '', ModelTypeConst.EMBEDDING, + base_azure_embedding_model_credential, AzureOpenAIEmbeddingModel, api_version='2023-05-15' + ), + ModelInfo('text-embedding-ada-002', '', ModelTypeConst.EMBEDDING, + base_azure_embedding_model_credential, AzureOpenAIEmbeddingModel, api_version='2023-05-15' + ), +] + +image_model_info = [ + ModelInfo('gpt-4o', '', ModelTypeConst.IMAGE, + base_azure_image_model_credential, AzureOpenAIImage, api_version='2023-05-15' + ), + ModelInfo('gpt-4o-mini', '', ModelTypeConst.IMAGE, + base_azure_image_model_credential, AzureOpenAIImage, api_version='2023-05-15' + ), +] + +tti_model_info = [ + ModelInfo('dall-e-3', '', ModelTypeConst.TTI, + base_azure_tti_model_credential, AzureOpenAITextToImage, api_version='2023-05-15' + ), +] + +tts_model_info = [ + ModelInfo('tts', '', ModelTypeConst.TTS, + base_azure_tts_model_credential, AzureOpenAITextToSpeech, api_version='2023-05-15' + ), +] + +stt_model_info = [ + ModelInfo('whisper', '', ModelTypeConst.STT, + base_azure_stt_model_credential, AzureOpenAISpeechToText, api_version='2023-05-15' + ), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_default_model_info(default_model_info[0]) + .append_model_info_list(default_model_info) + .append_model_info_list(embedding_model_info) + .append_default_model_info(embedding_model_info[0]) + .append_model_info_list(image_model_info) + .append_default_model_info(image_model_info[0]) + .append_model_info_list(stt_model_info) + .append_default_model_info(stt_model_info[0]) + .append_model_info_list(tts_model_info) + .append_default_model_info(tts_model_info[0]) + .append_model_info_list(tti_model_info) + .append_default_model_info(tti_model_info[0]) + .build() +) + + +class AzureModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_azure_provider', name='Azure OpenAI', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'azure_model_provider', 'icon', + 'azure_icon_svg'))) diff --git a/apps/models_provider/impl/azure_model_provider/credential/embedding.py b/apps/models_provider/impl/azure_model_provider/credential/embedding.py new file mode 100644 index 000000000..329b7acbd --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/credential/embedding.py @@ -0,0 +1,57 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 17:08 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AzureOpenAIEmbeddingCredential(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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key', 'api_version']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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')) + else: + return False + + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_version = forms.TextInputField("Api Version", required=True) + + api_base = forms.TextInputField('Azure Endpoint', required=True) + + api_key = forms.PasswordInputField("API Key", required=True) diff --git a/apps/models_provider/impl/azure_model_provider/credential/image.py b/apps/models_provider/impl/azure_model_provider/credential/image.py new file mode 100644 index 000000000..14ca792b1 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/credential/image.py @@ -0,0 +1,75 @@ +# coding=utf-8 +import base64 +import os +import traceback +from typing import Dict + +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext_lazy as _, gettext + + +class AzureOpenAIImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class AzureOpenAIImageModelCredential(BaseForm, BaseModelCredential): + api_version = forms.TextInputField("API Version", required=True) + api_base = forms.TextInputField('Azure Endpoint', required=True) + api_key = forms.PasswordInputField("API Key", required=True) + + 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)) + + for key in ['api_base', 'api_key', 'api_version']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return AzureOpenAIImageModelParams() diff --git a/apps/models_provider/impl/azure_model_provider/credential/llm.py b/apps/models_provider/impl/azure_model_provider/credential/llm.py new file mode 100644 index 000000000..1e1967f6f --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/credential/llm.py @@ -0,0 +1,96 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 17:08 + @desc: +""" +import traceback +from typing import Dict + +from langchain_core.messages import HumanMessage +from openai import BadRequestError + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext_lazy as _, gettext + + +class AzureLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class o3MiniLLMModelParams(BaseForm): + max_completion_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=5000, + _step=1, + precision=0) + + +class AzureLLMModelCredential(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)) + + for key in ['api_base', 'api_key', 'deployment_name', 'api_version']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException) or isinstance(e, BadRequestError): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('Verification failed, please check whether the parameters are correct')) + else: + return False + + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_version = forms.TextInputField("API Version", required=True) + + api_base = forms.TextInputField('Azure Endpoint', required=True) + + api_key = forms.PasswordInputField("API Key", required=True) + + deployment_name = forms.TextInputField("Deployment name", required=True) + + def get_model_params_setting_form(self, model_name): + if 'o3' in model_name or 'o1' in model_name: + return o3MiniLLMModelParams() + return AzureLLMModelParams() diff --git a/apps/models_provider/impl/azure_model_provider/credential/stt.py b/apps/models_provider/impl/azure_model_provider/credential/stt.py new file mode 100644 index 000000000..715078334 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/credential/stt.py @@ -0,0 +1,50 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AzureOpenAISTTModelCredential(BaseForm, BaseModelCredential): + api_version = forms.TextInputField("API Version", required=True) + api_base = forms.TextInputField('Azure Endpoint', required=True) + api_key = forms.PasswordInputField("API Key", required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key', 'api_version']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/azure_model_provider/credential/tti.py b/apps/models_provider/impl/azure_model_provider/credential/tti.py new file mode 100644 index 000000000..bd53d6574 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/credential/tti.py @@ -0,0 +1,87 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AzureOpenAITTIModelParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), _('Specify the size of the generated image, such as: 1024x1024')), + required=True, + default_value='1024x1024', + option_list=[ + {'value': '1024x1024', 'label': '1024x1024'}, + {'value': '1024x1792', 'label': '1024x1792'}, + {'value': '1792x1024', 'label': '1792x1024'}, + ], + text_field='label', + value_field='value' + ) + + quality = forms.SingleSelect( + TooltipLabel(_('Picture quality'), ''), + required=True, + default_value='standard', + option_list=[ + {'value': 'standard', 'label': 'standard'}, + {'value': 'hd', 'label': 'hd'}, + ], + text_field='label', + value_field='value' + ) + + n = forms.SliderField( + TooltipLabel(_('Number of pictures'), _('Specify the number of generated images')), + required=True, default_value=1, + _min=1, + _max=10, + _step=1, + precision=0) + + +class AzureOpenAITextToImageModelCredential(BaseForm, BaseModelCredential): + api_version = forms.TextInputField("API Version", required=True) + api_base = forms.TextInputField('Azure Endpoint', required=True) + api_key = forms.PasswordInputField("API Key", required=True) + + 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)) + + for key in ['api_base', 'api_key', 'api_version']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return AzureOpenAITTIModelParams() diff --git a/apps/models_provider/impl/azure_model_provider/credential/tts.py b/apps/models_provider/impl/azure_model_provider/credential/tts.py new file mode 100644 index 000000000..7c575b900 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/credential/tts.py @@ -0,0 +1,68 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class AzureOpenAITTSModelGeneralParams(BaseForm): + # alloy, echo, fable, onyx, nova, shimmer + voice = forms.SingleSelect( + TooltipLabel('Voice', + _('Try out the different sounds (Alloy, Echo, Fable, Onyx, Nova, and Sparkle) to find one that suits your desired tone and audience. The current voiceover is optimized for English.')), + required=True, default_value='alloy', + text_field='value', + value_field='value', + option_list=[ + {'text': 'alloy', 'value': 'alloy'}, + {'text': 'echo', 'value': 'echo'}, + {'text': 'fable', 'value': 'fable'}, + {'text': 'onyx', 'value': 'onyx'}, + {'text': 'nova', 'value': 'nova'}, + {'text': 'shimmer', 'value': 'shimmer'}, + ]) + + +class AzureOpenAITTSModelCredential(BaseForm, BaseModelCredential): + api_version = forms.TextInputField("API Version", required=True) + api_base = forms.TextInputField('Azure Endpoint', required=True) + api_key = forms.PasswordInputField("API Key", required=True) + + 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)) + + for key in ['api_base', 'api_key', 'api_version']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return AzureOpenAITTSModelGeneralParams() diff --git a/apps/models_provider/impl/azure_model_provider/icon/azure_icon_svg b/apps/models_provider/impl/azure_model_provider/icon/azure_icon_svg new file mode 100644 index 000000000..e5a2f98a0 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/icon/azure_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/azure_model_provider/model/azure_chat_model.py b/apps/models_provider/impl/azure_model_provider/model/azure_chat_model.py new file mode 100644 index 000000000..4c0b546ff --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/model/azure_chat_model.py @@ -0,0 +1,51 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: azure_chat_model.py + @date:2024/4/28 11:45 + @desc: +""" + +from typing import List, Dict + +from langchain_core.messages import BaseMessage, get_buffer_string +from langchain_openai import AzureChatOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel + + +class AzureChatModel(MaxKBBaseModel, AzureChatOpenAI): + @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 AzureChatModel( + azure_endpoint=model_credential.get('api_base'), + model_name=model_name, + openai_api_version=model_credential.get('api_version', '2024-02-15-preview'), + deployment_name=model_credential.get('deployment_name'), + openai_api_key=model_credential.get('api_key'), + openai_api_type="azure", + **optional_params, + streaming=True, + ) + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + try: + return super().get_num_tokens_from_messages(messages) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + + def get_num_tokens(self, text: str) -> int: + try: + return super().get_num_tokens(text) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) diff --git a/apps/models_provider/impl/azure_model_provider/model/embedding.py b/apps/models_provider/impl/azure_model_provider/model/embedding.py new file mode 100644 index 000000000..8b16d11b5 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/model/embedding.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 17:44 + @desc: +""" +from typing import Dict + +from langchain_openai import AzureOpenAIEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class AzureOpenAIEmbeddingModel(MaxKBBaseModel, AzureOpenAIEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return AzureOpenAIEmbeddingModel( + model=model_name, + openai_api_key=model_credential.get('api_key'), + azure_endpoint=model_credential.get('api_base'), + openai_api_version=model_credential.get('api_version'), + openai_api_type="azure", + ) diff --git a/apps/models_provider/impl/azure_model_provider/model/image.py b/apps/models_provider/impl/azure_model_provider/model/image.py new file mode 100644 index 000000000..5e960bc06 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/model/image.py @@ -0,0 +1,42 @@ +from typing import Dict, List + +from langchain_core.messages import BaseMessage, get_buffer_string +from langchain_openai import AzureChatOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class AzureOpenAIImage(MaxKBBaseModel, AzureChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return AzureOpenAIImage( + model_name=model_name, + openai_api_key=model_credential.get('api_key'), + azure_endpoint=model_credential.get('api_base'), + openai_api_version=model_credential.get('api_version'), + openai_api_type="azure", + streaming=True, + **optional_params, + ) + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + try: + return super().get_num_tokens_from_messages(messages) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + + def get_num_tokens(self, text: str) -> int: + try: + return super().get_num_tokens(text) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) diff --git a/apps/models_provider/impl/azure_model_provider/model/stt.py b/apps/models_provider/impl/azure_model_provider/model/stt.py new file mode 100644 index 000000000..125bd3f27 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/model/stt.py @@ -0,0 +1,62 @@ +import io +from typing import Dict + +from openai import AzureOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class AzureOpenAISpeechToText(MaxKBBaseModel, BaseSpeechToText): + api_base: str + api_key: str + api_version: str + model: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.api_version = kwargs.get('api_version') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return AzureOpenAISpeechToText( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + api_version=model_credential.get('api_version'), + **optional_params, + ) + + def check_auth(self): + client = AzureOpenAI( + azure_endpoint=self.api_base, + api_key=self.api_key, + api_version=self.api_version + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def speech_to_text(self, audio_file): + client = AzureOpenAI( + azure_endpoint=self.api_base, + api_key=self.api_key, + api_version=self.api_version + ) + audio_data = audio_file.read() + buffer = io.BytesIO(audio_data) + buffer.name = "file.mp3" # this is the important line + res = client.audio.transcriptions.create(model=self.model, language="zh", file=buffer) + return res.text diff --git a/apps/models_provider/impl/azure_model_provider/model/tti.py b/apps/models_provider/impl/azure_model_provider/model/tti.py new file mode 100644 index 000000000..30c0c01e5 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/model/tti.py @@ -0,0 +1,61 @@ +from typing import Dict + +from openai import AzureOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class AzureOpenAITextToImage(MaxKBBaseModel, BaseTextToImage): + api_base: str + api_key: str + api_version: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.api_version = kwargs.get('api_version') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'size': '1024x1024', 'quality': 'standard', 'n': 1}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return AzureOpenAITextToImage( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + api_version=model_credential.get('api_version'), + **optional_params, + ) + + def is_cache_model(self): + return False + + def check_auth(self): + chat = AzureOpenAI(api_key=self.api_key, azure_endpoint=self.api_base, api_version=self.api_version) + response_list = chat.models.with_raw_response.list() + + # self.generate_image('生成一个小猫图片') + + def generate_image(self, prompt: str, negative_prompt: str = None): + chat = AzureOpenAI(api_key=self.api_key, azure_endpoint=self.api_base, api_version=self.api_version) + res = chat.images.generate(model=self.model, prompt=prompt, **self.params) + file_urls = [] + for content in res.data: + url = content.url + file_urls.append(url) + + return file_urls diff --git a/apps/models_provider/impl/azure_model_provider/model/tts.py b/apps/models_provider/impl/azure_model_provider/model/tts.py new file mode 100644 index 000000000..b852dff52 --- /dev/null +++ b/apps/models_provider/impl/azure_model_provider/model/tts.py @@ -0,0 +1,69 @@ +from typing import Dict + +from openai import AzureOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class AzureOpenAITextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + api_base: str + api_key: str + api_version: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.api_version = kwargs.get('api_version') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'voice': 'alloy'}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return AzureOpenAITextToSpeech( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + api_version=model_credential.get('api_version'), + **optional_params, + ) + + def check_auth(self): + client = AzureOpenAI( + azure_endpoint=self.api_base, + api_key=self.api_key, + api_version=self.api_version + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def text_to_speech(self, text): + client = AzureOpenAI( + azure_endpoint=self.api_base, + api_key=self.api_key, + api_version=self.api_version + ) + text = _remove_empty_lines(text) + with client.audio.speech.with_streaming_response.create( + model=self.model, + input=text, + **self.params + ) as response: + return response.read() + + def is_cache_model(self): + return False diff --git a/apps/models_provider/impl/base_chat_open_ai.py b/apps/models_provider/impl/base_chat_open_ai.py new file mode 100644 index 000000000..54076b7ef --- /dev/null +++ b/apps/models_provider/impl/base_chat_open_ai.py @@ -0,0 +1,168 @@ +# coding=utf-8 +import warnings +from typing import List, Dict, Optional, Any, Iterator, cast, Type, Union + +import openai +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.language_models import LanguageModelInput +from langchain_core.messages import BaseMessage, get_buffer_string, BaseMessageChunk, AIMessageChunk +from langchain_core.outputs import ChatGenerationChunk, ChatGeneration +from langchain_core.runnables import RunnableConfig, ensure_config +from langchain_core.utils.pydantic import is_basemodel_subclass +from langchain_openai import ChatOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class BaseChatOpenAI(ChatOpenAI): + usage_metadata: dict = {} + custom_get_token_ids = custom_get_token_ids + + def get_last_generation_info(self) -> Optional[Dict[str, Any]]: + return self.usage_metadata + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + try: + return super().get_num_tokens_from_messages(messages) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + return self.usage_metadata.get('input_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + try: + return super().get_num_tokens(text) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) + return self.get_last_generation_info().get('output_tokens', 0) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + kwargs["stream"] = True + kwargs["stream_options"] = {"include_usage": True} + """Set default stream_options.""" + stream_usage = self._should_stream_usage(kwargs.get('stream_usage'), **kwargs) + # Note: stream_options is not a valid parameter for Azure OpenAI. + # To support users proxying Azure through ChatOpenAI, here we only specify + # stream_options if include_usage is set to True. + # See https://learn.microsoft.com/en-us/azure/ai-services/openai/whats-new + # for release notes. + if stream_usage: + kwargs["stream_options"] = {"include_usage": stream_usage} + + payload = self._get_request_payload(messages, stop=stop, **kwargs) + default_chunk_class: Type[BaseMessageChunk] = AIMessageChunk + base_generation_info = {} + + if "response_format" in payload and is_basemodel_subclass( + payload["response_format"] + ): + # TODO: Add support for streaming with Pydantic response_format. + warnings.warn("Streaming with Pydantic response_format not yet supported.") + chat_result = self._generate( + messages, stop, run_manager=run_manager, **kwargs + ) + msg = chat_result.generations[0].message + yield ChatGenerationChunk( + message=AIMessageChunk( + **msg.dict(exclude={"type", "additional_kwargs"}), + # preserve the "parsed" Pydantic object without converting to dict + additional_kwargs=msg.additional_kwargs, + ), + generation_info=chat_result.generations[0].generation_info, + ) + return + if self.include_response_headers: + raw_response = self.client.with_raw_response.create(**payload) + response = raw_response.parse() + base_generation_info = {"headers": dict(raw_response.headers)} + else: + response = self.client.create(**payload) + with response: + is_first_chunk = True + for chunk in response: + if not isinstance(chunk, dict): + chunk = chunk.model_dump() + + generation_chunk = super()._convert_chunk_to_generation_chunk( + chunk, + default_chunk_class, + base_generation_info if is_first_chunk else {}, + ) + if generation_chunk is None: + continue + + # custom code + if len(chunk['choices']) > 0 and 'reasoning_content' in chunk['choices'][0]['delta']: + generation_chunk.message.additional_kwargs["reasoning_content"] = chunk['choices'][0]['delta'][ + 'reasoning_content'] + + default_chunk_class = generation_chunk.message.__class__ + logprobs = (generation_chunk.generation_info or {}).get("logprobs") + if run_manager: + run_manager.on_llm_new_token( + generation_chunk.text, chunk=generation_chunk, logprobs=logprobs + ) + is_first_chunk = False + # custom code + if generation_chunk.message.usage_metadata is not None: + self.usage_metadata = generation_chunk.message.usage_metadata + yield generation_chunk + + def _create_chat_result(self, + response: Union[dict, openai.BaseModel], + generation_info: Optional[Dict] = None): + result = super()._create_chat_result(response, generation_info) + try: + reasoning_content = '' + reasoning_content_enable = False + for res in response.choices: + if 'reasoning_content' in res.message.model_extra: + reasoning_content_enable = True + _reasoning_content = res.message.model_extra.get('reasoning_content') + if _reasoning_content is not None: + reasoning_content += _reasoning_content + if reasoning_content_enable: + result.llm_output['reasoning_content'] = reasoning_content + except Exception as e: + pass + return result + + def invoke( + self, + input: LanguageModelInput, + config: Optional[RunnableConfig] = None, + *, + stop: Optional[List[str]] = None, + **kwargs: Any, + ) -> BaseMessage: + config = ensure_config(config) + chat_result = cast( + ChatGeneration, + self.generate_prompt( + [self._convert_input(input)], + stop=stop, + callbacks=config.get("callbacks"), + tags=config.get("tags"), + metadata=config.get("metadata"), + run_name=config.get("run_name"), + run_id=config.pop("run_id", None), + **kwargs, + ).generations[0][0], + ).message + self.usage_metadata = chat_result.response_metadata[ + 'token_usage'] if 'token_usage' in chat_result.response_metadata else chat_result.usage_metadata + return chat_result diff --git a/apps/models_provider/impl/base_stt.py b/apps/models_provider/impl/base_stt.py new file mode 100644 index 000000000..aae72a559 --- /dev/null +++ b/apps/models_provider/impl/base_stt.py @@ -0,0 +1,14 @@ +# coding=utf-8 +from abc import abstractmethod + +from pydantic import BaseModel + + +class BaseSpeechToText(BaseModel): + @abstractmethod + def check_auth(self): + pass + + @abstractmethod + def speech_to_text(self, audio_file): + pass diff --git a/apps/models_provider/impl/base_tti.py b/apps/models_provider/impl/base_tti.py new file mode 100644 index 000000000..5e34d12cd --- /dev/null +++ b/apps/models_provider/impl/base_tti.py @@ -0,0 +1,14 @@ +# coding=utf-8 +from abc import abstractmethod + +from pydantic import BaseModel + + +class BaseTextToImage(BaseModel): + @abstractmethod + def check_auth(self): + pass + + @abstractmethod + def generate_image(self, prompt: str, negative_prompt: str = None): + pass diff --git a/apps/models_provider/impl/base_tts.py b/apps/models_provider/impl/base_tts.py new file mode 100644 index 000000000..6311f2686 --- /dev/null +++ b/apps/models_provider/impl/base_tts.py @@ -0,0 +1,14 @@ +# coding=utf-8 +from abc import abstractmethod + +from pydantic import BaseModel + + +class BaseTextToSpeech(BaseModel): + @abstractmethod + def check_auth(self): + pass + + @abstractmethod + def text_to_speech(self, text): + pass diff --git a/apps/models_provider/impl/deepseek_model_provider/__init__.py b/apps/models_provider/impl/deepseek_model_provider/__init__.py new file mode 100644 index 000000000..ee456da1f --- /dev/null +++ b/apps/models_provider/impl/deepseek_model_provider/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :__init__.py.py +@Author :Brian Yang +@Date :5/12/24 7:38 AM +""" diff --git a/apps/models_provider/impl/deepseek_model_provider/credential/llm.py b/apps/models_provider/impl/deepseek_model_provider/credential/llm.py new file mode 100644 index 000000000..7943c4077 --- /dev/null +++ b/apps/models_provider/impl/deepseek_model_provider/credential/llm.py @@ -0,0 +1,77 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 17:51 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class DeepSeekLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class DeepSeekLLMModelCredential(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)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return DeepSeekLLMModelParams() diff --git a/apps/models_provider/impl/deepseek_model_provider/deepseek_model_provider.py b/apps/models_provider/impl/deepseek_model_provider/deepseek_model_provider.py new file mode 100644 index 000000000..abcd83f05 --- /dev/null +++ b/apps/models_provider/impl/deepseek_model_provider/deepseek_model_provider.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :deepseek_model_provider.py +@Author :Brian Yang +@Date :5/12/24 7:40 AM +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, \ + ModelInfoManage +from models_provider.impl.deepseek_model_provider.credential.llm import DeepSeekLLMModelCredential +from models_provider.impl.deepseek_model_provider.model.llm import DeepSeekChatModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +deepseek_llm_model_credential = DeepSeekLLMModelCredential() +deepseek_reasoner = ModelInfo('deepseek-reasoner', '', ModelTypeConst.LLM, + deepseek_llm_model_credential, DeepSeekChatModel + ) + +deepseek_chat = ModelInfo('deepseek-chat', _('Good at common conversational tasks, supports 32K contexts'), + ModelTypeConst.LLM, + deepseek_llm_model_credential, DeepSeekChatModel + ) + +deepseek_coder = ModelInfo('deepseek-coder', _('Good at handling programming tasks, supports 16K contexts'), + ModelTypeConst.LLM, + deepseek_llm_model_credential, + DeepSeekChatModel) + +model_info_manage = ModelInfoManage.builder().append_model_info(deepseek_reasoner).append_model_info(deepseek_chat).append_model_info( + deepseek_coder).append_default_model_info( + deepseek_coder).build() + + +class DeepSeekModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_deepseek_provider', name='DeepSeek', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'deepseek_model_provider', 'icon', + 'deepseek_icon_svg'))) diff --git a/apps/models_provider/impl/deepseek_model_provider/icon/deepseek_icon_svg b/apps/models_provider/impl/deepseek_model_provider/icon/deepseek_icon_svg new file mode 100644 index 000000000..6ace8911a --- /dev/null +++ b/apps/models_provider/impl/deepseek_model_provider/icon/deepseek_icon_svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/apps/models_provider/impl/deepseek_model_provider/model/llm.py b/apps/models_provider/impl/deepseek_model_provider/model/llm.py new file mode 100644 index 000000000..0d77b15f7 --- /dev/null +++ b/apps/models_provider/impl/deepseek_model_provider/model/llm.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :llm.py +@Author :Brian Yang +@Date :5/12/24 7:44 AM +""" +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class DeepSeekChatModel(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) + + deepseek_chat_open_ai = DeepSeekChatModel( + model=model_name, + openai_api_base='https://api.deepseek.com', + openai_api_key=model_credential.get('api_key'), + **optional_params + ) + return deepseek_chat_open_ai diff --git a/apps/models_provider/impl/gemini_model_provider/__init__.py b/apps/models_provider/impl/gemini_model_provider/__init__.py new file mode 100644 index 000000000..43fd3dd05 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :__init__.py.py +@Author :Brian Yang +@Date :5/13/24 7:40 AM +""" diff --git a/apps/models_provider/impl/gemini_model_provider/credential/embedding.py b/apps/models_provider/impl/gemini_model_provider/credential/embedding.py new file mode 100644 index 000000000..5d88b9fc1 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/credential/embedding.py @@ -0,0 +1,52 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 16:45 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class GeminiEmbeddingCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True): + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/gemini_model_provider/credential/image.py b/apps/models_provider/impl/gemini_model_provider/credential/image.py new file mode 100644 index 000000000..f880d31fd --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/credential/image.py @@ -0,0 +1,71 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class GeminiImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class GeminiImageModelCredential(BaseForm, BaseModelCredential): + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return GeminiImageModelParams() diff --git a/apps/models_provider/impl/gemini_model_provider/credential/llm.py b/apps/models_provider/impl/gemini_model_provider/credential/llm.py new file mode 100644 index 000000000..1bc6f5750 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/credential/llm.py @@ -0,0 +1,78 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 17:57 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class GeminiLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class GeminiLLMModelCredential(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)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.invoke([HumanMessage(content=gettext('Hello'))]) + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return GeminiLLMModelParams() diff --git a/apps/models_provider/impl/gemini_model_provider/credential/stt.py b/apps/models_provider/impl/gemini_model_provider/credential/stt.py new file mode 100644 index 000000000..475cf4c2e --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/credential/stt.py @@ -0,0 +1,48 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class GeminiSTTModelCredential(BaseForm, BaseModelCredential): + api_key = forms.PasswordInputField('API Key', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/gemini_model_provider/gemini_model_provider.py b/apps/models_provider/impl/gemini_model_provider/gemini_model_provider.py new file mode 100644 index 000000000..53c36901b --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/gemini_model_provider.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :gemini_model_provider.py +@Author :Brian Yang +@Date :5/13/24 7:47 AM +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, \ + ModelInfoManage +from models_provider.impl.gemini_model_provider.credential.embedding import GeminiEmbeddingCredential +from models_provider.impl.gemini_model_provider.credential.image import GeminiImageModelCredential +from models_provider.impl.gemini_model_provider.credential.llm import GeminiLLMModelCredential +from models_provider.impl.gemini_model_provider.credential.stt import GeminiSTTModelCredential +from models_provider.impl.gemini_model_provider.model.embedding import GeminiEmbeddingModel +from models_provider.impl.gemini_model_provider.model.image import GeminiImage +from models_provider.impl.gemini_model_provider.model.llm import GeminiChatModel +from models_provider.impl.gemini_model_provider.model.stt import GeminiSpeechToText +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + + +gemini_llm_model_credential = GeminiLLMModelCredential() +gemini_image_model_credential = GeminiImageModelCredential() +gemini_stt_model_credential = GeminiSTTModelCredential() +gemini_embedding_model_credential = GeminiEmbeddingCredential() + +model_info_list = [ + ModelInfo('gemini-1.0-pro', _('Latest Gemini 1.0 Pro model, updated with Google update'), + ModelTypeConst.LLM, + gemini_llm_model_credential, + GeminiChatModel), + ModelInfo('gemini-1.0-pro-vision', _('Latest Gemini 1.0 Pro Vision model, updated with Google update'), + ModelTypeConst.LLM, + gemini_llm_model_credential, + GeminiChatModel), +] + +model_image_info_list = [ + ModelInfo('gemini-1.5-flash', _('Latest Gemini 1.5 Flash model, updated with Google updates'), + ModelTypeConst.IMAGE, + gemini_image_model_credential, + GeminiImage), + ModelInfo('gemini-1.5-pro', _('Latest Gemini 1.5 Flash model, updated with Google updates'), + ModelTypeConst.IMAGE, + gemini_image_model_credential, + GeminiImage), +] + +model_stt_info_list = [ + ModelInfo('gemini-1.5-flash', _('Latest Gemini 1.5 Flash model, updated with Google updates'), + ModelTypeConst.STT, + gemini_stt_model_credential, + GeminiSpeechToText), + ModelInfo('gemini-1.5-pro', _('Latest Gemini 1.5 Flash model, updated with Google updates'), + ModelTypeConst.STT, + gemini_stt_model_credential, + GeminiSpeechToText), +] + +model_embedding_info_list = [ + ModelInfo('models/embedding-001', '', + ModelTypeConst.EMBEDDING, + gemini_embedding_model_credential, + GeminiEmbeddingModel), + ModelInfo('models/text-embedding-004', '', + ModelTypeConst.EMBEDDING, + gemini_embedding_model_credential, + GeminiEmbeddingModel), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_model_info_list(model_image_info_list) + .append_model_info_list(model_stt_info_list) + .append_model_info_list(model_embedding_info_list) + .append_default_model_info(model_info_list[0]) + .append_default_model_info(model_image_info_list[0]) + .append_default_model_info(model_stt_info_list[0]) + .append_default_model_info(model_embedding_info_list[0]) + .build() +) + + +class GeminiModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_gemini_provider', name='Gemini', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'gemini_model_provider', 'icon', + 'gemini_icon_svg'))) diff --git a/apps/models_provider/impl/gemini_model_provider/icon/gemini_icon_svg b/apps/models_provider/impl/gemini_model_provider/icon/gemini_icon_svg new file mode 100644 index 000000000..3ff8bdaf4 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/icon/gemini_icon_svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/apps/models_provider/impl/gemini_model_provider/model/embedding.py b/apps/models_provider/impl/gemini_model_provider/model/embedding.py new file mode 100644 index 000000000..d5ceb93c2 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/model/embedding.py @@ -0,0 +1,22 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 17:44 + @desc: +""" +from typing import Dict + +from langchain_google_genai import GoogleGenerativeAIEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class GeminiEmbeddingModel(MaxKBBaseModel, GoogleGenerativeAIEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return GeminiEmbeddingModel( + google_api_key=model_credential.get('api_key'), + model=model_name, + ) diff --git a/apps/models_provider/impl/gemini_model_provider/model/image.py b/apps/models_provider/impl/gemini_model_provider/model/image.py new file mode 100644 index 000000000..57fb32f27 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/model/image.py @@ -0,0 +1,24 @@ +from typing import Dict + +from langchain_google_genai import ChatGoogleGenerativeAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class GeminiImage(MaxKBBaseModel, ChatGoogleGenerativeAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return GeminiImage( + model=model_name, + google_api_key=model_credential.get('api_key'), + streaming=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/gemini_model_provider/model/llm.py b/apps/models_provider/impl/gemini_model_provider/model/llm.py new file mode 100644 index 000000000..c39f7e7d9 --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/model/llm.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :llm.py +@Author :Brian Yang +@Date :5/13/24 7:40 AM +""" +from typing import List, Dict, Optional, Sequence, Union, Any, Iterator, cast + +from google.ai.generativelanguage_v1 import GenerateContentResponse +from google.ai.generativelanguage_v1beta.types import ( + Tool as GoogleTool, +) +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.messages import BaseMessage +from langchain_core.outputs import ChatGenerationChunk +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_google_genai._function_utils import _ToolConfigDict, _ToolDict +from langchain_google_genai.chat_models import _chat_with_retry, _response_to_result, \ + _FunctionDeclarationType +from langchain_google_genai._common import ( + SafetySettingDict, +) +from models_provider.base_model_provider import MaxKBBaseModel + + +class GeminiChatModel(MaxKBBaseModel, ChatGoogleGenerativeAI): + + @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) + + gemini_chat = GeminiChatModel( + model=model_name, + google_api_key=model_credential.get('api_key'), + **optional_params + ) + return gemini_chat + + def get_last_generation_info(self) -> Optional[Dict[str, Any]]: + return self.__dict__.get('_last_generation_info') + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + return self.get_last_generation_info().get('input_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + return self.get_last_generation_info().get('output_tokens', 0) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + *, + tools: Optional[Sequence[Union[_ToolDict, GoogleTool]]] = None, + functions: Optional[Sequence[_FunctionDeclarationType]] = None, + safety_settings: Optional[SafetySettingDict] = None, + tool_config: Optional[Union[Dict, _ToolConfigDict]] = None, + generation_config: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + request = self._prepare_request( + messages, + stop=stop, + tools=tools, + functions=functions, + safety_settings=safety_settings, + tool_config=tool_config, + generation_config=generation_config, + ) + response: GenerateContentResponse = _chat_with_retry( + request=request, + generation_method=self.client.stream_generate_content, + **kwargs, + metadata=self.default_metadata, + ) + for chunk in response: + _chat_result = _response_to_result(chunk, stream=True) + gen = cast(ChatGenerationChunk, _chat_result.generations[0]) + if gen.message: + token_usage = gen.message.usage_metadata + self.__dict__.setdefault('_last_generation_info', {}).update(token_usage) + if run_manager: + run_manager.on_llm_new_token(gen.text) + yield gen diff --git a/apps/models_provider/impl/gemini_model_provider/model/stt.py b/apps/models_provider/impl/gemini_model_provider/model/stt.py new file mode 100644 index 000000000..2bf302efc --- /dev/null +++ b/apps/models_provider/impl/gemini_model_provider/model/stt.py @@ -0,0 +1,57 @@ +from typing import Dict + +from django.utils.translation import gettext as _ +from langchain_core.messages import HumanMessage +from langchain_google_genai import ChatGoogleGenerativeAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class GeminiSpeechToText(MaxKBBaseModel, BaseSpeechToText): + api_key: str + model: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return GeminiSpeechToText( + model=model_name, + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + client = ChatGoogleGenerativeAI( + model=self.model, + google_api_key=self.api_key + ) + response_list = client.invoke(_('Hello')) + # print(response_list) + + def speech_to_text(self, audio_file): + client = ChatGoogleGenerativeAI( + model=self.model, + google_api_key=self.api_key + ) + audio_data = audio_file.read() + msg = HumanMessage(content=[ + {'type': 'text', 'text': _('convert audio to text')}, + {"type": "media", 'mime_type': 'audio/mp3', "data": audio_data} + ]) + res = client.invoke([msg]) + return res.content diff --git a/apps/models_provider/impl/kimi_model_provider/__init__.py b/apps/models_provider/impl/kimi_model_provider/__init__.py new file mode 100644 index 000000000..53b7001e5 --- /dev/null +++ b/apps/models_provider/impl/kimi_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2023/10/31 17:16 + @desc: +""" diff --git a/apps/models_provider/impl/kimi_model_provider/credential/llm.py b/apps/models_provider/impl/kimi_model_provider/credential/llm.py new file mode 100644 index 000000000..7c2e4b174 --- /dev/null +++ b/apps/models_provider/impl/kimi_model_provider/credential/llm.py @@ -0,0 +1,77 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:06 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class KimiLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.3, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class KimiLLMModelCredential(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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return KimiLLMModelParams() diff --git a/apps/models_provider/impl/kimi_model_provider/icon/kimi_icon_svg b/apps/models_provider/impl/kimi_model_provider/icon/kimi_icon_svg new file mode 100644 index 000000000..8bd2a78fe --- /dev/null +++ b/apps/models_provider/impl/kimi_model_provider/icon/kimi_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/kimi_model_provider/kimi_model_provider.py b/apps/models_provider/impl/kimi_model_provider/kimi_model_provider.py new file mode 100644 index 000000000..e1ab6d7fa --- /dev/null +++ b/apps/models_provider/impl/kimi_model_provider/kimi_model_provider.py @@ -0,0 +1,42 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: kimi_model_provider.py + @date:2024/3/28 16:26 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, \ + ModelTypeConst, ModelInfoManage +from models_provider.impl.kimi_model_provider.credential.llm import KimiLLMModelCredential +from models_provider.impl.kimi_model_provider.model.llm import KimiChatModel +from maxkb.conf import PROJECT_DIR + +kimi_llm_model_credential = KimiLLMModelCredential() + +moonshot_v1_8k = ModelInfo('moonshot-v1-8k', '', ModelTypeConst.LLM, kimi_llm_model_credential, + KimiChatModel) +moonshot_v1_32k = ModelInfo('moonshot-v1-32k', '', ModelTypeConst.LLM, kimi_llm_model_credential, + KimiChatModel) +moonshot_v1_128k = ModelInfo('moonshot-v1-128k', '', ModelTypeConst.LLM, kimi_llm_model_credential, + KimiChatModel) + +model_info_manage = ModelInfoManage.builder().append_model_info(moonshot_v1_8k).append_model_info( + moonshot_v1_32k).append_default_model_info(moonshot_v1_128k).append_default_model_info(moonshot_v1_8k).build() + + +class KimiModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_dialogue_number(self): + return 3 + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_kimi_provider', name='Kimi', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'kimi_model_provider', 'icon', + 'kimi_icon_svg'))) diff --git a/apps/models_provider/impl/kimi_model_provider/model/llm.py b/apps/models_provider/impl/kimi_model_provider/model/llm.py new file mode 100644 index 000000000..64fcabff2 --- /dev/null +++ b/apps/models_provider/impl/kimi_model_provider/model/llm.py @@ -0,0 +1,31 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2023/11/10 17:45 + @desc: +""" +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class KimiChatModel(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) + + kimi_chat_open_ai = KimiChatModel( + openai_api_base=model_credential['api_base'], + openai_api_key=model_credential['api_key'], + model_name=model_name, + **optional_params + ) + return kimi_chat_open_ai diff --git a/apps/models_provider/impl/local_model_provider/__init__.py b/apps/models_provider/impl/local_model_provider/__init__.py new file mode 100644 index 000000000..90a8d72c3 --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: __init__.py + @date:2024/7/10 17:48 + @desc: +""" diff --git a/apps/models_provider/impl/local_model_provider/credential/embedding.py b/apps/models_provider/impl/local_model_provider/credential/embedding.py new file mode 100644 index 000000000..9d656ad98 --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/credential/embedding.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/11 11:06 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.local_model_provider.model.embedding import LocalEmbedding + + +class LocalEmbeddingCredential(BaseForm, BaseModelCredential): + + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=False): + if not model_type == 'EMBEDDING': + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + for key in ['cache_folder']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model: LocalEmbedding = provider.get_model(model_type, model_name, model_credential) + model.embed_query(gettext('Hello')) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return model + + cache_folder = forms.TextInputField(_('Model catalog'), required=True) diff --git a/apps/models_provider/impl/local_model_provider/credential/reranker.py b/apps/models_provider/impl/local_model_provider/credential/reranker.py new file mode 100644 index 000000000..94341d52f --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/credential/reranker.py @@ -0,0 +1,54 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: reranker.py + @date:2024/9/3 14:33 + @desc: +""" +import traceback +from typing import Dict + +from langchain_core.documents import Document + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.local_model_provider.model.reranker import LocalBaseReranker +from django.utils.translation import gettext_lazy as _, gettext + + +class LocalRerankerCredential(BaseForm, BaseModelCredential): + + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=False): + if not model_type == 'RERANKER': + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + for key in ['cache_dir']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model: LocalBaseReranker = provider.get_model(model_type, model_name, model_credential) + model.compress_documents([Document(page_content=gettext('Hello'))], gettext('Hello')) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return model + + cache_dir = forms.TextInputField(_('Model catalog'), required=True) diff --git a/apps/models_provider/impl/local_model_provider/icon/local_icon_svg b/apps/models_provider/impl/local_model_provider/icon/local_icon_svg new file mode 100644 index 000000000..62930faab --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/icon/local_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/local_model_provider/local_model_provider.py b/apps/models_provider/impl/local_model_provider/local_model_provider.py new file mode 100644 index 000000000..14603962a --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/local_model_provider.py @@ -0,0 +1,41 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: zhipu_model_provider.py + @date:2024/04/19 13:5 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import ModelProvideInfo, ModelTypeConst, ModelInfo, IModelProvider, \ + ModelInfoManage +from models_provider.impl.local_model_provider.credential.embedding import LocalEmbeddingCredential +from models_provider.impl.local_model_provider.credential.reranker import LocalRerankerCredential +from models_provider.impl.local_model_provider.model.embedding import LocalEmbedding +from models_provider.impl.local_model_provider.model.reranker import LocalReranker +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +embedding_text2vec_base_chinese = ModelInfo('shibing624/text2vec-base-chinese', '', ModelTypeConst.EMBEDDING, + LocalEmbeddingCredential(), LocalEmbedding) +bge_reranker_v2_m3 = ModelInfo('BAAI/bge-reranker-v2-m3', '', ModelTypeConst.RERANKER, + LocalRerankerCredential(), LocalReranker) + +model_info_manage = (ModelInfoManage.builder().append_model_info(embedding_text2vec_base_chinese) + .append_default_model_info(embedding_text2vec_base_chinese) + .append_model_info(bge_reranker_v2_m3) + .append_default_model_info(bge_reranker_v2_m3) + .build()) + + +class LocalModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_local_provider', name=_('local model'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'local_model_provider', 'icon', + 'local_icon_svg'))) diff --git a/apps/models_provider/impl/local_model_provider/model/embedding.py b/apps/models_provider/impl/local_model_provider/model/embedding.py new file mode 100644 index 000000000..3e296d5b1 --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/model/embedding.py @@ -0,0 +1,62 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/11 14:06 + @desc: +""" +from typing import Dict, List + +import requests +from langchain_core.embeddings import Embeddings +from pydantic import BaseModel +from langchain_huggingface import HuggingFaceEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel +from maxkb.const import CONFIG + + +class WebLocalEmbedding(MaxKBBaseModel, BaseModel, Embeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + pass + + model_id: str = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.model_id = kwargs.get('model_id', None) + + def embed_query(self, text: str) -> List[float]: + bind = f'{CONFIG.get("LOCAL_MODEL_HOST")}:{CONFIG.get("LOCAL_MODEL_PORT")}' + res = requests.post(f'{CONFIG.get("LOCAL_MODEL_PROTOCOL")}://{bind}/api/model/{self.model_id}/embed_query', + {'text': text}) + result = res.json() + if result.get('code', 500) == 200: + return result.get('data') + raise Exception(result.get('message')) + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + bind = f'{CONFIG.get("LOCAL_MODEL_HOST")}:{CONFIG.get("LOCAL_MODEL_PORT")}' + res = requests.post(f'{CONFIG.get("LOCAL_MODEL_PROTOCOL")}://{bind}/api/model/{self.model_id}/embed_documents', + {'texts': texts}) + result = res.json() + if result.get('code', 500) == 200: + return result.get('data') + raise Exception(result.get('message')) + + +class LocalEmbedding(MaxKBBaseModel, HuggingFaceEmbeddings): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + if model_kwargs.get('use_local', True): + return LocalEmbedding(model_name=model_name, cache_folder=model_credential.get('cache_folder'), + model_kwargs={'device': model_credential.get('device')}, + encode_kwargs={'normalize_embeddings': True} + ) + return WebLocalEmbedding(model_name=model_name, cache_folder=model_credential.get('cache_folder'), + model_kwargs={'device': model_credential.get('device')}, + encode_kwargs={'normalize_embeddings': True}, + **model_kwargs) diff --git a/apps/models_provider/impl/local_model_provider/model/reranker.py b/apps/models_provider/impl/local_model_provider/model/reranker.py new file mode 100644 index 000000000..7b1402359 --- /dev/null +++ b/apps/models_provider/impl/local_model_provider/model/reranker.py @@ -0,0 +1,101 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: reranker.py.py + @date:2024/9/2 16:42 + @desc: +""" +from typing import Sequence, Optional, Dict, Any, ClassVar + +import requests +import torch +from langchain_core.callbacks import Callbacks +from langchain_core.documents import BaseDocumentCompressor, Document +from transformers import AutoModelForSequenceClassification, AutoTokenizer + +from models_provider.base_model_provider import MaxKBBaseModel +from maxkb.const import CONFIG + + +class LocalReranker(MaxKBBaseModel): + def __init__(self, model_name, top_n=3, cache_dir=None): + super().__init__() + self.model_name = model_name + self.cache_dir = cache_dir + self.top_n = top_n + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + if model_kwargs.get('use_local', True): + return LocalBaseReranker(model_name=model_name, cache_dir=model_credential.get('cache_dir'), + model_kwargs={'device': model_credential.get('device', 'cpu')} + + ) + return WebLocalBaseReranker(model_name=model_name, cache_dir=model_credential.get('cache_dir'), + model_kwargs={'device': model_credential.get('device')}, + **model_kwargs) + + +class WebLocalBaseReranker(MaxKBBaseModel, BaseDocumentCompressor): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + pass + + model_id: str = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.model_id = kwargs.get('model_id', None) + + def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \ + Sequence[Document]: + if documents is None or len(documents) == 0: + return [] + bind = f'{CONFIG.get("LOCAL_MODEL_HOST")}:{CONFIG.get("LOCAL_MODEL_PORT")}' + res = requests.post( + f'{CONFIG.get("LOCAL_MODEL_PROTOCOL")}://{bind}/api/model/{self.model_id}/compress_documents', + json={'documents': [{'page_content': document.page_content, 'metadata': document.metadata} for document in + documents], 'query': query}, headers={'Content-Type': 'application/json'}) + result = res.json() + if result.get('code', 500) == 200: + return [Document(page_content=document.get('page_content'), metadata=document.get('metadata')) for document + in result.get('data')] + raise Exception(result.get('message')) + + +class LocalBaseReranker(MaxKBBaseModel, BaseDocumentCompressor): + client: Any = None + tokenizer: Any = None + model: Optional[str] = None + cache_dir: Optional[str] = None + model_kwargs: Any = {} + + def __init__(self, model_name, cache_dir=None, **model_kwargs): + super().__init__() + self.model = model_name + self.cache_dir = cache_dir + self.model_kwargs = model_kwargs + self.client = AutoModelForSequenceClassification.from_pretrained(self.model, cache_dir=self.cache_dir) + self.tokenizer = AutoTokenizer.from_pretrained(self.model, cache_dir=self.cache_dir) + self.client = self.client.to(self.model_kwargs.get('device', 'cpu')) + self.client.eval() + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return LocalBaseReranker(model_name, cache_dir=model_credential.get('cache_dir'), **model_kwargs) + + def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \ + Sequence[Document]: + if documents is None or len(documents) == 0: + return [] + with torch.no_grad(): + inputs = self.tokenizer([[query, document.page_content] for document in documents], padding=True, + truncation=True, return_tensors='pt', max_length=512) + scores = [torch.sigmoid(s).float().item() for s in + self.client(**inputs, return_dict=True).logits.view(-1, ).float()] + result = [Document(page_content=documents[index].page_content, metadata={'relevance_score': scores[index]}) + for index + in range(len(documents))] + result.sort(key=lambda row: row.metadata.get('relevance_score'), reverse=True) + return result diff --git a/apps/models_provider/impl/ollama_model_provider/__init__.py b/apps/models_provider/impl/ollama_model_provider/__init__.py new file mode 100644 index 000000000..6da6cdb69 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/3/5 17:20 + @desc: +""" diff --git a/apps/models_provider/impl/ollama_model_provider/credential/embedding.py b/apps/models_provider/impl/ollama_model_provider/credential/embedding.py new file mode 100644 index 000000000..15a869a45 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/credential/embedding.py @@ -0,0 +1,49 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 15:10 + @desc: +""" +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.local_model_provider.model.embedding import LocalEmbedding + + +class OllamaEmbeddingModelCredential(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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base')) + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, _('API domain name is invalid')) + exist = [model for model in (model_list.get('models') if model_list.get('models') is not None else []) if + model.get('model') == model_name or model.get('model').replace(":latest", "") == model_name] + if len(exist) == 0: + raise AppApiException(ValidCode.model_not_fount, + _('The model does not exist, please download the model first')) + model: LocalEmbedding = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + return True + + def encryption_dict(self, model_info: Dict[str, object]): + return model_info + + def build_model(self, model_info: Dict[str, object]): + for key in ['model']: + if key not in model_info: + raise AppApiException(500, _('{key} is required').format(key=key)) + return self + + api_base = forms.TextInputField('API URL', required=True) diff --git a/apps/models_provider/impl/ollama_model_provider/credential/image.py b/apps/models_provider/impl/ollama_model_provider/credential/image.py new file mode 100644 index 000000000..c30091547 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/credential/image.py @@ -0,0 +1,56 @@ +# coding=utf-8 +from typing import Dict + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext_lazy as _, gettext + + +class OllamaImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class OllamaImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base')) + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, gettext('API domain name is invalid')) + exist = [model for model in (model_list.get('models') if model_list.get('models') is not None else []) if + model.get('model') == model_name or model.get('model').replace(":latest", "") == model_name] + if len(exist) == 0: + raise AppApiException(ValidCode.model_not_fount, + gettext('The model does not exist, please download the model first')) + + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return OllamaImageModelParams() diff --git a/apps/models_provider/impl/ollama_model_provider/credential/llm.py b/apps/models_provider/impl/ollama_model_provider/credential/llm.py new file mode 100644 index 000000000..73af9b3e6 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/credential/llm.py @@ -0,0 +1,70 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:19 + @desc: +""" +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class OllamaLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.3, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class OllamaLLMModelCredential(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)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base')) + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, gettext('API domain name is invalid')) + exist = [model for model in (model_list.get('models') if model_list.get('models') is not None else []) if + model.get('model') == model_name or model.get('model').replace(":latest", "") == model_name] + if len(exist) == 0: + raise AppApiException(ValidCode.model_not_fount, + gettext('The model does not exist, please download the model first')) + return True + + def encryption_dict(self, model_info: Dict[str, object]): + 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', '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') + return self + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return OllamaLLMModelParams() diff --git a/apps/models_provider/impl/ollama_model_provider/credential/reranker.py b/apps/models_provider/impl/ollama_model_provider/credential/reranker.py new file mode 100644 index 000000000..d82f3cfee --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/credential/reranker.py @@ -0,0 +1,66 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 15:10 + @desc: +""" +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.ollama_model_provider.model.reranker import OllamaReranker +from langchain_core.documents import BaseDocumentCompressor, Document +from django.utils.translation import gettext_lazy as _, gettext + + +class OllamaReRankModelCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=False): + if not model_type == 'RERANKER': + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base')) + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, _('API domain name is invalid')) + exist = [model for model in (model_list.get('models') if model_list.get('models') is not None else []) if + model.get('model') == model_name or model.get('model').replace(":latest", "") == model_name] + if len(exist) == 0: + raise AppApiException(ValidCode.model_not_fount, + _('The model does not exist, please download the model first')) + + try: + model: OllamaReranker = provider.get_model(model_type, model_name, model_credential) + model.compress_documents([Document(page_content=gettext('Hello'))], gettext('Hello')) + except Exception as e: + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model_info: Dict[str, object]): + return model_info + + def build_model(self, model_info: Dict[str, object]): + for key in ['model']: + if key not in model_info: + raise AppApiException(500, _('{key} is required').format(key=key)) + return self + + api_base = forms.TextInputField('API URL', required=True) diff --git a/apps/models_provider/impl/ollama_model_provider/icon/ollama_icon_svg b/apps/models_provider/impl/ollama_model_provider/icon/ollama_icon_svg new file mode 100644 index 000000000..ba96d43b5 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/icon/ollama_icon_svg @@ -0,0 +1,3 @@ + + diff --git a/apps/models_provider/impl/ollama_model_provider/model/embedding.py b/apps/models_provider/impl/ollama_model_provider/model/embedding.py new file mode 100644 index 000000000..8454aafe9 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/model/embedding.py @@ -0,0 +1,48 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 15:02 + @desc: +""" +from typing import Dict, List + +from langchain_community.embeddings import OllamaEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class OllamaEmbedding(MaxKBBaseModel, OllamaEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return OllamaEmbedding( + model=model_name, + base_url=model_credential.get('api_base'), + ) + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Embed documents using an Ollama deployed embedding model. + + Args: + texts: The list of texts to embed. + + Returns: + List of embeddings, one for each text. + """ + instruction_pairs = [f"{text}" for text in texts] + embeddings = self._embed(instruction_pairs) + return embeddings + + def embed_query(self, text: str) -> List[float]: + """Embed a query using a Ollama deployed embedding model. + + Args: + text: The text to embed. + + Returns: + Embeddings for the text. + """ + instruction_pair = f"{text}" + embedding = self._embed([instruction_pair])[0] + return embedding diff --git a/apps/models_provider/impl/ollama_model_provider/model/image.py b/apps/models_provider/impl/ollama_model_provider/model/image.py new file mode 100644 index 000000000..c2cdd7210 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/model/image.py @@ -0,0 +1,32 @@ +from typing import Dict +from urllib.parse import urlparse, ParseResult + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +class OllamaImage(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + api_base = model_credential.get('api_base', '') + base_url = get_base_url(api_base) + base_url = base_url if base_url.endswith('/v1') else (base_url + '/v1') + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return OllamaImage( + model_name=model_name, + openai_api_base=base_url, + openai_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/ollama_model_provider/model/llm.py b/apps/models_provider/impl/ollama_model_provider/model/llm.py new file mode 100644 index 000000000..047582426 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/model/llm.py @@ -0,0 +1,48 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/3/6 11:48 + @desc: +""" +from typing import List, Dict +from urllib.parse import urlparse, ParseResult + +from langchain_core.messages import BaseMessage, get_buffer_string +from langchain_ollama.chat_models import ChatOllama + + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +class OllamaChatModel(MaxKBBaseModel, ChatOllama): + @staticmethod + def is_cache_model(): + return False + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + api_base = model_credential.get('api_base', '') + base_url = get_base_url(api_base) + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + + return OllamaChatModel(model=model_name, base_url=base_url, + stream=True, **optional_params) + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + + def get_num_tokens(self, text: str) -> int: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) diff --git a/apps/models_provider/impl/ollama_model_provider/model/reranker.py b/apps/models_provider/impl/ollama_model_provider/model/reranker.py new file mode 100644 index 000000000..22d6e63d2 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/model/reranker.py @@ -0,0 +1,49 @@ +from typing import Sequence, Optional, Any, Dict + +from langchain_community.embeddings import OllamaEmbeddings +from langchain_core.callbacks import Callbacks +from langchain_core.documents import Document +from models_provider.base_model_provider import MaxKBBaseModel +from sklearn.metrics.pairwise import cosine_similarity +from pydantic import BaseModel, Field + + +class OllamaReranker(MaxKBBaseModel, OllamaEmbeddings, BaseModel): + top_n: Optional[int] = Field(3, description="Number of top documents to return") + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return OllamaReranker( + model=model_name, + base_url=model_credential.get('api_base'), + **optional_params + ) + + def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \ + Sequence[Document]: + """Rank documents based on their similarity to the query. + + Args: + query: The query text. + documents: The list of document texts to rank. + + Returns: + List of documents sorted by relevance to the query. + """ + # 获取查询和文档的嵌入 + query_embedding = self.embed_query(query) + documents = [doc.page_content for doc in documents] + document_embeddings = self.embed_documents(documents) + # 计算相似度 + similarities = cosine_similarity([query_embedding], document_embeddings)[0] + ranked_docs = [(doc,_) for _, doc in sorted(zip(similarities, documents), reverse=True)][:self.top_n] + return [ + Document( + page_content=doc, # 第一个值是文档内容 + metadata={'relevance_score': score} # 第二个值是相似度分数 + ) + for doc, score in ranked_docs + ] + + diff --git a/apps/models_provider/impl/ollama_model_provider/ollama_model_provider.py b/apps/models_provider/impl/ollama_model_provider/ollama_model_provider.py new file mode 100644 index 000000000..2ad2107e1 --- /dev/null +++ b/apps/models_provider/impl/ollama_model_provider/ollama_model_provider.py @@ -0,0 +1,279 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: ollama_model_provider.py + @date:2024/3/5 17:23 + @desc: +""" +import json +import os +from typing import Dict, Iterator +from urllib.parse import urlparse, ParseResult + +import requests +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, \ + BaseModelCredential, DownModelChunk, DownModelChunkStatus, ValidCode, ModelInfoManage +from models_provider.impl.ollama_model_provider.credential.embedding import OllamaEmbeddingModelCredential +from models_provider.impl.ollama_model_provider.credential.image import OllamaImageModelCredential +from models_provider.impl.ollama_model_provider.credential.llm import OllamaLLMModelCredential +from models_provider.impl.ollama_model_provider.credential.reranker import OllamaReRankModelCredential +from models_provider.impl.ollama_model_provider.model.embedding import OllamaEmbedding +from models_provider.impl.ollama_model_provider.model.image import OllamaImage +from models_provider.impl.ollama_model_provider.model.llm import OllamaChatModel +from models_provider.impl.ollama_model_provider.model.reranker import OllamaReranker +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +"" + +ollama_llm_model_credential = OllamaLLMModelCredential() +model_info_list = [ + ModelInfo( + 'deepseek-r1:1.5b', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'deepseek-r1:7b', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'deepseek-r1:8b', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'deepseek-r1:14b', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'deepseek-r1:32b', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + + ModelInfo( + 'llama2', + _('Llama 2 is a set of pretrained and fine-tuned generative text models ranging in size from 7 billion to 70 billion. This is a repository of 7B pretrained models. Links to other models can be found in the index at the bottom.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'llama2:13b', + _('Llama 2 is a set of pretrained and fine-tuned generative text models ranging in size from 7 billion to 70 billion. This is a repository of 13B pretrained models. Links to other models can be found in the index at the bottom.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'llama2:70b', + _('Llama 2 is a set of pretrained and fine-tuned generative text models ranging in size from 7 billion to 70 billion. This is a repository of 70B pretrained models. Links to other models can be found in the index at the bottom.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'llama2-chinese:13b', + _('Since the Chinese alignment of Llama2 itself is weak, we use the Chinese instruction set to fine-tune meta-llama/Llama-2-13b-chat-hf with LoRA so that it has strong Chinese conversation capabilities.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'llama3:8b', + _('Meta Llama 3: The most capable public product LLM to date. 8 billion parameters.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'llama3:70b', + _('Meta Llama 3: The most capable public product LLM to date. 70 billion parameters.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:0.5b', + _("Compared with previous versions, qwen 1.5 0.5b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 500 million parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:1.8b', + _("Compared with previous versions, qwen 1.5 1.8b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 1.8 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:4b', + _("Compared with previous versions, qwen 1.5 4b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 4 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + + ModelInfo( + 'qwen:7b', + _("Compared with previous versions, qwen 1.5 7b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 7 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:14b', + _("Compared with previous versions, qwen 1.5 14b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 14 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:32b', + _("Compared with previous versions, qwen 1.5 32b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 32 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:72b', + _("Compared with previous versions, qwen 1.5 72b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 72 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen:110b', + _("Compared with previous versions, qwen 1.5 110b has significantly enhanced the model's alignment with human preferences and its multi-language processing capabilities. Models of all sizes support a context length of 32768 tokens. 110 billion parameters."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2:72b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2:57b-a14b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2:7b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:72b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:32b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:14b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:7b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:1.5b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:0.5b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'qwen2.5:3b-instruct', + '', + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), + ModelInfo( + 'phi3', + _("Phi-3 Mini is Microsoft's 3.8B parameter, lightweight, state-of-the-art open model."), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel), +] +ollama_embedding_model_credential = OllamaEmbeddingModelCredential() +ollama_image_model_credential = OllamaImageModelCredential() +ollama_reranker_model_credential = OllamaReRankModelCredential() +embedding_model_info = [ + ModelInfo( + 'nomic-embed-text', + _('A high-performance open embedding model with a large token context window.'), + ModelTypeConst.EMBEDDING, ollama_embedding_model_credential, OllamaEmbedding), +] +reranker_model_info = [ + ModelInfo( + 'linux6200/bge-reranker-v2-m3', + '', + ModelTypeConst.RERANKER, ollama_reranker_model_credential, OllamaReranker), +] + +image_model_info = [ + ModelInfo( + 'llava:7b', + '', + ModelTypeConst.IMAGE, ollama_image_model_credential, OllamaImage), + ModelInfo( + 'llava:13b', + '', + ModelTypeConst.IMAGE, ollama_image_model_credential, OllamaImage), + ModelInfo( + 'llava:34b', + '', + ModelTypeConst.IMAGE, ollama_image_model_credential, OllamaImage), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_model_info_list(embedding_model_info) + .append_default_model_info(ModelInfo( + 'phi3', + _('Phi-3 Mini is Microsoft\'s 3.8B parameter, lightweight, state-of-the-art open model.'), + ModelTypeConst.LLM, ollama_llm_model_credential, OllamaChatModel)) + .append_default_model_info(ModelInfo( + 'nomic-embed-text', + _('A high-performance open embedding model with a large token context window.'), + ModelTypeConst.EMBEDDING, ollama_embedding_model_credential, OllamaEmbedding), ) + .append_model_info_list(image_model_info) + .append_default_model_info(image_model_info[0]) + .append_model_info_list(reranker_model_info) + .append_default_model_info(reranker_model_info[0]) + .build() +) + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +def convert_to_down_model_chunk(row_str: str, chunk_index: int): + row = json.loads(row_str) + status = DownModelChunkStatus.unknown + digest = "" + progress = 100 + if 'status' in row: + digest = row.get('status') + if row.get('status') == 'success': + status = DownModelChunkStatus.success + if row.get('status').__contains__("pulling"): + progress = 0 + status = DownModelChunkStatus.pulling + if 'total' in row and 'completed' in row: + progress = (row.get('completed') / row.get('total') * 100) + elif 'error' in row: + status = DownModelChunkStatus.error + digest = row.get('error') + return DownModelChunk(status=status, digest=digest, progress=progress, details=row_str, index=chunk_index) + + +def convert(response_stream) -> Iterator[DownModelChunk]: + temp = "" + index = 0 + for c in response_stream: + index += 1 + row_content = c.decode() + temp += row_content + if row_content.endswith('}') or row_content.endswith('\n'): + rows = [t for t in temp.split("\n") if len(t) > 0] + for row in rows: + yield convert_to_down_model_chunk(row, index) + temp = "" + + if len(temp) > 0: + rows = [t for t in temp.split("\n") if len(t) > 0] + for row in rows: + yield convert_to_down_model_chunk(row, index) + + +class OllamaModelProvider(IModelProvider): + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_ollama_provider', name='Ollama', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'ollama_model_provider', 'icon', + 'ollama_icon_svg'))) + + @staticmethod + def get_base_model_list(api_base): + base_url = get_base_url(api_base) + r = requests.request(method="GET", url=f"{base_url}/api/tags", timeout=5) + r.raise_for_status() + return r.json() + + def down_model(self, model_type: str, model_name, model_credential: Dict[str, object]) -> Iterator[DownModelChunk]: + api_base = model_credential.get('api_base', '') + base_url = get_base_url(api_base) + r = requests.request( + method="POST", + url=f"{base_url}/api/pull", + data=json.dumps({"name": model_name}).encode(), + stream=True, + ) + return convert(r) diff --git a/apps/models_provider/impl/openai_model_provider/__init__.py b/apps/models_provider/impl/openai_model_provider/__init__.py new file mode 100644 index 000000000..2dc4ab10d --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/3/28 16:25 + @desc: +""" diff --git a/apps/models_provider/impl/openai_model_provider/credential/embedding.py b/apps/models_provider/impl/openai_model_provider/credential/embedding.py new file mode 100644 index 000000000..8e04ef567 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/credential/embedding.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 16:45 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class OpenAIEmbeddingCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True): + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/openai_model_provider/credential/image.py b/apps/models_provider/impl/openai_model_provider/credential/image.py new file mode 100644 index 000000000..f67fc40a8 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/credential/image.py @@ -0,0 +1,74 @@ +# coding=utf-8 +import base64 +import os +import traceback +from typing import Dict + +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext_lazy as _, gettext + + +class OpenAIImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class OpenAIImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return OpenAIImageModelParams() diff --git a/apps/models_provider/impl/openai_model_provider/credential/llm.py b/apps/models_provider/impl/openai_model_provider/credential/llm.py new file mode 100644 index 000000000..c97476c33 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/credential/llm.py @@ -0,0 +1,80 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:32 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage +from openai import BadRequestError + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class OpenAILLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class OpenAILLMModelCredential(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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException) or isinstance(e, BadRequestError): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return OpenAILLMModelParams() diff --git a/apps/models_provider/impl/openai_model_provider/credential/stt.py b/apps/models_provider/impl/openai_model_provider/credential/stt.py new file mode 100644 index 000000000..6a1dd8474 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/credential/stt.py @@ -0,0 +1,49 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class OpenAISTTModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/openai_model_provider/credential/tti.py b/apps/models_provider/impl/openai_model_provider/credential/tti.py new file mode 100644 index 000000000..a266d9b87 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/credential/tti.py @@ -0,0 +1,90 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class OpenAITTIModelParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), + _('The image generation endpoint allows you to create raw images based on text prompts. When using the DALL·E 3, the image size can be 1024x1024, 1024x1792 or 1792x1024 pixels.')), + required=True, + default_value='1024x1024', + option_list=[ + {'value': '1024x1024', 'label': '1024x1024'}, + {'value': '1024x1792', 'label': '1024x1792'}, + {'value': '1792x1024', 'label': '1792x1024'}, + ], + text_field='label', + value_field='value' + ) + + quality = forms.SingleSelect( + TooltipLabel(_('Picture quality'), _(''' +By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest. + ''')), + required=True, + default_value='standard', + option_list=[ + {'value': 'standard', 'label': 'standard'}, + {'value': 'hd', 'label': 'hd'}, + ], + text_field='label', + value_field='value' + ) + + n = forms.SliderField( + TooltipLabel(_('Number of pictures'), + _('You can use DALL·E 3 to request 1 image at a time (requesting more images by issuing parallel requests), or use DALL·E 2 with the n parameter to request up to 10 images at a time.')), + required=True, default_value=1, + _min=1, + _max=10, + _step=1, + precision=0) + + +class OpenAITextToImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return OpenAITTIModelParams() diff --git a/apps/models_provider/impl/openai_model_provider/credential/tts.py b/apps/models_provider/impl/openai_model_provider/credential/tts.py new file mode 100644 index 000000000..b499f3506 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/credential/tts.py @@ -0,0 +1,68 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class OpenAITTSModelGeneralParams(BaseForm): + # alloy, echo, fable, onyx, nova, shimmer + voice = forms.SingleSelect( + TooltipLabel('Voice', + _('Try out the different sounds (Alloy, Echo, Fable, Onyx, Nova, and Sparkle) to find one that suits your desired tone and audience. The current voiceover is optimized for English.')), + required=True, default_value='alloy', + text_field='value', + value_field='value', + option_list=[ + {'text': 'alloy', 'value': 'alloy'}, + {'text': 'echo', 'value': 'echo'}, + {'text': 'fable', 'value': 'fable'}, + {'text': 'onyx', 'value': 'onyx'}, + {'text': 'nova', 'value': 'nova'}, + {'text': 'shimmer', 'value': 'shimmer'}, + ]) + + +class OpenAITTSModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return OpenAITTSModelGeneralParams() diff --git a/apps/models_provider/impl/openai_model_provider/icon/openai_icon_svg b/apps/models_provider/impl/openai_model_provider/icon/openai_icon_svg new file mode 100644 index 000000000..da75338da --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/icon/openai_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/openai_model_provider/model/embedding.py b/apps/models_provider/impl/openai_model_provider/model/embedding.py new file mode 100644 index 000000000..3a0aaeeb1 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/model/embedding.py @@ -0,0 +1,39 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 17:44 + @desc: +""" +from typing import Dict, List + +import openai + +from models_provider.base_model_provider import MaxKBBaseModel + + +class OpenAIEmbeddingModel(MaxKBBaseModel): + model_name: str + + def __init__(self, api_key, base_url, model_name: str): + self.client = openai.OpenAI(api_key=api_key, base_url=base_url).embeddings + self.model_name = model_name + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return OpenAIEmbeddingModel( + api_key=model_credential.get('api_key'), + model_name=model_name, + base_url=model_credential.get('api_base'), + ) + + def embed_query(self, text: str): + res = self.embed_documents([text]) + return res[0] + + def embed_documents( + self, texts: List[str], chunk_size: int | None = None + ) -> List[List[float]]: + res = self.client.create(input=texts, model=self.model_name, encoding_format="float") + return [e.embedding for e in res.data] diff --git a/apps/models_provider/impl/openai_model_provider/model/image.py b/apps/models_provider/impl/openai_model_provider/model/image.py new file mode 100644 index 000000000..7dc6d8c12 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/model/image.py @@ -0,0 +1,20 @@ +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class OpenAIImage(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return OpenAIImage( + model_name=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/openai_model_provider/model/llm.py b/apps/models_provider/impl/openai_model_provider/model/llm.py new file mode 100644 index 000000000..a645fb7c8 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/model/llm.py @@ -0,0 +1,58 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/4/18 15:28 + @desc: +""" +from typing import List, Dict + +from langchain_core.messages import BaseMessage, get_buffer_string +from langchain_openai.chat_models import ChatOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class OpenAIChatModel(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) + streaming = model_kwargs.get('streaming', True) + if 'o1' in model_name: + streaming = False + azure_chat_open_ai = OpenAIChatModel( + model=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + **optional_params, + streaming=streaming, + custom_get_token_ids=custom_get_token_ids + ) + return azure_chat_open_ai + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + try: + return super().get_num_tokens_from_messages(messages) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + + def get_num_tokens(self, text: str) -> int: + try: + return super().get_num_tokens(text) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) diff --git a/apps/models_provider/impl/openai_model_provider/model/stt.py b/apps/models_provider/impl/openai_model_provider/model/stt.py new file mode 100644 index 000000000..accb2cc8a --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/model/stt.py @@ -0,0 +1,59 @@ +import asyncio +import io +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class OpenAISpeechToText(MaxKBBaseModel, BaseSpeechToText): + api_base: str + api_key: str + model: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return OpenAISpeechToText( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def speech_to_text(self, audio_file): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + audio_data = audio_file.read() + buffer = io.BytesIO(audio_data) + buffer.name = "file.mp3" # this is the important line + res = client.audio.transcriptions.create(model=self.model, language="zh", file=buffer) + return res.text + diff --git a/apps/models_provider/impl/openai_model_provider/model/tti.py b/apps/models_provider/impl/openai_model_provider/model/tti.py new file mode 100644 index 000000000..6d62f4d0b --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/model/tti.py @@ -0,0 +1,58 @@ +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class OpenAITextToImage(MaxKBBaseModel, BaseTextToImage): + api_base: str + api_key: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'size': '1024x1024', 'quality': 'standard', 'n': 1}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return OpenAITextToImage( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def is_cache_model(self): + return False + + def check_auth(self): + chat = OpenAI(api_key=self.api_key, base_url=self.api_base) + response_list = chat.models.with_raw_response.list() + + # self.generate_image('生成一个小猫图片') + + def generate_image(self, prompt: str, negative_prompt: str = None): + chat = OpenAI(api_key=self.api_key, base_url=self.api_base) + res = chat.images.generate(model=self.model, prompt=prompt, **self.params) + file_urls = [] + for content in res.data: + url = content.url + file_urls.append(url) + + return file_urls diff --git a/apps/models_provider/impl/openai_model_provider/model/tts.py b/apps/models_provider/impl/openai_model_provider/model/tts.py new file mode 100644 index 000000000..382a220e5 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/model/tts.py @@ -0,0 +1,64 @@ +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class OpenAITextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + api_base: str + api_key: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'voice': 'alloy'}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return OpenAITextToSpeech( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def text_to_speech(self, text): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + text = _remove_empty_lines(text) + with client.audio.speech.with_streaming_response.create( + model=self.model, + input=text, + **self.params + ) as response: + return response.read() + + def is_cache_model(self): + return False diff --git a/apps/models_provider/impl/openai_model_provider/openai_model_provider.py b/apps/models_provider/impl/openai_model_provider/openai_model_provider.py new file mode 100644 index 000000000..028466c58 --- /dev/null +++ b/apps/models_provider/impl/openai_model_provider/openai_model_provider.py @@ -0,0 +1,147 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: openai_model_provider.py + @date:2024/3/28 16:26 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, \ + ModelTypeConst, ModelInfoManage +from models_provider.impl.openai_model_provider.credential.embedding import OpenAIEmbeddingCredential +from models_provider.impl.openai_model_provider.credential.image import OpenAIImageModelCredential +from models_provider.impl.openai_model_provider.credential.llm import OpenAILLMModelCredential +from models_provider.impl.openai_model_provider.credential.stt import OpenAISTTModelCredential +from models_provider.impl.openai_model_provider.credential.tti import OpenAITextToImageModelCredential +from models_provider.impl.openai_model_provider.credential.tts import OpenAITTSModelCredential +from models_provider.impl.openai_model_provider.model.embedding import OpenAIEmbeddingModel +from models_provider.impl.openai_model_provider.model.image import OpenAIImage +from models_provider.impl.openai_model_provider.model.llm import OpenAIChatModel +from models_provider.impl.openai_model_provider.model.stt import OpenAISpeechToText +from models_provider.impl.openai_model_provider.model.tti import OpenAITextToImage +from models_provider.impl.openai_model_provider.model.tts import OpenAITextToSpeech +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext_lazy as _ + +openai_llm_model_credential = OpenAILLMModelCredential() +openai_stt_model_credential = OpenAISTTModelCredential() +openai_tts_model_credential = OpenAITTSModelCredential() +openai_image_model_credential = OpenAIImageModelCredential() +openai_tti_model_credential = OpenAITextToImageModelCredential() +model_info_list = [ + ModelInfo('gpt-3.5-turbo', _('The latest gpt-3.5-turbo, updated with OpenAI adjustments'), ModelTypeConst.LLM, + openai_llm_model_credential, OpenAIChatModel + ), + ModelInfo('gpt-4', _('Latest gpt-4, updated with OpenAI adjustments'), ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4o', _('The latest GPT-4o, cheaper and faster than gpt-4-turbo, updated with OpenAI adjustments'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4o-mini', _('The latest gpt-4o-mini, cheaper and faster than gpt-4o, updated with OpenAI adjustments'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4-turbo', _('The latest gpt-4-turbo, updated with OpenAI adjustments'), ModelTypeConst.LLM, + openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4-turbo-preview', _('The latest gpt-4-turbo-preview, updated with OpenAI adjustments'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-3.5-turbo-0125', + _('gpt-3.5-turbo snapshot on January 25, 2024, supporting context length 16,385 tokens'), ModelTypeConst.LLM, + openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-3.5-turbo-1106', + _('gpt-3.5-turbo snapshot on November 6, 2023, supporting context length 16,385 tokens'), ModelTypeConst.LLM, + openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-3.5-turbo-0613', + _('[Legacy] gpt-3.5-turbo snapshot on June 13, 2023, will be deprecated on June 13, 2024'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4o-2024-05-13', + _('gpt-4o snapshot on May 13, 2024, supporting context length 128,000 tokens'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4-turbo-2024-04-09', + _('gpt-4-turbo snapshot on April 9, 2024, supporting context length 128,000 tokens'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4-0125-preview', _('gpt-4-turbo snapshot on January 25, 2024, supporting context length 128,000 tokens'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('gpt-4-1106-preview', _('gpt-4-turbo snapshot on November 6, 2023, supporting context length 128,000 tokens'), + ModelTypeConst.LLM, openai_llm_model_credential, + OpenAIChatModel), + ModelInfo('whisper-1', '', + ModelTypeConst.STT, openai_stt_model_credential, + OpenAISpeechToText), + ModelInfo('tts-1', '', + ModelTypeConst.TTS, openai_tts_model_credential, + OpenAITextToSpeech) +] +open_ai_embedding_credential = OpenAIEmbeddingCredential() +model_info_embedding_list = [ + ModelInfo('text-embedding-ada-002', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + OpenAIEmbeddingModel), + ModelInfo('text-embedding-3-small', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + OpenAIEmbeddingModel), + ModelInfo('text-embedding-3-large', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + OpenAIEmbeddingModel) +] + +model_info_image_list = [ + ModelInfo('gpt-4o', _('The latest GPT-4o, cheaper and faster than gpt-4-turbo, updated with OpenAI adjustments'), + ModelTypeConst.IMAGE, openai_image_model_credential, + OpenAIImage), + ModelInfo('gpt-4o-mini', _('The latest gpt-4o-mini, cheaper and faster than gpt-4o, updated with OpenAI adjustments'), + ModelTypeConst.IMAGE, openai_image_model_credential, + OpenAIImage), +] + +model_info_tti_list = [ + ModelInfo('dall-e-2', '', + ModelTypeConst.TTI, openai_tti_model_credential, + OpenAITextToImage), + ModelInfo('dall-e-3', '', + ModelTypeConst.TTI, openai_tti_model_credential, + OpenAITextToImage), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info(ModelInfo('gpt-3.5-turbo', _('The latest gpt-3.5-turbo, updated with OpenAI adjustments'), ModelTypeConst.LLM, + openai_llm_model_credential, OpenAIChatModel + )) + .append_model_info_list(model_info_embedding_list) + .append_default_model_info(model_info_embedding_list[0]) + .append_model_info_list(model_info_image_list) + .append_default_model_info(model_info_image_list[0]) + .append_model_info_list(model_info_tti_list) + .append_default_model_info(model_info_tti_list[0]) + .append_default_model_info(ModelInfo('whisper-1', '', + ModelTypeConst.STT, openai_stt_model_credential, + OpenAISpeechToText) + ) + .append_default_model_info(ModelInfo('tts-1', '', + ModelTypeConst.TTS, openai_tts_model_credential, + OpenAITextToSpeech)) + .build() +) + + +class OpenAIModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_openai_provider', name='OpenAI', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'openai_model_provider', 'icon', + 'openai_icon_svg'))) diff --git a/apps/models_provider/impl/qwen_model_provider/__init__.py b/apps/models_provider/impl/qwen_model_provider/__init__.py new file mode 100644 index 000000000..53b7001e5 --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2023/10/31 17:16 + @desc: +""" diff --git a/apps/models_provider/impl/qwen_model_provider/credential/image.py b/apps/models_provider/impl/qwen_model_provider/credential/image.py new file mode 100644 index 000000000..6c99bd3f0 --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/credential/image.py @@ -0,0 +1,78 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:41 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QwenModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=1.0, + _min=0.1, + _max=1.9, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class QwenVLModelCredential(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)) + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return QwenModelParams() diff --git a/apps/models_provider/impl/qwen_model_provider/credential/llm.py b/apps/models_provider/impl/qwen_model_provider/credential/llm.py new file mode 100644 index 000000000..b697556cf --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/credential/llm.py @@ -0,0 +1,76 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:41 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QwenModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=1.0, + _min=0.1, + _max=1.9, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class OpenAILLMModelCredential(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)) + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return QwenModelParams() diff --git a/apps/models_provider/impl/qwen_model_provider/credential/tti.py b/apps/models_provider/impl/qwen_model_provider/credential/tti.py new file mode 100644 index 000000000..c0a468d64 --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/credential/tti.py @@ -0,0 +1,98 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:41 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QwenModelParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), _('Specify the size of the generated image, such as: 1024x1024')), + required=True, + default_value='1024*1024', + option_list=[ + {'value': '1024*1024', 'label': '1024*1024'}, + {'value': '720*1280', 'label': '720*1280'}, + {'value': '768*1152', 'label': '768*1152'}, + {'value': '1280*720', 'label': '1280*720'}, + ], + text_field='label', + value_field='value') + n = forms.SliderField( + TooltipLabel(_('Number of pictures'), _('Specify the number of generated images')), + required=True, default_value=1, + _min=1, + _max=4, + _step=1, + precision=0) + style = forms.SingleSelect( + TooltipLabel(_('Style'), _('Specify the style of generated images')), + required=True, + default_value='', + option_list=[ + {'value': '', 'label': _('Default value, the image style is randomly output by the model')}, + {'value': '', 'label': _('photography')}, + {'value': '', 'label': _('Portraits')}, + {'value': '<3d cartoon>', 'label': _('3D cartoon')}, + {'value': '', 'label': _('animation')}, + {'value': '', 'label': _('painting')}, + {'value': '', 'label': _('watercolor')}, + {'value': '', 'label': _('sketch')}, + {'value': '', 'label': _('Chinese painting')}, + {'value': '', 'label': _('flat illustration')}, + ], + text_field='label', + value_field='value' + ) + + +class QwenTextToImageModelCredential(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)) + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return QwenModelParams() diff --git a/apps/models_provider/impl/qwen_model_provider/icon/qwen_icon_svg b/apps/models_provider/impl/qwen_model_provider/icon/qwen_icon_svg new file mode 100644 index 000000000..cb9a718af --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/icon/qwen_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/qwen_model_provider/model/image.py b/apps/models_provider/impl/qwen_model_provider/model/image.py new file mode 100644 index 000000000..ac74a8f49 --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/model/image.py @@ -0,0 +1,26 @@ +# coding=utf-8 + +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class QwenVLChatModel(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) + chat_tong_yi = QwenVLChatModel( + model_name=model_name, + openai_api_key=model_credential.get('api_key'), + openai_api_base='https://dashscope.aliyuncs.com/compatible-mode/v1', + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) + return chat_tong_yi diff --git a/apps/models_provider/impl/qwen_model_provider/model/llm.py b/apps/models_provider/impl/qwen_model_provider/model/llm.py new file mode 100644 index 000000000..893121290 --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/model/llm.py @@ -0,0 +1,31 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/4/28 11:44 + @desc: +""" +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class QwenChatModel(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) + chat_tong_yi = QwenChatModel( + model_name=model_name, + openai_api_key=model_credential.get('api_key'), + openai_api_base='https://dashscope.aliyuncs.com/compatible-mode/v1', + streaming=True, + stream_usage=True, + **optional_params, + ) + return chat_tong_yi diff --git a/apps/models_provider/impl/qwen_model_provider/model/tti.py b/apps/models_provider/impl/qwen_model_provider/model/tti.py new file mode 100644 index 000000000..48db77831 --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/model/tti.py @@ -0,0 +1,59 @@ +# coding=utf-8 +from http import HTTPStatus +from typing import Dict + +from dashscope import ImageSynthesis +from django.utils.translation import gettext +from langchain_community.chat_models import ChatTongyi +from langchain_core.messages import HumanMessage + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + + +class QwenTextToImageModel(MaxKBBaseModel, BaseTextToImage): + api_key: str + model_name: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.model_name = kwargs.get('model_name') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'size': '1024*1024', 'style': '', 'n': 1}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + chat_tong_yi = QwenTextToImageModel( + model_name=model_name, + api_key=model_credential.get('api_key'), + **optional_params, + ) + return chat_tong_yi + + def is_cache_model(self): + return False + + def check_auth(self): + chat = ChatTongyi(api_key=self.api_key, model_name='qwen-max') + chat.invoke([HumanMessage([{"type": "text", "text": gettext('Hello')}])]) + + def generate_image(self, prompt: str, negative_prompt: str = None): + # api_base='https://dashscope.aliyuncs.com/compatible-mode/v1', + rsp = ImageSynthesis.call(api_key=self.api_key, + model=self.model_name, + prompt=prompt, + negative_prompt=negative_prompt, + **self.params) + file_urls = [] + if rsp.status_code == HTTPStatus.OK: + for result in rsp.output.results: + file_urls.append(result.url) + else: + print('sync_call Failed, status_code: %s, code: %s, message: %s' % + (rsp.status_code, rsp.code, rsp.message)) + return file_urls diff --git a/apps/models_provider/impl/qwen_model_provider/qwen_model_provider.py b/apps/models_provider/impl/qwen_model_provider/qwen_model_provider.py new file mode 100644 index 000000000..e894e2d8d --- /dev/null +++ b/apps/models_provider/impl/qwen_model_provider/qwen_model_provider.py @@ -0,0 +1,65 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: qwen_model_provider.py + @date:2023/10/31 16:19 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import ModelProvideInfo, ModelTypeConst, ModelInfo, IModelProvider, \ + ModelInfoManage +from models_provider.impl.qwen_model_provider.credential.image import QwenVLModelCredential +from models_provider.impl.qwen_model_provider.credential.llm import OpenAILLMModelCredential +from models_provider.impl.qwen_model_provider.credential.tti import QwenTextToImageModelCredential +from models_provider.impl.qwen_model_provider.model.image import QwenVLChatModel + +from models_provider.impl.qwen_model_provider.model.llm import QwenChatModel +from models_provider.impl.qwen_model_provider.model.tti import QwenTextToImageModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +qwen_model_credential = OpenAILLMModelCredential() +qwenvl_model_credential = QwenVLModelCredential() +qwentti_model_credential = QwenTextToImageModelCredential() + +module_info_list = [ + ModelInfo('qwen-turbo', '', ModelTypeConst.LLM, qwen_model_credential, QwenChatModel), + ModelInfo('qwen-plus', '', ModelTypeConst.LLM, qwen_model_credential, QwenChatModel), + ModelInfo('qwen-max', '', ModelTypeConst.LLM, qwen_model_credential, QwenChatModel) +] +module_info_vl_list = [ + ModelInfo('qwen-vl-max', '', ModelTypeConst.IMAGE, qwenvl_model_credential, QwenVLChatModel), + ModelInfo('qwen-vl-max-0809', '', ModelTypeConst.IMAGE, qwenvl_model_credential, QwenVLChatModel), + ModelInfo('qwen-vl-plus-0809', '', ModelTypeConst.IMAGE, qwenvl_model_credential, QwenVLChatModel), +] +module_info_tti_list = [ + ModelInfo('wanx-v1', + _('Tongyi Wanxiang - a large image model for text generation, supports bilingual input in Chinese and English, and supports the input of reference pictures for reference content or reference style migration. Key styles include but are not limited to watercolor, oil painting, Chinese painting, sketch, flat illustration, two-dimensional, and 3D. Cartoon.'), + ModelTypeConst.TTI, qwentti_model_credential, QwenTextToImageModel), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(module_info_list) + .append_default_model_info( + ModelInfo('qwen-turbo', '', ModelTypeConst.LLM, qwen_model_credential, QwenChatModel)) + .append_model_info_list(module_info_vl_list) + .append_default_model_info(module_info_vl_list[0]) + .append_model_info_list(module_info_tti_list) + .append_default_model_info(module_info_tti_list[0]) + .build() +) + + +class QwenModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_qwen_provider', name=_('Tongyi Qianwen'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'qwen_model_provider', 'icon', + 'qwen_icon_svg'))) diff --git a/apps/models_provider/impl/siliconCloud_model_provider/__init__.py b/apps/models_provider/impl/siliconCloud_model_provider/__init__.py new file mode 100644 index 000000000..2dc4ab10d --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/3/28 16:25 + @desc: +""" diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/embedding.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/embedding.py new file mode 100644 index 000000000..0e9dc2e5f --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/embedding.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 16:45 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class SiliconCloudEmbeddingCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True): + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/image.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/image.py new file mode 100644 index 000000000..fa2c74eda --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/image.py @@ -0,0 +1,74 @@ +# coding=utf-8 +import base64 +import os +import traceback +from typing import Dict + +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from django.utils.translation import gettext_lazy as _, gettext + + +class SiliconCloudImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class SiliconCloudImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return SiliconCloudImageModelParams() diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/llm.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/llm.py new file mode 100644 index 000000000..903ffd7a2 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/llm.py @@ -0,0 +1,79 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:32 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class SiliconCloudLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class SiliconCloudLLMModelCredential(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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return SiliconCloudLLMModelParams() diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/reranker.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/reranker.py new file mode 100644 index 000000000..a7c94a0e2 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/reranker.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: reranker.py + @date:2024/9/9 17:51 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ +from langchain_core.documents import Document + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.siliconCloud_model_provider.model.reranker import SiliconCloudReranker + + +class SiliconCloudRerankerCredential(BaseForm, BaseModelCredential): + + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=False): + if not model_type == 'RERANKER': + raise AppApiException(ValidCode.valid_error.value, + _('{model_type} Model type is not supported').format(model_type=model_type)) + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model: SiliconCloudReranker = provider.get_model(model_type, model_name, model_credential) + model.compress_documents([Document(page_content=_('Hello'))], _('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/stt.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/stt.py new file mode 100644 index 000000000..6ce4e8791 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/stt.py @@ -0,0 +1,49 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class SiliconCloudSTTModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py new file mode 100644 index 000000000..7a29d6e44 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/tti.py @@ -0,0 +1,90 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class SiliconCloudTTIModelParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), + _('The image generation endpoint allows you to create raw images based on text prompts. When using the DALL·E 3, the image size can be 1024x1024, 1024x1792 or 1792x1024 pixels.')), + required=True, + default_value='1024x1024', + option_list=[ + {'value': '1024x1024', 'label': '1024x1024'}, + {'value': '1024x1792', 'label': '1024x1792'}, + {'value': '1792x1024', 'label': '1792x1024'}, + ], + text_field='label', + value_field='value' + ) + + quality = forms.SingleSelect( + TooltipLabel(_('Picture quality'), _(''' +By default, images are produced in standard quality, but with DALL·E 3 you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest. + ''')), + required=True, + default_value='standard', + option_list=[ + {'value': 'standard', 'label': 'standard'}, + {'value': 'hd', 'label': 'hd'}, + ], + text_field='label', + value_field='value' + ) + + n = forms.SliderField( + TooltipLabel(_('Number of pictures'), + _('You can use DALL·E 3 to request 1 image at a time (requesting more images by issuing parallel requests), or use DALL·E 2 with the n parameter to request up to 10 images at a time.')), + required=True, default_value=1, + _min=1, + _max=10, + _step=1, + precision=0) + + +class SiliconCloudTextToImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return SiliconCloudTTIModelParams() diff --git a/apps/models_provider/impl/siliconCloud_model_provider/credential/tts.py b/apps/models_provider/impl/siliconCloud_model_provider/credential/tts.py new file mode 100644 index 000000000..dc0d8228b --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/credential/tts.py @@ -0,0 +1,48 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class SiliconCloudTTSModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + diff --git a/apps/models_provider/impl/siliconCloud_model_provider/icon/siliconCloud_icon_svg b/apps/models_provider/impl/siliconCloud_model_provider/icon/siliconCloud_icon_svg new file mode 100644 index 000000000..339fff751 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/icon/siliconCloud_icon_svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/embedding.py b/apps/models_provider/impl/siliconCloud_model_provider/model/embedding.py new file mode 100644 index 000000000..6a347ecf3 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/embedding.py @@ -0,0 +1,23 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 17:44 + @desc: +""" +from typing import Dict + +from langchain_community.embeddings import OpenAIEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class SiliconCloudEmbeddingModel(MaxKBBaseModel, OpenAIEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return SiliconCloudEmbeddingModel( + api_key=model_credential.get('api_key'), + model=model_name, + openai_api_base=model_credential.get('api_base'), + ) diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/image.py b/apps/models_provider/impl/siliconCloud_model_provider/model/image.py new file mode 100644 index 000000000..3f4efc095 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/image.py @@ -0,0 +1,20 @@ +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class SiliconCloudImage(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return SiliconCloudImage( + model_name=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/llm.py b/apps/models_provider/impl/siliconCloud_model_provider/model/llm.py new file mode 100644 index 000000000..256fed80a --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/llm.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/4/18 15:28 + @desc: +""" +from typing import List, Dict + +from langchain_core.messages import BaseMessage, get_buffer_string +from langchain_openai.chat_models import ChatOpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class SiliconCloudChatModel(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 SiliconCloudChatModel( + model=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + **optional_params + ) diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/reranker.py b/apps/models_provider/impl/siliconCloud_model_provider/model/reranker.py new file mode 100644 index 000000000..4ff71b9f9 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/reranker.py @@ -0,0 +1,74 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: siliconcloud_reranker.py + @date:2024/9/10 9:45 + @desc: SiliconCloud 文档重排封装 +""" + +from typing import Sequence, Optional, Any, Dict +import requests + +from langchain_core.callbacks import Callbacks +from langchain_core.documents import BaseDocumentCompressor, Document + +from models_provider.base_model_provider import MaxKBBaseModel +from django.utils.translation import gettext as _ + + +class SiliconCloudReranker(MaxKBBaseModel, BaseDocumentCompressor): + api_base: Optional[str] + """SiliconCloud API URL""" + model: Optional[str] + """SiliconCloud 重排模型 ID""" + api_key: Optional[str] + """API Key""" + + top_n: Optional[int] = 3 # 取前 N 个最相关的结果 + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return SiliconCloudReranker( + api_base=model_credential.get('api_base'), + model=model_name, + api_key=model_credential.get('api_key'), + top_n=model_kwargs.get('top_n', 3) + ) + + def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \ + Sequence[Document]: + if not documents: + return [] + + # 预处理文本 + texts = [doc.page_content for doc in documents] + + # 发送请求到 SiliconCloud API + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + payload = { + "model": self.model, + "query": query, + "documents": texts, + "top_n": self.top_n, + "return_documents": True, + } + + response = requests.post(f"{self.api_base}/rerank", json=payload, headers=headers) + + if response.status_code != 200: + raise RuntimeError(f"SiliconCloud API 请求失败: {response.text}") + + res = response.json() + + # 解析返回结果 + return [ + Document( + page_content=item.get('document', {}).get('text', ''), + metadata={'relevance_score': item.get('relevance_score')} + ) + for item in res.get('results', []) + ] diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/stt.py b/apps/models_provider/impl/siliconCloud_model_provider/model/stt.py new file mode 100644 index 000000000..54899d68d --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/stt.py @@ -0,0 +1,59 @@ +import asyncio +import io +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class SiliconCloudSpeechToText(MaxKBBaseModel, BaseSpeechToText): + api_base: str + api_key: str + model: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return SiliconCloudSpeechToText( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def speech_to_text(self, audio_file): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + audio_data = audio_file.read() + buffer = io.BytesIO(audio_data) + buffer.name = "file.mp3" # this is the important line + res = client.audio.transcriptions.create(model=self.model, language="zh", file=buffer) + return res.text + diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/tti.py b/apps/models_provider/impl/siliconCloud_model_provider/model/tti.py new file mode 100644 index 000000000..194ded361 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/tti.py @@ -0,0 +1,58 @@ +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class SiliconCloudTextToImage(MaxKBBaseModel, BaseTextToImage): + api_base: str + api_key: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'size': '1024x1024', 'quality': 'standard', 'n': 1}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return SiliconCloudTextToImage( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def is_cache_model(self): + return False + + def check_auth(self): + chat = OpenAI(api_key=self.api_key, base_url=self.api_base) + response_list = chat.models.with_raw_response.list() + + # self.generate_image('生成一个小猫图片') + + def generate_image(self, prompt: str, negative_prompt: str = None): + chat = OpenAI(api_key=self.api_key, base_url=self.api_base) + res = chat.images.generate(model=self.model, prompt=prompt, **self.params) + file_urls = [] + for content in res.data: + url = content.url + file_urls.append(url) + + return file_urls diff --git a/apps/models_provider/impl/siliconCloud_model_provider/model/tts.py b/apps/models_provider/impl/siliconCloud_model_provider/model/tts.py new file mode 100644 index 000000000..9d3bb77f5 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/model/tts.py @@ -0,0 +1,87 @@ +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class SiliconCloudTextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + api_base: str + api_key: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'voice': 'alloy'}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return SiliconCloudTextToSpeech( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def text_to_speech(self, text): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + text = _remove_empty_lines(text) + with client.audio.speech.with_streaming_response.create( + model=self.model, + input=text, + **self.params + ) as response: + return response.read() + + import requests + + url = "https://api.siliconflow.cn/v1/audio/speech" + + payload = { + "model": "FunAudioLLM/CosyVoice2-0.5B", + "input": "Can you say it with a happy emotion? <|endofprompt|>I'm so happy, Spring Festival is coming!", + "voice": "FunAudioLLM/CosyVoice2-0.5B:alex", + "response_format": "mp3", + "sample_rate": 123, + "stream": True, + "speed": 1, + "gain": 0 + } + headers = { + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + + response = requests.request("POST", url, json=payload, headers=headers) + + print(response.text) + + def is_cache_model(self): + return False diff --git a/apps/models_provider/impl/siliconCloud_model_provider/siliconCloud_model_provider.py b/apps/models_provider/impl/siliconCloud_model_provider/siliconCloud_model_provider.py new file mode 100644 index 000000000..c9e545098 --- /dev/null +++ b/apps/models_provider/impl/siliconCloud_model_provider/siliconCloud_model_provider.py @@ -0,0 +1,146 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: openai_model_provider.py + @date:2024/3/28 16:26 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, \ + ModelTypeConst, ModelInfoManage +from models_provider.impl.openai_model_provider.openai_model_provider import openai_tts_model_credential +from models_provider.impl.siliconCloud_model_provider.credential.embedding import \ + SiliconCloudEmbeddingCredential +from models_provider.impl.siliconCloud_model_provider.credential.llm import SiliconCloudLLMModelCredential +from models_provider.impl.siliconCloud_model_provider.credential.reranker import SiliconCloudRerankerCredential +from models_provider.impl.siliconCloud_model_provider.credential.stt import SiliconCloudSTTModelCredential +from models_provider.impl.siliconCloud_model_provider.credential.tti import \ + SiliconCloudTextToImageModelCredential +from models_provider.impl.siliconCloud_model_provider.model.embedding import SiliconCloudEmbeddingModel +from models_provider.impl.siliconCloud_model_provider.model.llm import SiliconCloudChatModel +from models_provider.impl.siliconCloud_model_provider.model.reranker import SiliconCloudReranker +from models_provider.impl.siliconCloud_model_provider.model.stt import SiliconCloudSpeechToText +from models_provider.impl.siliconCloud_model_provider.model.tti import SiliconCloudTextToImage +from models_provider.impl.siliconCloud_model_provider.model.tts import SiliconCloudTextToSpeech +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +openai_llm_model_credential = SiliconCloudLLMModelCredential() +openai_stt_model_credential = SiliconCloudSTTModelCredential() +openai_reranker_model_credential = SiliconCloudRerankerCredential() +openai_tti_model_credential = SiliconCloudTextToImageModelCredential() +model_info_list = [ + ModelInfo('deepseek-ai/DeepSeek-R1-Distill-Llama-8B', '', ModelTypeConst.LLM, + openai_llm_model_credential, SiliconCloudChatModel + ), + ModelInfo('deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', '', ModelTypeConst.LLM, + openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B', '', + ModelTypeConst.LLM, openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('Qwen/Qwen2.5-7B-Instruct', + '', + ModelTypeConst.LLM, openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('Qwen/Qwen2.5-Coder-7B-Instruct', '', + ModelTypeConst.LLM, openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('internlm/internlm2_5-7b-chat', '', + ModelTypeConst.LLM, openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('Qwen/Qwen2-1.5B-Instruct', '', + ModelTypeConst.LLM, openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('THUDM/glm-4-9b-chat', '', + ModelTypeConst.LLM, openai_llm_model_credential, + SiliconCloudChatModel), + ModelInfo('FunAudioLLM/SenseVoiceSmall', '', + ModelTypeConst.STT, openai_stt_model_credential, + SiliconCloudSpeechToText), +] +open_ai_embedding_credential = SiliconCloudEmbeddingCredential() +model_info_embedding_list = [ + ModelInfo('netease-youdao/bce-embedding-base_v1', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + SiliconCloudEmbeddingModel), + ModelInfo('BAAI/bge-m3', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + SiliconCloudEmbeddingModel), + ModelInfo('BAAI/bge-large-en-v1.5', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + SiliconCloudEmbeddingModel), + ModelInfo('BAAI/bge-large-zh-v1.5', '', + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + SiliconCloudEmbeddingModel), +] + +model_info_tti_list = [ + ModelInfo('deepseek-ai/Janus-Pro-7B', '', + ModelTypeConst.TTI, openai_tti_model_credential, + SiliconCloudTextToImage), + ModelInfo('stabilityai/stable-diffusion-3-5-large', '', + ModelTypeConst.TTI, openai_tti_model_credential, + SiliconCloudTextToImage), + ModelInfo('black-forest-labs/FLUX.1-schnell', '', + ModelTypeConst.TTI, openai_tti_model_credential, + SiliconCloudTextToImage), + ModelInfo('stabilityai/stable-diffusion-3-medium', '', + ModelTypeConst.TTI, openai_tti_model_credential, + SiliconCloudTextToImage), + ModelInfo('stabilityai/stable-diffusion-xl-base-1.0', '', + ModelTypeConst.TTI, openai_tti_model_credential, + SiliconCloudTextToImage), + ModelInfo('stabilityai/stable-diffusion-2-1', '', + ModelTypeConst.TTI, openai_tti_model_credential, + SiliconCloudTextToImage), +] +model_rerank_list = [ + ModelInfo('netease-youdao/bce-reranker-base_v1', '', ModelTypeConst.RERANKER, + openai_reranker_model_credential, SiliconCloudReranker + ), + ModelInfo('BAAI/bge-reranker-v2-m3', '', ModelTypeConst.RERANKER, + openai_reranker_model_credential, SiliconCloudReranker + ), +] +model_tts_list = [ + ModelInfo('FunAudioLLM/CosyVoice2-0.5B', '', + ModelTypeConst.TTS, openai_tts_model_credential, + SiliconCloudTextToSpeech), +] +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info( + ModelInfo('gpt-3.5-turbo', _('The latest gpt-3.5-turbo, updated with OpenAI adjustments'), ModelTypeConst.LLM, + openai_llm_model_credential, SiliconCloudChatModel + )) + .append_model_info_list(model_info_embedding_list) + .append_default_model_info(model_info_embedding_list[0]) + .append_model_info_list(model_info_tti_list) + .append_default_model_info(model_info_tti_list[0]) + .append_default_model_info(ModelInfo('whisper-1', '', + ModelTypeConst.STT, openai_stt_model_credential, + SiliconCloudSpeechToText)) + .append_model_info_list(model_rerank_list) + .append_default_model_info(model_rerank_list[0]) + .append_model_info_list(model_tts_list) + .append_default_model_info(model_tts_list[0]) + + .build() +) + + +class SiliconCloudModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_siliconCloud_provider', name='SILICONFLOW', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'siliconCloud_model_provider', + 'icon', + 'siliconCloud_icon_svg'))) diff --git a/apps/models_provider/impl/tencent_cloud_model_provider/__init__.py b/apps/models_provider/impl/tencent_cloud_model_provider/__init__.py new file mode 100644 index 000000000..2dc4ab10d --- /dev/null +++ b/apps/models_provider/impl/tencent_cloud_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/3/28 16:25 + @desc: +""" diff --git a/apps/models_provider/impl/tencent_cloud_model_provider/credential/llm.py b/apps/models_provider/impl/tencent_cloud_model_provider/credential/llm.py new file mode 100644 index 000000000..8612956f2 --- /dev/null +++ b/apps/models_provider/impl/tencent_cloud_model_provider/credential/llm.py @@ -0,0 +1,79 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:32 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class TencentCloudLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class TencentCloudLLMModelCredential(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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return TencentCloudLLMModelParams() diff --git a/apps/models_provider/impl/tencent_cloud_model_provider/icon/tencent_cloud_icon_svg b/apps/models_provider/impl/tencent_cloud_model_provider/icon/tencent_cloud_icon_svg new file mode 100644 index 000000000..ff559eaff --- /dev/null +++ b/apps/models_provider/impl/tencent_cloud_model_provider/icon/tencent_cloud_icon_svg @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/apps/models_provider/impl/tencent_cloud_model_provider/model/llm.py b/apps/models_provider/impl/tencent_cloud_model_provider/model/llm.py new file mode 100644 index 000000000..216ca8752 --- /dev/null +++ b/apps/models_provider/impl/tencent_cloud_model_provider/model/llm.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/4/18 15:28 + @desc: +""" +from typing import List, Dict + +from langchain_core.messages import BaseMessage, get_buffer_string + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class TencentCloudChatModel(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) + azure_chat_open_ai = TencentCloudChatModel( + model=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + **optional_params, + custom_get_token_ids=custom_get_token_ids + ) + return azure_chat_open_ai + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + try: + return super().get_num_tokens_from_messages(messages) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + + def get_num_tokens(self, text: str) -> int: + try: + return super().get_num_tokens(text) + except Exception as e: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) diff --git a/apps/models_provider/impl/tencent_cloud_model_provider/tencent_cloud_model_provider.py b/apps/models_provider/impl/tencent_cloud_model_provider/tencent_cloud_model_provider.py new file mode 100644 index 000000000..3a6bd9c20 --- /dev/null +++ b/apps/models_provider/impl/tencent_cloud_model_provider/tencent_cloud_model_provider.py @@ -0,0 +1,61 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: openai_model_provider.py + @date:2024/3/28 16:26 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, \ + ModelTypeConst, ModelInfoManage +from models_provider.impl.openai_model_provider.credential.embedding import OpenAIEmbeddingCredential +from models_provider.impl.openai_model_provider.credential.image import OpenAIImageModelCredential +from models_provider.impl.openai_model_provider.credential.llm import OpenAILLMModelCredential +from models_provider.impl.openai_model_provider.credential.stt import OpenAISTTModelCredential +from models_provider.impl.openai_model_provider.credential.tti import OpenAITextToImageModelCredential +from models_provider.impl.openai_model_provider.credential.tts import OpenAITTSModelCredential +from models_provider.impl.openai_model_provider.model.embedding import OpenAIEmbeddingModel +from models_provider.impl.openai_model_provider.model.image import OpenAIImage +from models_provider.impl.openai_model_provider.model.llm import OpenAIChatModel +from models_provider.impl.openai_model_provider.model.stt import OpenAISpeechToText +from models_provider.impl.openai_model_provider.model.tti import OpenAITextToImage +from models_provider.impl.openai_model_provider.model.tts import OpenAITextToSpeech +from models_provider.impl.tencent_cloud_model_provider.credential.llm import TencentCloudLLMModelCredential +from models_provider.impl.tencent_cloud_model_provider.model.llm import TencentCloudChatModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext_lazy as _ + +openai_llm_model_credential = TencentCloudLLMModelCredential() +model_info_list = [ + ModelInfo('deepseek-v3', '', ModelTypeConst.LLM, + openai_llm_model_credential, TencentCloudChatModel + ), + ModelInfo('deepseek-r1', '', ModelTypeConst.LLM, + openai_llm_model_credential, TencentCloudChatModel + ), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info( + ModelInfo('deepseek-v3', '', ModelTypeConst.LLM, + openai_llm_model_credential, TencentCloudChatModel + )) + .build() +) + + +class TencentCloudModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_tencent_cloud_provider', name=_('Tencent Cloud'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'tencent_cloud_model_provider', + 'icon', + 'tencent_cloud_icon_svg'))) diff --git a/apps/models_provider/impl/tencent_model_provider/__init__.py b/apps/models_provider/impl/tencent_model_provider/__init__.py new file mode 100644 index 000000000..8cb7f459e --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- diff --git a/apps/models_provider/impl/tencent_model_provider/credential/embedding.py b/apps/models_provider/impl/tencent_model_provider/credential/embedding.py new file mode 100644 index 000000000..1b36f7251 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/credential/embedding.py @@ -0,0 +1,41 @@ +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class TencentEmbeddingCredential(BaseForm, BaseModelCredential): + + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True) -> bool: + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + self.valid_form(model_credential) + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]) -> Dict[str, object]: + encrypted_secret_key = super().encryption(model.get('SecretKey', '')) + return {**model, 'SecretKey': encrypted_secret_key} + + SecretId = forms.PasswordInputField('SecretId', required=True) + SecretKey = forms.PasswordInputField('SecretKey', required=True) diff --git a/apps/models_provider/impl/tencent_model_provider/credential/image.py b/apps/models_provider/impl/tencent_model_provider/credential/image.py new file mode 100644 index 000000000..2d2982139 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/credential/image.py @@ -0,0 +1,78 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 18:41 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QwenModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=1.0, + _min=0.1, + _max=1.9, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class TencentVisionModelCredential(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)) + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return QwenModelParams() diff --git a/apps/models_provider/impl/tencent_model_provider/credential/llm.py b/apps/models_provider/impl/tencent_model_provider/credential/llm.py new file mode 100644 index 000000000..0357c097b --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/credential/llm.py @@ -0,0 +1,70 @@ +# coding=utf-8 +import traceback + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class TencentLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.5, + _min=0.1, + _max=2.0, + _step=0.01, + precision=2) + + +class TencentLLMModelCredential(BaseForm, BaseModelCredential): + REQUIRED_FIELDS = ['hunyuan_app_id', 'hunyuan_secret_id', 'hunyuan_secret_key'] + + @classmethod + def _validate_model_type(cls, model_type, provider, raise_exception=False): + if not any(mt['value'] == model_type for mt in provider.get_model_type_list()): + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + return False + return True + + @classmethod + def _validate_credential_fields(cls, model_credential, raise_exception=False): + missing_keys = [key for key in cls.REQUIRED_FIELDS if key not in model_credential] + if missing_keys: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('{keys} is required').format(keys=", ".join(missing_keys))) + return False + return True + + def is_valid(self, model_type, model_name, model_credential, model_params, provider, raise_exception=False): + if not (self._validate_model_type(model_type, provider, raise_exception) and + self._validate_credential_fields(model_credential, raise_exception)): + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + return False + return True + + def encryption_dict(self, model): + return {**model, 'hunyuan_secret_key': super().encryption(model.get('hunyuan_secret_key', ''))} + + hunyuan_app_id = forms.TextInputField('APP ID', required=True) + hunyuan_secret_id = forms.PasswordInputField('SecretId', required=True) + hunyuan_secret_key = forms.PasswordInputField('SecretKey', required=True) + + def get_model_params_setting_form(self, model_name): + return TencentLLMModelParams() diff --git a/apps/models_provider/impl/tencent_model_provider/credential/tti.py b/apps/models_provider/impl/tencent_model_provider/credential/tti.py new file mode 100644 index 000000000..464c06b17 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/credential/tti.py @@ -0,0 +1,116 @@ +# coding=utf-8 +import traceback + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class TencentTTIModelParams(BaseForm): + Style = forms.SingleSelect( + TooltipLabel(_('painting style'), _('If not passed, the default value is 201 (Japanese anime style)')), + required=True, + default_value='201', + option_list=[ + {'value': '000', 'label': _('Not limited to style')}, + {'value': '101', 'label': _('ink painting')}, + {'value': '102', 'label': _('concept art')}, + {'value': '103', 'label': _('Oil painting 1')}, + {'value': '118', 'label': _('Oil Painting 2 (Van Gogh)')}, + {'value': '104', 'label': _('watercolor painting')}, + {'value': '105', 'label': _('pixel art')}, + {'value': '106', 'label': _('impasto style')}, + {'value': '107', 'label': _('illustration')}, + {'value': '108', 'label': _('paper cut style')}, + {'value': '109', 'label': _('Impressionism 1 (Monet)')}, + {'value': '119', 'label': _('Impressionism 2')}, + {'value': '110', 'label': '2.5D'}, + {'value': '111', 'label': _('classical portraiture')}, + {'value': '112', 'label': _('black and white sketch')}, + {'value': '113', 'label': _('cyberpunk')}, + {'value': '114', 'label': _('science fiction style')}, + {'value': '115', 'label': _('dark style')}, + {'value': '116', 'label': '3D'}, + {'value': '117', 'label': _('vaporwave')}, + {'value': '201', 'label': _('Japanese animation')}, + {'value': '202', 'label': _('monster style')}, + {'value': '203', 'label': _('Beautiful ancient style')}, + {'value': '204', 'label': _('retro anime')}, + {'value': '301', 'label': _('Game cartoon hand drawing')}, + {'value': '401', 'label': _('Universal realistic style')}, + ], + value_field='value', + text_field='label' + ) + + Resolution = forms.SingleSelect( + TooltipLabel(_('Generate image resolution'), _('If not transmitted, the default value is 768:768.')), + required=True, + default_value='768:768', + option_list=[ + {'value': '768:768', 'label': '768:768(1:1)'}, + {'value': '768:1024', 'label': '768:1024(3:4)'}, + {'value': '1024:768', 'label': '1024:768(4:3)'}, + {'value': '1024:1024', 'label': '1024:1024(1:1)'}, + {'value': '720:1280', 'label': '720:1280(9:16)'}, + {'value': '1280:720', 'label': '1280:720(16:9)'}, + {'value': '768:1280', 'label': '768:1280(3:5)'}, + {'value': '1280:768', 'label': '1280:768(5:3)'}, + {'value': '1080:1920', 'label': '1080:1920(9:16)'}, + {'value': '1920:1080', 'label': '1920:1080(16:9)'}, + ], + value_field='value', + text_field='label' + ) + + +class TencentTTIModelCredential(BaseForm, BaseModelCredential): + REQUIRED_FIELDS = ['hunyuan_secret_id', 'hunyuan_secret_key'] + + @classmethod + def _validate_model_type(cls, model_type, provider, raise_exception=False): + if not any(mt['value'] == model_type for mt in provider.get_model_type_list()): + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('{model_type} Model type is not supported').format(model_type=model_type)) + return False + return True + + @classmethod + def _validate_credential_fields(cls, model_credential, raise_exception=False): + missing_keys = [key for key in cls.REQUIRED_FIELDS if key not in model_credential] + if missing_keys: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext('{keys} is required').format(keys=", ".join(missing_keys))) + return False + return True + + def is_valid(self, model_type, model_name, model_credential, model_params, provider, raise_exception=False): + if not (self._validate_model_type(model_type, provider, raise_exception) and + self._validate_credential_fields(model_credential, raise_exception)): + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + return False + return True + + def encryption_dict(self, model): + return {**model, 'hunyuan_secret_key': super().encryption(model.get('hunyuan_secret_key', ''))} + + hunyuan_secret_id = forms.PasswordInputField('SecretId', required=True) + hunyuan_secret_key = forms.PasswordInputField('SecretKey', required=True) + + def get_model_params_setting_form(self, model_name): + return TencentTTIModelParams() diff --git a/apps/models_provider/impl/tencent_model_provider/icon/tencent_icon_svg b/apps/models_provider/impl/tencent_model_provider/icon/tencent_icon_svg new file mode 100644 index 000000000..6cec08b74 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/icon/tencent_icon_svg @@ -0,0 +1,5 @@ + + + + diff --git a/apps/models_provider/impl/tencent_model_provider/model/embedding.py b/apps/models_provider/impl/tencent_model_provider/model/embedding.py new file mode 100644 index 000000000..6392ca3e6 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/model/embedding.py @@ -0,0 +1,41 @@ + +from typing import Dict, List + +from langchain_core.embeddings import Embeddings +from tencentcloud.common import credential +from tencentcloud.hunyuan.v20230901.hunyuan_client import HunyuanClient +from tencentcloud.hunyuan.v20230901.models import GetEmbeddingRequest + +from models_provider.base_model_provider import MaxKBBaseModel + + +class TencentEmbeddingModel(MaxKBBaseModel, Embeddings): + def embed_documents(self, texts: List[str]) -> List[List[float]]: + return [self.embed_query(text) for text in texts] + + def embed_query(self, text: str) -> List[float]: + request = GetEmbeddingRequest() + request.Input = text + res = self.client.GetEmbedding(request) + return res.Data[0].Embedding + + def __init__(self, secret_id: str, secret_key: str, model_name: str): + self.secret_id = secret_id + self.secret_key = secret_key + self.model_name = model_name + cred = credential.Credential( + secret_id, secret_key + ) + self.client = HunyuanClient(cred, "") + + @staticmethod + def new_instance(model_type: str, model_name: str, model_credential: Dict[str, str], **model_kwargs): + return TencentEmbeddingModel( + secret_id=model_credential.get('SecretId'), + secret_key=model_credential.get('SecretKey'), + model_name=model_name, + ) + + def _generate_auth_token(self): + # Example method to generate an authentication token for the model API + return f"{self.secret_id}:{self.secret_key}" diff --git a/apps/models_provider/impl/tencent_model_provider/model/hunyuan.py b/apps/models_provider/impl/tencent_model_provider/model/hunyuan.py new file mode 100644 index 000000000..9055c4cb1 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/model/hunyuan.py @@ -0,0 +1,280 @@ +import json +import logging +from typing import Any, Dict, Iterator, List, Mapping, Optional, Type + +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.language_models.chat_models import ( + BaseChatModel, + generate_from_stream, +) +from langchain_core.messages import ( + AIMessage, + AIMessageChunk, + BaseMessage, + BaseMessageChunk, + ChatMessage, + ChatMessageChunk, + HumanMessage, + HumanMessageChunk, SystemMessage, +) +from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult +from pydantic import Field, SecretStr, root_validator +from langchain_core.utils import ( + convert_to_secret_str, + get_from_dict_or_env, + get_pydantic_field_names, + pre_init, +) + +logger = logging.getLogger(__name__) + + +def _convert_message_to_dict(message: BaseMessage) -> dict: + message_dict: Dict[str, Any] + if isinstance(message, ChatMessage): + message_dict = {"Role": message.role, "Content": message.content} + elif isinstance(message, HumanMessage): + message_dict = {"Role": "user", "Content": message.content} + elif isinstance(message, AIMessage): + message_dict = {"Role": "assistant", "Content": message.content} + elif isinstance(message, SystemMessage): + message_dict = {"Role": "system", "Content": message.content} + else: + raise TypeError(f"Got unknown type {message}") + + return message_dict + + +def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: + role = _dict["Role"] + if role == "user": + return HumanMessage(content=_dict["Content"]) + elif role == "assistant": + return AIMessage(content=_dict.get("Content", "") or "") + else: + return ChatMessage(content=_dict["Content"], role=role) + + +def _convert_delta_to_message_chunk( + _dict: Mapping[str, Any], default_class: Type[BaseMessageChunk] +) -> BaseMessageChunk: + role = _dict.get("Role") + content = _dict.get("Content") or "" + + if role == "user" or default_class == HumanMessageChunk: + return HumanMessageChunk(content=content) + elif role == "assistant" or default_class == AIMessageChunk: + return AIMessageChunk(content=content) + elif role or default_class == ChatMessageChunk: + return ChatMessageChunk(content=content, role=role) # type: ignore[arg-type] + else: + return default_class(content=content) # type: ignore[call-arg] + + +def _create_chat_result(response: Mapping[str, Any]) -> ChatResult: + generations = [] + for choice in response["Choices"]: + message = _convert_dict_to_message(choice["Message"]) + generations.append(ChatGeneration(message=message)) + + token_usage = response["Usage"] + llm_output = {"token_usage": token_usage} + return ChatResult(generations=generations, llm_output=llm_output) + + +class ChatHunyuan(BaseChatModel): + """Tencent Hunyuan chat models API by Tencent. + + For more information, see https://cloud.tencent.com/document/product/1729 + """ + + @property + def lc_secrets(self) -> Dict[str, str]: + return { + "hunyuan_app_id": "HUNYUAN_APP_ID", + "hunyuan_secret_id": "HUNYUAN_SECRET_ID", + "hunyuan_secret_key": "HUNYUAN_SECRET_KEY", + } + + @property + def lc_serializable(self) -> bool: + return True + + hunyuan_app_id: Optional[int] = None + """Hunyuan App ID""" + hunyuan_secret_id: Optional[str] = None + """Hunyuan Secret ID""" + hunyuan_secret_key: Optional[SecretStr] = None + """Hunyuan Secret Key""" + streaming: bool = False + """Whether to stream the results or not.""" + request_timeout: int = 60 + """Timeout for requests to Hunyuan API. Default is 60 seconds.""" + temperature: float = 1.0 + """What sampling temperature to use.""" + top_p: float = 1.0 + """What probability mass to use.""" + model: str = "hunyuan-lite" + """What Model to use. + Optional model: + - hunyuan-lite、 + - hunyuan-standard + - hunyuan-standard-256K + - hunyuan-pro + - hunyuan-code + - hunyuan-role + - hunyuan-functioncall + - hunyuan-vision + """ + stream_moderation: bool = False + """Whether to review the results or not when streaming is true.""" + enable_enhancement: bool = True + """Whether to enhancement the results or not.""" + + model_kwargs: Dict[str, Any] = Field(default_factory=dict) + """Holds any model parameters valid for API call not explicitly specified.""" + + class Config: + """Configuration for this pydantic object.""" + + validate_by_name = True + + @root_validator(pre=True) + def build_extra(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """Build extra kwargs from additional params that were passed in.""" + all_required_field_names = get_pydantic_field_names(cls) + extra = values.get("model_kwargs", {}) + for field_name in list(values): + if field_name in extra: + raise ValueError(f"Found {field_name} supplied twice.") + if field_name not in all_required_field_names: + logger.warning( + f"""WARNING! {field_name} is not default parameter. + {field_name} was transferred to model_kwargs. + Please confirm that {field_name} is what you intended.""" + ) + extra[field_name] = values.pop(field_name) + + invalid_model_kwargs = all_required_field_names.intersection(extra.keys()) + if invalid_model_kwargs: + raise ValueError( + f"Parameters {invalid_model_kwargs} should be specified explicitly. " + f"Instead they were passed in as part of `model_kwargs` parameter." + ) + + values["model_kwargs"] = extra + return values + + @pre_init + def validate_environment(cls, values: Dict) -> Dict: + values["hunyuan_app_id"] = get_from_dict_or_env( + values, + "hunyuan_app_id", + "HUNYUAN_APP_ID", + ) + values["hunyuan_secret_id"] = get_from_dict_or_env( + values, + "hunyuan_secret_id", + "HUNYUAN_SECRET_ID", + ) + values["hunyuan_secret_key"] = convert_to_secret_str( + get_from_dict_or_env( + values, + "hunyuan_secret_key", + "HUNYUAN_SECRET_KEY", + ) + ) + return values + + @property + def _default_params(self) -> Dict[str, Any]: + """Get the default parameters for calling Hunyuan API.""" + normal_params = { + "Temperature": self.temperature, + "TopP": self.top_p, + "Model": self.model, + "Stream": self.streaming, + "StreamModeration": self.stream_moderation, + "EnableEnhancement": self.enable_enhancement, + } + return {**normal_params, **self.model_kwargs} + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + if self.streaming: + stream_iter = self._stream( + messages=messages, stop=stop, run_manager=run_manager, **kwargs + ) + return generate_from_stream(stream_iter) + + res = self._chat(messages, **kwargs) + return _create_chat_result(json.loads(res.to_json_string())) + + usage_metadata: dict = {} + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + res = self._chat(messages, **kwargs) + + default_chunk_class = AIMessageChunk + for chunk in res: + chunk = chunk.get("data", "") + if len(chunk) == 0: + continue + response = json.loads(chunk) + if "error" in response: + raise ValueError(f"Error from Hunyuan api response: {response}") + + for choice in response["Choices"]: + chunk = _convert_delta_to_message_chunk( + choice["Delta"], default_chunk_class + ) + default_chunk_class = chunk.__class__ + # FinishReason === stop + if choice.get("FinishReason") == "stop": + self.usage_metadata = response.get("Usage", {}) + cg_chunk = ChatGenerationChunk(message=chunk) + if run_manager: + run_manager.on_llm_new_token(chunk.content, chunk=cg_chunk) + yield cg_chunk + + def _chat(self, messages: List[BaseMessage], **kwargs: Any) -> Any: + if self.hunyuan_secret_key is None: + raise ValueError("Hunyuan secret key is not set.") + + try: + from tencentcloud.common import credential + from tencentcloud.hunyuan.v20230901 import hunyuan_client, models + except ImportError: + raise ImportError( + "Could not import tencentcloud python package. " + "Please install it with `pip install tencentcloud-sdk-python`." + ) + + parameters = {**self._default_params, **kwargs} + cred = credential.Credential( + self.hunyuan_secret_id, str(self.hunyuan_secret_key.get_secret_value()) + ) + client = hunyuan_client.HunyuanClient(cred, "") + req = models.ChatCompletionsRequest() + params = { + "Messages": [_convert_message_to_dict(m) for m in messages], + **parameters, + } + req.from_json_string(json.dumps(params)) + resp = client.ChatCompletions(req) + return resp + + @property + def _llm_type(self) -> str: + return "hunyuan-chat" diff --git a/apps/models_provider/impl/tencent_model_provider/model/image.py b/apps/models_provider/impl/tencent_model_provider/model/image.py new file mode 100644 index 000000000..17b2d4cee --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/model/image.py @@ -0,0 +1,20 @@ +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class TencentVision(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return TencentVision( + model_name=model_name, + openai_api_base='https://api.hunyuan.cloud.tencent.com/v1', + openai_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/tencent_model_provider/model/llm.py b/apps/models_provider/impl/tencent_model_provider/model/llm.py new file mode 100644 index 000000000..e86edd54b --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/model/llm.py @@ -0,0 +1,45 @@ +# coding=utf-8 + +from typing import List, Dict, Optional, Any + +from langchain_core.messages import BaseMessage + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.tencent_model_provider.model.hunyuan import ChatHunyuan + + +class TencentModel(MaxKBBaseModel, ChatHunyuan): + @staticmethod + def is_cache_model(): + return False + + def __init__(self, model_name: str, credentials: Dict[str, str], streaming: bool = False, **kwargs): + hunyuan_app_id = credentials.get('hunyuan_app_id') + hunyuan_secret_id = credentials.get('hunyuan_secret_id') + hunyuan_secret_key = credentials.get('hunyuan_secret_key') + + optional_params = MaxKBBaseModel.filter_optional_params(kwargs) + + if not all([hunyuan_app_id, hunyuan_secret_id, hunyuan_secret_key]): + raise ValueError( + "All of 'hunyuan_app_id', 'hunyuan_secret_id', and 'hunyuan_secret_key' must be provided in credentials.") + + super().__init__(model=model_name, hunyuan_app_id=hunyuan_app_id, hunyuan_secret_id=hunyuan_secret_id, + hunyuan_secret_key=hunyuan_secret_key, streaming=streaming, + temperature=optional_params.get('temperature', 1.0) + ) + + @staticmethod + def new_instance(model_type: str, model_name: str, model_credential: Dict[str, object], + **model_kwargs) -> 'TencentModel': + streaming = model_kwargs.pop('streaming', False) + return TencentModel(model_name=model_name, credentials=model_credential, streaming=streaming, **model_kwargs) + + def get_last_generation_info(self) -> Optional[Dict[str, Any]]: + return self.usage_metadata + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + return self.usage_metadata.get('PromptTokens', 0) + + def get_num_tokens(self, text: str) -> int: + return self.usage_metadata.get('CompletionTokens', 0) diff --git a/apps/models_provider/impl/tencent_model_provider/model/tti.py b/apps/models_provider/impl/tencent_model_provider/model/tti.py new file mode 100644 index 000000000..d737f5f85 --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/model/tti.py @@ -0,0 +1,92 @@ +# coding=utf-8 + +import json +from typing import Dict + +from django.utils.translation import gettext as _ +from tencentcloud.common import credential +from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException +from tencentcloud.common.profile.client_profile import ClientProfile +from tencentcloud.common.profile.http_profile import HttpProfile +from tencentcloud.hunyuan.v20230901 import hunyuan_client, models + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage +from models_provider.impl.tencent_model_provider.model.hunyuan import ChatHunyuan + + +class TencentTextToImageModel(MaxKBBaseModel, BaseTextToImage): + hunyuan_secret_id: str + hunyuan_secret_key: str + model: str + params: dict + + @staticmethod + def is_cache_model(): + return False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.hunyuan_secret_id = kwargs.get('hunyuan_secret_id') + self.hunyuan_secret_key = kwargs.get('hunyuan_secret_key') + self.model = kwargs.get('model_name') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type: str, model_name: str, model_credential: Dict[str, object], + **model_kwargs) -> 'TencentTextToImageModel': + optional_params = {'params': {'Style': '201', 'Resolution': '768:768'}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return TencentTextToImageModel( + model=model_name, + hunyuan_secret_id=model_credential.get('hunyuan_secret_id'), + hunyuan_secret_key=model_credential.get('hunyuan_secret_key'), + **optional_params + ) + + def check_auth(self): + chat = ChatHunyuan(hunyuan_app_id='111111', + hunyuan_secret_id=self.hunyuan_secret_id, + hunyuan_secret_key=self.hunyuan_secret_key, + model="hunyuan-standard") + res = chat.invoke(_('Hello')) + # print(res) + + def generate_image(self, prompt: str, negative_prompt: str = None): + try: + # 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 + # 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 + # 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + cred = credential.Credential(self.hunyuan_secret_id, self.hunyuan_secret_key) + # 实例化一个http选项,可选的,没有特殊需求可以跳过 + httpProfile = HttpProfile() + httpProfile.endpoint = "hunyuan.tencentcloudapi.com" + + # 实例化一个client选项,可选的,没有特殊需求可以跳过 + clientProfile = ClientProfile() + clientProfile.httpProfile = httpProfile + # 实例化要请求产品的client对象,clientProfile是可选的 + client = hunyuan_client.HunyuanClient(cred, "ap-guangzhou", clientProfile) + + # 实例化一个请求对象,每个接口都会对应一个request对象 + req = models.TextToImageLiteRequest() + params = { + "Prompt": prompt, + "NegativePrompt": negative_prompt, + "RspImgType": "url", + **self.params + } + req.from_json_string(json.dumps(params)) + + # 返回的resp是一个TextToImageLiteResponse的实例,与请求对象对应 + resp = client.TextToImageLite(req) + # 输出json格式的字符串回包 + print(resp.to_json_string()) + file_urls = [] + + file_urls.append(resp.ResultImage) + return file_urls + except TencentCloudSDKException as err: + print(err) diff --git a/apps/models_provider/impl/tencent_model_provider/tencent_model_provider.py b/apps/models_provider/impl/tencent_model_provider/tencent_model_provider.py new file mode 100644 index 000000000..5e1c2714e --- /dev/null +++ b/apps/models_provider/impl/tencent_model_provider/tencent_model_provider.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +import os +from common.utils.common import get_file_content +from models_provider.base_model_provider import ( + IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, ModelInfoManage +) +from models_provider.impl.tencent_model_provider.credential.embedding import TencentEmbeddingCredential +from models_provider.impl.tencent_model_provider.credential.image import TencentVisionModelCredential +from models_provider.impl.tencent_model_provider.credential.llm import TencentLLMModelCredential +from models_provider.impl.tencent_model_provider.credential.tti import TencentTTIModelCredential +from models_provider.impl.tencent_model_provider.model.embedding import TencentEmbeddingModel +from models_provider.impl.tencent_model_provider.model.image import TencentVision +from models_provider.impl.tencent_model_provider.model.llm import TencentModel +from models_provider.impl.tencent_model_provider.model.tti import TencentTextToImageModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +def _create_model_info(model_name, description, model_type, credential_class, model_class): + return ModelInfo( + name=model_name, + desc=description, + model_type=model_type, + model_credential=credential_class(), + model_class=model_class + ) + + +def _get_tencent_icon_path(): + return os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'tencent_model_provider', + 'icon', 'tencent_icon_svg') + + +def _initialize_model_info(): + model_info_list = [_create_model_info( + 'hunyuan-pro', + _('The most effective version of the current hybrid model, the trillion-level parameter scale MOE-32K long article model. Reaching the absolute leading level on various benchmarks, with complex instructions and reasoning, complex mathematical capabilities, support for function call, and application focus optimization in fields such as multi-language translation, finance, law, and medical care'), + ModelTypeConst.LLM, + TencentLLMModelCredential, + TencentModel + ), + _create_model_info( + 'hunyuan-standard', + _('A better routing strategy is adopted to simultaneously alleviate the problems of load balancing and expert convergence. For long articles, the needle-in-a-haystack index reaches 99.9%'), + ModelTypeConst.LLM, + TencentLLMModelCredential, + TencentModel), + _create_model_info( + 'hunyuan-lite', + _('Upgraded to MOE structure, the context window is 256k, leading many open source models in multiple evaluation sets such as NLP, code, mathematics, industry, etc.'), + ModelTypeConst.LLM, + TencentLLMModelCredential, + TencentModel), + _create_model_info( + 'hunyuan-role', + _("Hunyuan's latest version of the role-playing model, a role-playing model launched by Hunyuan's official fine-tuning training, is based on the Hunyuan model combined with the role-playing scene data set for additional training, and has better basic effects in role-playing scenes."), + ModelTypeConst.LLM, + TencentLLMModelCredential, + TencentModel), + _create_model_info( + 'hunyuan-functioncall', + _("Hunyuan's latest MOE architecture FunctionCall model has been trained with high-quality FunctionCall data and has a context window of 32K, leading in multiple dimensions of evaluation indicators."), + ModelTypeConst.LLM, + TencentLLMModelCredential, + TencentModel), + _create_model_info( + 'hunyuan-code', + _("Hunyuan's latest code generation model, after training the base model with 200B high-quality code data, and iterating on high-quality SFT data for half a year, the context long window length has been increased to 8K, and it ranks among the top in the automatic evaluation indicators of code generation in the five major languages; the five major languages In the manual high-quality evaluation of 10 comprehensive code tasks that consider all aspects, the performance is in the first echelon."), + ModelTypeConst.LLM, + TencentLLMModelCredential, + TencentModel), + ] + + tencent_embedding_model_info = _create_model_info( + 'hunyuan-embedding', + _("Tencent's Hunyuan Embedding interface can convert text into high-quality vector data. The vector dimension is 1024 dimensions."), + ModelTypeConst.EMBEDDING, + TencentEmbeddingCredential, + TencentEmbeddingModel + ) + + model_info_embedding_list = [tencent_embedding_model_info] + + model_info_vision_list = [_create_model_info( + 'hunyuan-vision', + _('Mixed element visual model'), + ModelTypeConst.IMAGE, + TencentVisionModelCredential, + TencentVision)] + + model_info_tti_list = [_create_model_info( + 'hunyuan-dit', + _('Hunyuan graph model'), + ModelTypeConst.TTI, + TencentTTIModelCredential, + TencentTextToImageModel)] + + model_info_manage = ModelInfoManage.builder() \ + .append_model_info_list(model_info_list) \ + .append_model_info_list(model_info_embedding_list) \ + .append_model_info_list(model_info_vision_list) \ + .append_default_model_info(model_info_vision_list[0]) \ + .append_model_info_list(model_info_tti_list) \ + .append_default_model_info(model_info_tti_list[0]) \ + .append_default_model_info(model_info_list[0]) \ + .append_default_model_info(tencent_embedding_model_info) \ + .build() + + return model_info_manage + + +class TencentModelProvider(IModelProvider): + def __init__(self): + self._model_info_manage = _initialize_model_info() + + def get_model_info_manage(self): + return self._model_info_manage + + def get_model_provide_info(self): + icon_path = _get_tencent_icon_path() + icon_data = get_file_content(icon_path) + return ModelProvideInfo( + provider='model_tencent_provider', + name=_('Tencent Hunyuan'), + icon=icon_data + ) diff --git a/apps/models_provider/impl/vllm_model_provider/__init__.py b/apps/models_provider/impl/vllm_model_provider/__init__.py new file mode 100644 index 000000000..9bad5790a --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/apps/models_provider/impl/vllm_model_provider/credential/embedding.py b/apps/models_provider/impl/vllm_model_provider/credential/embedding.py new file mode 100644 index 000000000..9ba9967d6 --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/credential/embedding.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 16:45 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VllmEmbeddingCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True): + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/vllm_model_provider/credential/image.py b/apps/models_provider/impl/vllm_model_provider/credential/image.py new file mode 100644 index 000000000..4ec92ca20 --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/credential/image.py @@ -0,0 +1,72 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VllmImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class VllmImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": "你好"}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return VllmImageModelParams() diff --git a/apps/models_provider/impl/vllm_model_provider/credential/llm.py b/apps/models_provider/impl/vllm_model_provider/credential/llm.py new file mode 100644 index 000000000..02b1b9a67 --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/credential/llm.py @@ -0,0 +1,73 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class VLLMModelCredential(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)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base'), model_credential.get('api_key')) + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, gettext('API domain name is invalid')) + exist = provider.get_model_info_by_name(model_list, model_name) + if len(exist) == 0: + raise AppApiException(ValidCode.valid_error.value, + gettext('The model does not exist, please download the model first')) + model = provider.get_model(model_type, model_name, model_credential, **model_params) + try: + res = model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + return True + + def encryption_dict(self, model_info: Dict[str, object]): + 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', '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') + return self + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return VLLMModelParams() diff --git a/apps/models_provider/impl/vllm_model_provider/icon/vllm_icon_svg b/apps/models_provider/impl/vllm_model_provider/icon/vllm_icon_svg new file mode 100644 index 000000000..1ad7d0a6d --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/icon/vllm_icon_svg @@ -0,0 +1,5 @@ + + + + diff --git a/apps/models_provider/impl/vllm_model_provider/model/embedding.py b/apps/models_provider/impl/vllm_model_provider/model/embedding.py new file mode 100644 index 000000000..d6709a0fe --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/model/embedding.py @@ -0,0 +1,23 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 17:44 + @desc: +""" +from typing import Dict + +from langchain_community.embeddings import OpenAIEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class VllmEmbeddingModel(MaxKBBaseModel, OpenAIEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return VllmEmbeddingModel( + model=model_name, + openai_api_key=model_credential.get('api_key'), + openai_api_base=model_credential.get('api_base'), + ) diff --git a/apps/models_provider/impl/vllm_model_provider/model/image.py b/apps/models_provider/impl/vllm_model_provider/model/image.py new file mode 100644 index 000000000..450567bf7 --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/model/image.py @@ -0,0 +1,38 @@ +from typing import Dict, List + +from langchain_core.messages import get_buffer_string, BaseMessage + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class VllmImage(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return VllmImage( + model_name=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) + + def is_cache_model(self): + return False + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + return self.usage_metadata.get('input_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) + return self.get_last_generation_info().get('output_tokens', 0) diff --git a/apps/models_provider/impl/vllm_model_provider/model/llm.py b/apps/models_provider/impl/vllm_model_provider/model/llm.py new file mode 100644 index 000000000..5367aa56c --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/model/llm.py @@ -0,0 +1,50 @@ +# coding=utf-8 + +from typing import Dict, List +from urllib.parse import urlparse, ParseResult + +from langchain_core.messages import BaseMessage, get_buffer_string + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +class VllmChatModel(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) + vllm_chat_open_ai = VllmChatModel( + model=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + **optional_params, + streaming=True, + stream_usage=True, + ) + return vllm_chat_open_ai + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + return self.usage_metadata.get('input_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) + return self.get_last_generation_info().get('output_tokens', 0) diff --git a/apps/models_provider/impl/vllm_model_provider/vllm_model_provider.py b/apps/models_provider/impl/vllm_model_provider/vllm_model_provider.py new file mode 100644 index 000000000..6206817f4 --- /dev/null +++ b/apps/models_provider/impl/vllm_model_provider/vllm_model_provider.py @@ -0,0 +1,84 @@ +# coding=utf-8 +import os +from urllib.parse import urlparse, ParseResult + +import requests + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, \ + ModelInfoManage +from models_provider.impl.vllm_model_provider.credential.embedding import VllmEmbeddingCredential +from models_provider.impl.vllm_model_provider.credential.image import VllmImageModelCredential +from models_provider.impl.vllm_model_provider.credential.llm import VLLMModelCredential +from models_provider.impl.vllm_model_provider.model.embedding import VllmEmbeddingModel +from models_provider.impl.vllm_model_provider.model.image import VllmImage +from models_provider.impl.vllm_model_provider.model.llm import VllmChatModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +v_llm_model_credential = VLLMModelCredential() +image_model_credential = VllmImageModelCredential() +embedding_model_credential = VllmEmbeddingCredential() + +model_info_list = [ + ModelInfo('facebook/opt-125m', _('Facebook’s 125M parameter model'), ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel), + ModelInfo('BAAI/Aquila-7B', _('BAAI’s 7B parameter model'), ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel), + ModelInfo('BAAI/AquilaChat-7B', _('BAAI’s 13B parameter mode'), ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel), + +] + +image_model_info_list = [ + ModelInfo('Qwen/Qwen2-VL-2B-Instruct', '', ModelTypeConst.IMAGE, image_model_credential, VllmImage), +] + +embedding_model_info_list = [ + ModelInfo('HIT-TMG/KaLM-embedding-multilingual-mini-instruct-v1.5', '', ModelTypeConst.EMBEDDING, embedding_model_credential, VllmEmbeddingModel), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info(ModelInfo('facebook/opt-125m', + _('Facebook’s 125M parameter model'), + ModelTypeConst.LLM, v_llm_model_credential, VllmChatModel)) + .append_model_info_list(image_model_info_list) + .append_default_model_info(image_model_info_list[0]) + .append_model_info_list(embedding_model_info_list) + .append_default_model_info(embedding_model_info_list[0]) + .build() +) + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +class VllmModelProvider(IModelProvider): + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_vllm_provider', name='vLLM', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'vllm_model_provider', 'icon', + 'vllm_icon_svg'))) + + @staticmethod + def get_base_model_list(api_base, api_key): + base_url = get_base_url(api_base) + base_url = base_url if base_url.endswith('/v1') else (base_url + '/v1') + headers = {} + if api_key: + headers['Authorization'] = f"Bearer {api_key}" + r = requests.request(method="GET", url=f"{base_url}/models", headers=headers, timeout=5) + r.raise_for_status() + return r.json().get('data') + + @staticmethod + def get_model_info_by_name(model_list, model_name): + if model_list is None: + return [] + return [model for model in model_list if model.get('id') == model_name] diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/__init__.py b/apps/models_provider/impl/volcanic_engine_model_provider/__init__.py new file mode 100644 index 000000000..8cb7f459e --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/credential/embedding.py b/apps/models_provider/impl/volcanic_engine_model_provider/credential/embedding.py new file mode 100644 index 000000000..e2950940c --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/credential/embedding.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/7/12 16:45 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VolcanicEmbeddingCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True): + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/credential/image.py b/apps/models_provider/impl/volcanic_engine_model_provider/credential/image.py new file mode 100644 index 000000000..b5c410ef5 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/credential/image.py @@ -0,0 +1,72 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VolcanicEngineImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.95, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class VolcanicEngineImageModelCredential(BaseForm, BaseModelCredential): + api_key = forms.PasswordInputField('API Key', required=True) + api_base = forms.TextInputField('API URL', required=True) + + 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)) + + for key in ['api_key', 'api_base']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return VolcanicEngineImageModelParams() diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/credential/llm.py b/apps/models_provider/impl/volcanic_engine_model_provider/credential/llm.py new file mode 100644 index 000000000..70c373a35 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/credential/llm.py @@ -0,0 +1,79 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/11 17:57 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VolcanicEngineLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.3, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class VolcanicEngineLLMModelCredential(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)) + + for key in ['access_key_id', 'secret_access_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.invoke([HumanMessage(content=gettext('Hello'))]) + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'access_key_id': super().encryption(model.get('access_key_id', ''))} + + access_key_id = forms.PasswordInputField('Access Key ID', required=True) + secret_access_key = forms.PasswordInputField('Secret Access Key', required=True) + + def get_model_params_setting_form(self, model_name): + return VolcanicEngineLLMModelParams() diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/credential/stt.py b/apps/models_provider/impl/volcanic_engine_model_provider/credential/stt.py new file mode 100644 index 000000000..f7e9ecc87 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/credential/stt.py @@ -0,0 +1,52 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VolcanicEngineSTTModelCredential(BaseForm, BaseModelCredential): + volcanic_api_url = forms.TextInputField('API URL', required=True, + default_value='wss://openspeech.bytedance.com/api/v2/asr') + volcanic_app_id = forms.TextInputField('App ID', required=True) + volcanic_token = forms.PasswordInputField('Access Token', required=True) + volcanic_cluster = forms.TextInputField('Cluster ID', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['volcanic_api_url', 'volcanic_app_id', 'volcanic_token', 'volcanic_cluster']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'volcanic_token': super().encryption(model.get('volcanic_token', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/credential/tti.py b/apps/models_provider/impl/volcanic_engine_model_provider/credential/tti.py new file mode 100644 index 000000000..af9bea144 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/credential/tti.py @@ -0,0 +1,68 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VolcanicEngineTTIModelGeneralParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), + _('If the gap between width, height and 512 is too large, the picture rendering effect will be poor and the probability of excessive delay will increase significantly. Recommended ratio and corresponding width and height before super score: width*height')), + required=True, + default_value='512*512', + option_list=[ + {'value': '512*512', 'label': '512*512'}, + {'value': '512*384', 'label': '512*384'}, + {'value': '384*512', 'label': '384*512'}, + {'value': '512*341', 'label': '512*341'}, + {'value': '341*512', 'label': '341*512'}, + {'value': '512*288', 'label': '512*288'}, + {'value': '288*512', 'label': '288*512'}, + ], + text_field='label', + value_field='value') + + +class VolcanicEngineTTIModelCredential(BaseForm, BaseModelCredential): + access_key = forms.PasswordInputField('Access Key ID', required=True) + secret_key = forms.PasswordInputField('Secret Access Key', required=True) + + 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)) + + for key in ['access_key', 'secret_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'secret_key': super().encryption(model.get('secret_key', ''))} + + def get_model_params_setting_form(self, model_name): + return VolcanicEngineTTIModelGeneralParams() diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/credential/tts.py b/apps/models_provider/impl/volcanic_engine_model_provider/credential/tts.py new file mode 100644 index 000000000..2bef1211d --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/credential/tts.py @@ -0,0 +1,78 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class VolcanicEngineTTSModelGeneralParams(BaseForm): + voice_type = forms.SingleSelect( + TooltipLabel(_('timbre'), _('Chinese sounds can support mixed scenes of Chinese and English')), + required=True, default_value='zh_female_cancan_mars_bigtts', + text_field='value', + value_field='value', + option_list=[ + {'text': '灿灿/Shiny', 'value': 'zh_female_cancan_mars_bigtts'}, + {'text': '清新女声', 'value': 'zh_female_qingxinnvsheng_mars_bigtts'}, + {'text': '爽快思思/Skye', 'value': 'zh_female_shuangkuaisisi_moon_bigtts'}, + {'text': '湾区大叔', 'value': 'zh_female_wanqudashu_moon_bigtts' }, + {'text': '呆萌川妹', 'value': 'zh_female_daimengchuanmei_moon_bigtts'}, + {'text': '广州德哥', 'value': 'zh_male_guozhoudege_moon_bigtts'}, + {'text': '北京小爷', 'value': 'zh_male_beijingxiaoye_moon_bigtts'}, + {'text': '少年梓辛/Brayan', 'value': 'zh_male_shaonianzixin_moon_bigtts'}, + {'text': '魅力女友', 'value': 'zh_female_meilinvyou_moon_bigtts'}, + ]) + speed_ratio = forms.SliderField( + TooltipLabel(_('speaking speed'), _('[0.2,3], the default is 1, usually one decimal place is enough')), + required=True, default_value=1, + _min=0.2, + _max=3, + _step=0.1, + precision=1) + + +class VolcanicEngineTTSModelCredential(BaseForm, BaseModelCredential): + volcanic_api_url = forms.TextInputField('API URL', required=True, + default_value='wss://openspeech.bytedance.com/api/v1/tts/ws_binary') + volcanic_app_id = forms.TextInputField('App ID', required=True) + volcanic_token = forms.PasswordInputField('Access Token', required=True) + volcanic_cluster = forms.TextInputField('Cluster ID', required=True) + + 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)) + + for key in ['volcanic_api_url', 'volcanic_app_id', 'volcanic_token', 'volcanic_cluster']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'volcanic_token': super().encryption(model.get('volcanic_token', ''))} + + def get_model_params_setting_form(self, model_name): + return VolcanicEngineTTSModelGeneralParams() diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/icon/volcanic_engine_icon_svg b/apps/models_provider/impl/volcanic_engine_model_provider/icon/volcanic_engine_icon_svg new file mode 100644 index 000000000..05a1279ef --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/icon/volcanic_engine_icon_svg @@ -0,0 +1,5 @@ + + + + diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/model/embedding.py b/apps/models_provider/impl/volcanic_engine_model_provider/model/embedding.py new file mode 100644 index 000000000..38c31185c --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/model/embedding.py @@ -0,0 +1,16 @@ +from typing import Dict + +from langchain_openai import OpenAIEmbeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class VolcanicEngineEmbeddingModel(MaxKBBaseModel, OpenAIEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return VolcanicEngineEmbeddingModel( + openai_api_key=model_credential.get('api_key'), + model=model_name, + openai_api_base=model_credential.get('api_base'), + check_embedding_ctx_length=False, + ) diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/model/iat_mp3_16k.mp3 b/apps/models_provider/impl/volcanic_engine_model_provider/model/iat_mp3_16k.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..75e744c8ff5188208a3797561efb2e096d4fa015 GIT binary patch literal 17876 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 4 + header_size = res[0] & 0x0f + message_type = res[1] >> 4 + message_type_specific_flags = res[1] & 0x0f + serialization_method = res[2] >> 4 + message_compression = res[2] & 0x0f + reserved = res[3] + header_extensions = res[4:header_size * 4] + payload = res[header_size * 4:] + result = {} + payload_msg = None + payload_size = 0 + if message_type == SERVER_FULL_RESPONSE: + payload_size = int.from_bytes(payload[:4], "big", signed=True) + payload_msg = payload[4:] + elif message_type == SERVER_ACK: + seq = int.from_bytes(payload[:4], "big", signed=True) + result['seq'] = seq + if len(payload) >= 8: + payload_size = int.from_bytes(payload[4:8], "big", signed=False) + payload_msg = payload[8:] + elif message_type == SERVER_ERROR_RESPONSE: + code = int.from_bytes(payload[:4], "big", signed=False) + result['code'] = code + payload_size = int.from_bytes(payload[4:8], "big", signed=False) + payload_msg = payload[8:] + print(f"Error code: {code}, message: {payload_msg}") + if payload_msg is None: + return result + if message_compression == GZIP: + payload_msg = gzip.decompress(payload_msg) + if serialization_method == JSON: + payload_msg = json.loads(str(payload_msg, "utf-8")) + elif serialization_method != NO_SERIALIZATION: + payload_msg = str(payload_msg, "utf-8") + result['payload_msg'] = payload_msg + result['payload_size'] = payload_size + return result + + +def read_wav_info(data: bytes = None) -> (int, int, int, int, int): + with BytesIO(data) as _f: + wave_fp = wave.open(_f, 'rb') + nchannels, sampwidth, framerate, nframes = wave_fp.getparams()[:4] + wave_bytes = wave_fp.readframes(nframes) + return nchannels, sampwidth, framerate, nframes, len(wave_bytes) + + +class VolcanicEngineSpeechToText(MaxKBBaseModel, BaseSpeechToText): + workflow: str = "audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate" + show_language: bool = False + show_utterances: bool = False + result_type: str = "full" + format: str = "mp3" + rate: int = 16000 + language: str = "zh-CN" + bits: int = 16 + channel: int = 1 + codec: str = "raw" + audio_type: int = 1 + secret: str = "access_secret" + auth_method: str = "token" + mp3_seg_size: int = 10000 + success_code: int = 1000 # success code, default is 1000 + seg_duration: int = 15000 + nbest: int = 1 + + volcanic_app_id: str + volcanic_cluster: str + volcanic_api_url: str + volcanic_token: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.volcanic_api_url = kwargs.get('volcanic_api_url') + self.volcanic_token = kwargs.get('volcanic_token') + self.volcanic_app_id = kwargs.get('volcanic_app_id') + self.volcanic_cluster = kwargs.get('volcanic_cluster') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return VolcanicEngineSpeechToText( + volcanic_api_url=model_credential.get('volcanic_api_url'), + volcanic_token=model_credential.get('volcanic_token'), + volcanic_app_id=model_credential.get('volcanic_app_id'), + volcanic_cluster=model_credential.get('volcanic_cluster'), + **optional_params + ) + + def construct_request(self, reqid): + req = { + 'app': { + 'appid': self.volcanic_app_id, + 'cluster': self.volcanic_cluster, + 'token': self.volcanic_token, + }, + 'user': { + 'uid': 'uid' + }, + 'request': { + 'reqid': reqid, + 'nbest': self.nbest, + 'workflow': self.workflow, + 'show_language': self.show_language, + 'show_utterances': self.show_utterances, + 'result_type': self.result_type, + "sequence": 1 + }, + 'audio': { + 'format': self.format, + 'rate': self.rate, + 'language': self.language, + 'bits': self.bits, + 'channel': self.channel, + 'codec': self.codec + } + } + return req + + @staticmethod + def slice_data(data: bytes, chunk_size: int) -> (list, bool): + """ + slice data + :param data: wav data + :param chunk_size: the segment size in one request + :return: segment data, last flag + """ + data_len = len(data) + offset = 0 + while offset + chunk_size < data_len: + yield data[offset: offset + chunk_size], False + offset += chunk_size + else: + yield data[offset: data_len], True + + def _real_processor(self, request_params: dict) -> dict: + pass + + def token_auth(self): + return {'Authorization': 'Bearer; {}'.format(self.volcanic_token)} + + def signature_auth(self, data): + header_dicts = { + 'Custom': 'auth_custom', + } + + url_parse = urlparse(self.volcanic_api_url) + input_str = 'GET {} HTTP/1.1\n'.format(url_parse.path) + auth_headers = 'Custom' + for header in auth_headers.split(','): + input_str += '{}\n'.format(header_dicts[header]) + input_data = bytearray(input_str, 'utf-8') + input_data += data + mac = base64.urlsafe_b64encode( + hmac.new(self.secret.encode('utf-8'), input_data, digestmod=sha256).digest()) + header_dicts['Authorization'] = 'HMAC256; access_token="{}"; mac="{}"; h="{}"'.format(self.volcanic_token, + str(mac, 'utf-8'), + auth_headers) + return header_dicts + + async def segment_data_processor(self, wav_data: bytes, segment_size: int): + reqid = str(uuid.uuid4()) + # 构建 full client request,并序列化压缩 + request_params = self.construct_request(reqid) + payload_bytes = str.encode(json.dumps(request_params)) + payload_bytes = gzip.compress(payload_bytes) + full_client_request = bytearray(generate_full_default_header()) + full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + full_client_request.extend(payload_bytes) # payload + header = None + if self.auth_method == "token": + header = self.token_auth() + elif self.auth_method == "signature": + header = self.signature_auth(full_client_request) + async with websockets.connect(self.volcanic_api_url, extra_headers=header, max_size=1000000000, + ssl=ssl_context) as ws: + # 发送 full client request + await ws.send(full_client_request) + res = await ws.recv() + result = parse_response(res) + if 'payload_msg' in result and result['payload_msg']['code'] != self.success_code: + raise Exception( + f"Error code: {result['payload_msg']['code']}, message: {result['payload_msg']['message']}") + for seq, (chunk, last) in enumerate(VolcanicEngineSpeechToText.slice_data(wav_data, segment_size), 1): + # if no compression, comment this line + payload_bytes = gzip.compress(chunk) + audio_only_request = bytearray(generate_audio_default_header()) + if last: + audio_only_request = bytearray(generate_last_audio_default_header()) + audio_only_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + audio_only_request.extend(payload_bytes) # payload + # 发送 audio-only client request + await ws.send(audio_only_request) + res = await ws.recv() + result = parse_response(res) + if 'payload_msg' in result and result['payload_msg']['code'] != self.success_code: + return result + return result['payload_msg']['result'][0]['text'] + + def check_auth(self): + cwd = os.path.dirname(os.path.abspath(__file__)) + with open(f'{cwd}/iat_mp3_16k.mp3', 'rb') as f: + self.speech_to_text(f) + + def speech_to_text(self, file): + data = file.read() + audio_data = bytes(data) + if self.format == "mp3": + segment_size = self.mp3_seg_size + return asyncio.run(self.segment_data_processor(audio_data, segment_size)) + if self.format != "wav": + raise Exception("format should in wav or mp3") + nchannels, sampwidth, framerate, nframes, wav_len = read_wav_info( + audio_data) + size_per_sec = nchannels * sampwidth * framerate + segment_size = int(size_per_sec * self.seg_duration / 1000) + return asyncio.run(self.segment_data_processor(audio_data, segment_size)) diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/model/tti.py b/apps/models_provider/impl/volcanic_engine_model_provider/model/tti.py new file mode 100644 index 000000000..9b478b442 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/model/tti.py @@ -0,0 +1,172 @@ +# coding=utf-8 + +''' +requires Python 3.6 or later + +pip install asyncio +pip install websockets + +''' + +import datetime +import hashlib +import hmac +import json +import sys +from typing import Dict + +import requests + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + +method = 'POST' +host = 'visual.volcengineapi.com' +region = 'cn-north-1' +endpoint = 'https://visual.volcengineapi.com' +service = 'cv' + +req_key_dict = { + 'general_v1.4': 'high_aes_general_v14', + 'general_v2.0': 'high_aes_general_v20', + 'general_v2.0_L': 'high_aes_general_v20_L', + 'anime_v1.3': 'high_aes', + 'anime_v1.3.1': 'high_aes', +} + + +def sign(key, msg): + return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() + + +def getSignatureKey(key, dateStamp, regionName, serviceName): + kDate = sign(key.encode('utf-8'), dateStamp) + kRegion = sign(kDate, regionName) + kService = sign(kRegion, serviceName) + kSigning = sign(kService, 'request') + return kSigning + + +def formatQuery(parameters): + request_parameters_init = '' + for key in sorted(parameters): + request_parameters_init += key + '=' + parameters[key] + '&' + request_parameters = request_parameters_init[:-1] + return request_parameters + + +def signV4Request(access_key, secret_key, service, req_query, req_body): + if access_key is None or secret_key is None: + print('No access key is available.') + sys.exit() + + t = datetime.datetime.utcnow() + current_date = t.strftime('%Y%m%dT%H%M%SZ') + # current_date = '20210818T095729Z' + datestamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope + canonical_uri = '/' + canonical_querystring = req_query + signed_headers = 'content-type;host;x-content-sha256;x-date' + payload_hash = hashlib.sha256(req_body.encode('utf-8')).hexdigest() + content_type = 'application/json' + canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + \ + '\n' + 'x-content-sha256:' + payload_hash + \ + '\n' + 'x-date:' + current_date + '\n' + canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + \ + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash + # print(canonical_request) + algorithm = 'HMAC-SHA256' + credential_scope = datestamp + '/' + region + '/' + service + '/' + 'request' + string_to_sign = algorithm + '\n' + current_date + '\n' + credential_scope + '\n' + hashlib.sha256( + canonical_request.encode('utf-8')).hexdigest() + # print(string_to_sign) + signing_key = getSignatureKey(secret_key, datestamp, region, service) + # print(signing_key) + signature = hmac.new(signing_key, (string_to_sign).encode( + 'utf-8'), hashlib.sha256).hexdigest() + # print(signature) + + authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + \ + credential_scope + ', ' + 'SignedHeaders=' + \ + signed_headers + ', ' + 'Signature=' + signature + # print(authorization_header) + headers = {'X-Date': current_date, + 'Authorization': authorization_header, + 'X-Content-Sha256': payload_hash, + 'Content-Type': content_type + } + # print(headers) + + # ************* SEND THE REQUEST ************* + request_url = endpoint + '?' + canonical_querystring + + print('\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++') + print('Request URL = ' + request_url) + try: + r = requests.post(request_url, headers=headers, data=req_body) + except Exception as err: + print(f'error occurred: {err}') + raise + else: + print('\nRESPONSE++++++++++++++++++++++++++++++++++++') + print(f'Response code: {r.status_code}\n') + # 使用 replace 方法将 \u0026 替换为 & + resp_str = r.text.replace("\\u0026", "&") + if r.status_code != 200: + raise Exception(f'Error: {resp_str}') + print(f'Response body: {resp_str}\n') + return json.loads(resp_str)['data']['image_urls'] + + +class VolcanicEngineTextToImage(MaxKBBaseModel, BaseTextToImage): + access_key: str + secret_key: str + model_version: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.access_key = kwargs.get('access_key') + self.secret_key = kwargs.get('secret_key') + self.model_version = kwargs.get('model_version') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return VolcanicEngineTextToImage( + model_version=model_name, + access_key=model_credential.get('access_key'), + secret_key=model_credential.get('secret_key'), + **optional_params + ) + + def check_auth(self): + res = self.generate_image('生成一张小猫图片') + print(res) + + def generate_image(self, prompt: str, negative_prompt: str = None): + # 请求Query,按照接口文档中填入即可 + query_params = { + 'Action': 'CVProcess', + 'Version': '2022-08-31', + } + formatted_query = formatQuery(query_params) + size = self.params.pop('size', '512*512').split('*') + body_params = { + "req_key": req_key_dict[self.model_version], + "prompt": prompt, + "model_version": self.model_version, + "return_url": True, + "width": int(size[0]), + "height": int(size[1]), + **self.params + } + formatted_body = json.dumps(body_params) + return signV4Request(self.access_key, self.secret_key, service, formatted_query, formatted_body) + + def is_cache_model(self): + return False diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/model/tts.py b/apps/models_provider/impl/volcanic_engine_model_provider/model/tts.py new file mode 100644 index 000000000..6f8f3c600 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/model/tts.py @@ -0,0 +1,182 @@ +# coding=utf-8 + +''' +requires Python 3.6 or later + +pip install asyncio +pip install websockets + +''' + +import asyncio +import copy +import gzip +import json +import re +import ssl +import uuid +from typing import Dict + +import websockets +from django.utils.translation import gettext as _ + +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech + +MESSAGE_TYPES = {11: "audio-only server response", 12: "frontend server response", 15: "error message from server"} +MESSAGE_TYPE_SPECIFIC_FLAGS = {0: "no sequence number", 1: "sequence number > 0", + 2: "last message from server (seq < 0)", 3: "sequence number < 0"} +MESSAGE_SERIALIZATION_METHODS = {0: "no serialization", 1: "JSON", 15: "custom type"} +MESSAGE_COMPRESSIONS = {0: "no compression", 1: "gzip", 15: "custom compression method"} + +# version: b0001 (4 bits) +# header size: b0001 (4 bits) +# message type: b0001 (Full client request) (4bits) +# message type specific flags: b0000 (none) (4bits) +# message serialization method: b0001 (JSON) (4 bits) +# message compression: b0001 (gzip) (4bits) +# reserved data: 0x00 (1 byte) +default_header = bytearray(b'\x11\x10\x11\x00') + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +ssl_context.check_hostname = False +ssl_context.verify_mode = ssl.CERT_NONE + + +class VolcanicEngineTextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + volcanic_app_id: str + volcanic_cluster: str + volcanic_api_url: str + volcanic_token: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.volcanic_api_url = kwargs.get('volcanic_api_url') + self.volcanic_token = kwargs.get('volcanic_token') + self.volcanic_app_id = kwargs.get('volcanic_app_id') + self.volcanic_cluster = kwargs.get('volcanic_cluster') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'voice_type': 'zh_female_cancan_mars_bigtts', 'speed_ratio': 1.0}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return VolcanicEngineTextToSpeech( + volcanic_api_url=model_credential.get('volcanic_api_url'), + volcanic_token=model_credential.get('volcanic_token'), + volcanic_app_id=model_credential.get('volcanic_app_id'), + volcanic_cluster=model_credential.get('volcanic_cluster'), + **optional_params + ) + + def check_auth(self): + self.text_to_speech(_('Hello')) + + def text_to_speech(self, text): + request_json = { + "app": { + "appid": self.volcanic_app_id, + "token": "access_token", + "cluster": self.volcanic_cluster + }, + "user": { + "uid": "uid" + }, + "audio": { + "encoding": "mp3", + "volume_ratio": 1.0, + "pitch_ratio": 1.0, + } | self.params, + "request": { + "reqid": str(uuid.uuid4()), + "text": '', + "text_type": "plain", + "operation": "xxx" + } + } + text = _remove_empty_lines(text) + + return asyncio.run(self.submit(request_json, text)) + + def is_cache_model(self): + return False + + def token_auth(self): + return {'Authorization': 'Bearer; {}'.format(self.volcanic_token)} + + async def submit(self, request_json, text): + submit_request_json = copy.deepcopy(request_json) + submit_request_json["request"]["operation"] = "submit" + header = {"Authorization": f"Bearer; {self.volcanic_token}"} + result = b'' + async with websockets.connect(self.volcanic_api_url, extra_headers=header, ping_interval=None, + ssl=ssl_context) as ws: + lines = [text[i:i + 200] for i in range(0, len(text), 200)] + for line in lines: + if self.is_table_format_chars_only(line): + continue + submit_request_json["request"]["reqid"] = str(uuid.uuid4()) + submit_request_json["request"]["text"] = line + payload_bytes = str.encode(json.dumps(submit_request_json)) + payload_bytes = gzip.compress(payload_bytes) # if no compression, comment this line + full_client_request = bytearray(default_header) + full_client_request.extend((len(payload_bytes)).to_bytes(4, 'big')) # payload size(4 bytes) + full_client_request.extend(payload_bytes) # payload + await ws.send(full_client_request) + result += await self.parse_response(ws) + return result + + @staticmethod + def is_table_format_chars_only(s): + # 检查是否仅包含 "|", "-", 和空格字符 + return bool(s) and re.fullmatch(r'[|\-\s]+', s) + + @staticmethod + async def parse_response(ws): + result = b'' + while True: + res = await ws.recv() + protocol_version = res[0] >> 4 + header_size = res[0] & 0x0f + message_type = res[1] >> 4 + message_type_specific_flags = res[1] & 0x0f + serialization_method = res[2] >> 4 + message_compression = res[2] & 0x0f + reserved = res[3] + header_extensions = res[4:header_size * 4] + payload = res[header_size * 4:] + if header_size != 1: + # print(f" Header extensions: {header_extensions}") + pass + if message_type == 0xb: # audio-only server response + if message_type_specific_flags == 0: # no sequence number as ACK + continue + else: + sequence_number = int.from_bytes(payload[:4], "big", signed=True) + payload_size = int.from_bytes(payload[4:8], "big", signed=False) + payload = payload[8:] + result += payload + if sequence_number < 0: + break + else: + continue + elif message_type == 0xf: + code = int.from_bytes(payload[:4], "big", signed=False) + msg_size = int.from_bytes(payload[4:8], "big", signed=False) + error_msg = payload[8:] + if message_compression == 1: + error_msg = gzip.decompress(error_msg) + error_msg = str(error_msg, "utf-8") + raise Exception(f"Error code: {code}, message: {error_msg}") + elif message_type == 0xc: + msg_size = int.from_bytes(payload[:4], "big", signed=False) + payload = payload[4:] + if message_compression == 1: + payload = gzip.decompress(payload) + else: + break + return result diff --git a/apps/models_provider/impl/volcanic_engine_model_provider/volcanic_engine_model_provider.py b/apps/models_provider/impl/volcanic_engine_model_provider/volcanic_engine_model_provider.py new file mode 100644 index 000000000..2d16c6f16 --- /dev/null +++ b/apps/models_provider/impl/volcanic_engine_model_provider/volcanic_engine_model_provider.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Project :MaxKB +@File :gemini_model_provider.py +@Author :Brian Yang +@Date :5/13/24 7:47 AM +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, \ + ModelInfoManage +from models_provider.impl.openai_model_provider.credential.llm import OpenAILLMModelCredential +from models_provider.impl.volcanic_engine_model_provider.credential.embedding import VolcanicEmbeddingCredential +from models_provider.impl.volcanic_engine_model_provider.credential.image import \ + VolcanicEngineImageModelCredential +from models_provider.impl.volcanic_engine_model_provider.credential.tti import VolcanicEngineTTIModelCredential +from models_provider.impl.volcanic_engine_model_provider.credential.tts import VolcanicEngineTTSModelCredential +from models_provider.impl.volcanic_engine_model_provider.model.embedding import VolcanicEngineEmbeddingModel +from models_provider.impl.volcanic_engine_model_provider.model.image import VolcanicEngineImage +from models_provider.impl.volcanic_engine_model_provider.model.llm import VolcanicEngineChatModel +from models_provider.impl.volcanic_engine_model_provider.credential.stt import VolcanicEngineSTTModelCredential +from models_provider.impl.volcanic_engine_model_provider.model.stt import VolcanicEngineSpeechToText +from models_provider.impl.volcanic_engine_model_provider.model.tti import VolcanicEngineTextToImage +from models_provider.impl.volcanic_engine_model_provider.model.tts import VolcanicEngineTextToSpeech + +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +volcanic_engine_llm_model_credential = OpenAILLMModelCredential() +volcanic_engine_stt_model_credential = VolcanicEngineSTTModelCredential() +volcanic_engine_tts_model_credential = VolcanicEngineTTSModelCredential() +volcanic_engine_image_model_credential = VolcanicEngineImageModelCredential() +volcanic_engine_tti_model_credential = VolcanicEngineTTIModelCredential() + +model_info_list = [ + ModelInfo('ep-xxxxxxxxxx-yyyy', + _('The user goes to the model inference page of Volcano Ark to create an inference access point. Here, you need to enter ep-xxxxxxxxxx-yyyy to call it.'), + ModelTypeConst.LLM, + volcanic_engine_llm_model_credential, VolcanicEngineChatModel + ), + ModelInfo('ep-xxxxxxxxxx-yyyy', + _('The user goes to the model inference page of Volcano Ark to create an inference access point. Here, you need to enter ep-xxxxxxxxxx-yyyy to call it.'), + ModelTypeConst.IMAGE, + volcanic_engine_image_model_credential, VolcanicEngineImage + ), + ModelInfo('asr', + '', + ModelTypeConst.STT, + volcanic_engine_stt_model_credential, VolcanicEngineSpeechToText + ), + ModelInfo('tts', + '', + ModelTypeConst.TTS, + volcanic_engine_tts_model_credential, VolcanicEngineTextToSpeech + ), + ModelInfo('general_v2.0', + _('Universal 2.0-Vincent Diagram'), + ModelTypeConst.TTI, + volcanic_engine_tti_model_credential, VolcanicEngineTextToImage + ), + ModelInfo('general_v2.0_L', + _('Universal 2.0Pro-Vincent Chart'), + ModelTypeConst.TTI, + volcanic_engine_tti_model_credential, VolcanicEngineTextToImage + ), + ModelInfo('general_v1.4', + _('Universal 1.4-Vincent Chart'), + ModelTypeConst.TTI, + volcanic_engine_tti_model_credential, VolcanicEngineTextToImage + ), + ModelInfo('anime_v1.3', + _('Animation 1.3.0-Vincent Picture'), + ModelTypeConst.TTI, + volcanic_engine_tti_model_credential, VolcanicEngineTextToImage + ), + ModelInfo('anime_v1.3.1', + _('Animation 1.3.1-Vincent Picture'), + ModelTypeConst.TTI, + volcanic_engine_tti_model_credential, VolcanicEngineTextToImage + ), +] + +open_ai_embedding_credential = VolcanicEmbeddingCredential() +model_info_embedding_list = [ + ModelInfo('ep-xxxxxxxxxx-yyyy', + _('The user goes to the model inference page of Volcano Ark to create an inference access point. Here, you need to enter ep-xxxxxxxxxx-yyyy to call it.'), + ModelTypeConst.EMBEDDING, open_ai_embedding_credential, + VolcanicEngineEmbeddingModel) +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info(model_info_list[0]) + .append_default_model_info(model_info_list[1]) + .append_default_model_info(model_info_list[2]) + .append_default_model_info(model_info_list[3]) + .append_default_model_info(model_info_list[4]) + .append_model_info_list(model_info_embedding_list) + .append_default_model_info(model_info_embedding_list[0]) + .build() +) + + +class VolcanicEngineModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_volcanic_engine_provider', name=_('volcano engine'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'volcanic_engine_model_provider', + 'icon', + 'volcanic_engine_icon_svg'))) diff --git a/apps/models_provider/impl/wenxin_model_provider/__init__.py b/apps/models_provider/impl/wenxin_model_provider/__init__.py new file mode 100644 index 000000000..53b7001e5 --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2023/10/31 17:16 + @desc: +""" diff --git a/apps/models_provider/impl/wenxin_model_provider/credential/embedding.py b/apps/models_provider/impl/wenxin_model_provider/credential/embedding.py new file mode 100644 index 000000000..5d72b773a --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/credential/embedding.py @@ -0,0 +1,49 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/10/17 15:40 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class QianfanEmbeddingCredential(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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + self.valid_form(model_credential) + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'qianfan_sk': super().encryption(model.get('qianfan_sk', ''))} + + qianfan_ak = forms.PasswordInputField('API Key', required=True) + + qianfan_sk = forms.PasswordInputField("Secret Key", required=True) diff --git a/apps/models_provider/impl/wenxin_model_provider/credential/llm.py b/apps/models_provider/impl/wenxin_model_provider/credential/llm.py new file mode 100644 index 000000000..bb458fb6f --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/credential/llm.py @@ -0,0 +1,82 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/12 10:19 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class WenxinLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.95, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=2, + _max=100000, + _step=1, + precision=0) + + +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)) + 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 key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model.invoke( + [HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + raise e + return True + + def encryption_dict(self, model_info: Dict[str, object]): + return {**model_info, 'secret_key': super().encryption(model_info.get('secret_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') + return self + + api_key = forms.PasswordInputField('API Key', required=True) + + secret_key = forms.PasswordInputField("Secret Key", required=True) + + def get_model_params_setting_form(self, model_name): + return WenxinLLMModelParams() diff --git a/apps/models_provider/impl/wenxin_model_provider/icon/azure_icon_svg b/apps/models_provider/impl/wenxin_model_provider/icon/azure_icon_svg new file mode 100644 index 000000000..4added84b --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/icon/azure_icon_svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/models_provider/impl/wenxin_model_provider/model/embedding.py b/apps/models_provider/impl/wenxin_model_provider/model/embedding.py new file mode 100644 index 000000000..418aabde5 --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/model/embedding.py @@ -0,0 +1,23 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/10/17 16:48 + @desc: +""" +from typing import Dict + +from langchain_community.embeddings import QianfanEmbeddingsEndpoint + +from models_provider.base_model_provider import MaxKBBaseModel + + +class QianfanEmbeddings(MaxKBBaseModel, QianfanEmbeddingsEndpoint): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return QianfanEmbeddings( + model=model_name, + qianfan_ak=model_credential.get('qianfan_ak'), + qianfan_sk=model_credential.get('qianfan_sk'), + ) diff --git a/apps/models_provider/impl/wenxin_model_provider/model/llm.py b/apps/models_provider/impl/wenxin_model_provider/model/llm.py new file mode 100644 index 000000000..aa79692b2 --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/model/llm.py @@ -0,0 +1,76 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2023/11/10 17:45 + @desc: +""" +from typing import List, Dict, Optional, Any, Iterator + +from langchain_community.chat_models.baidu_qianfan_endpoint import _convert_dict_to_message, QianfanChatEndpoint +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.messages import ( + AIMessageChunk, + BaseMessage, +) +from langchain_core.outputs import ChatGenerationChunk + +from models_provider.base_model_provider import MaxKBBaseModel + + +class QianfanChatModel(MaxKBBaseModel, QianfanChatEndpoint): + @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 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) + + usage_metadata: dict = {} + + def get_last_generation_info(self) -> Optional[Dict[str, Any]]: + return self.usage_metadata + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + return self.usage_metadata.get('prompt_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + return self.usage_metadata.get('completion_tokens', 0) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + kwargs = {**self.init_kwargs, **kwargs} + params = self._convert_prompt_msg_params(messages, **kwargs) + params["stop"] = stop + params["stream"] = True + for res in self.client.do(**params): + if res: + msg = _convert_dict_to_message(res) + additional_kwargs = msg.additional_kwargs.get("function_call", {}) + if msg.content == "" or res.get("body").get("is_end"): + token_usage = res.get("body").get("usage") + self.usage_metadata = token_usage + chunk = ChatGenerationChunk( + text=res["result"], + message=AIMessageChunk( # type: ignore[call-arg] + content=msg.content, + role="assistant", + additional_kwargs=additional_kwargs, + ), + generation_info=msg.additional_kwargs, + ) + if run_manager: + run_manager.on_llm_new_token(chunk.text, chunk=chunk) + yield chunk diff --git a/apps/models_provider/impl/wenxin_model_provider/wenxin_model_provider.py b/apps/models_provider/impl/wenxin_model_provider/wenxin_model_provider.py new file mode 100644 index 000000000..a4de65254 --- /dev/null +++ b/apps/models_provider/impl/wenxin_model_provider/wenxin_model_provider.py @@ -0,0 +1,68 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: wenxin_model_provider.py + @date:2023/10/31 16:19 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import ModelProvideInfo, ModelTypeConst, ModelInfo, IModelProvider, \ + ModelInfoManage +from models_provider.impl.wenxin_model_provider.credential.embedding import QianfanEmbeddingCredential +from models_provider.impl.wenxin_model_provider.credential.llm import WenxinLLMModelCredential +from models_provider.impl.wenxin_model_provider.model.embedding import QianfanEmbeddings +from models_provider.impl.wenxin_model_provider.model.llm import QianfanChatModel +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +win_xin_llm_model_credential = WenxinLLMModelCredential() +qianfan_embedding_credential = QianfanEmbeddingCredential() +model_info_list = [ModelInfo('ERNIE-Bot-4', + _('ERNIE-Bot-4 is a large language model independently developed by Baidu. It covers massive Chinese data and has stronger capabilities in dialogue Q&A, content creation and generation.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('ERNIE-Bot', + _('ERNIE-Bot is a large language model independently developed by Baidu. It covers massive Chinese data and has stronger capabilities in dialogue Q&A, content creation and generation.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('ERNIE-Bot-turbo', + _('ERNIE-Bot-turbo is a large language model independently developed by Baidu. It covers massive Chinese data, has stronger capabilities in dialogue Q&A, content creation and generation, and has a faster response speed.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('BLOOMZ-7B', + _('BLOOMZ-7B is a well-known large language model in the industry. It was developed and open sourced by BigScience and can output text in 46 languages and 13 programming languages.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('Llama-2-7b-chat', + 'Llama-2-7b-chat was developed by Meta AI and is open source. It performs well in scenarios such as coding, reasoning and knowledge application. Llama-2-7b-chat is a high-performance native open source version suitable for conversation scenarios.', + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('Llama-2-13b-chat', + _('Llama-2-13b-chat was developed by Meta AI and is open source. It performs well in scenarios such as coding, reasoning and knowledge application. Llama-2-13b-chat is a native open source version with balanced performance and effect, suitable for conversation scenarios.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('Llama-2-70b-chat', + _('Llama-2-70b-chat was developed by Meta AI and is open source. It performs well in scenarios such as coding, reasoning, and knowledge application. Llama-2-70b-chat is a native open source version with high-precision effects.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel), + ModelInfo('Qianfan-Chinese-Llama-2-7B', + _('The Chinese enhanced version developed by the Qianfan team based on Llama-2-7b has performed well on Chinese knowledge bases such as CMMLU and C-EVAL.'), + ModelTypeConst.LLM, win_xin_llm_model_credential, QianfanChatModel) + ] +embedding_model_info = ModelInfo('Embedding-V1', + _('Embedding-V1 is a text representation model based on Baidu Wenxin large model technology. It can convert text into a vector form represented by numerical values and can be used in text retrieval, information recommendation, knowledge mining and other scenarios. Embedding-V1 provides the Embeddings interface, which can generate corresponding vector representations based on input content. You can call this interface to input text into the model and obtain the corresponding vector representation for subsequent text processing and analysis.'), + ModelTypeConst.EMBEDDING, qianfan_embedding_credential, QianfanEmbeddings) +model_info_manage = ModelInfoManage.builder().append_model_info_list(model_info_list).append_default_model_info( + ModelInfo('ERNIE-Bot-4', + _('ERNIE-Bot-4 is a large language model independently developed by Baidu. It covers massive Chinese data and has stronger capabilities in dialogue Q&A, content creation and generation.'), + ModelTypeConst.LLM, + win_xin_llm_model_credential, + QianfanChatModel)).append_model_info(embedding_model_info).append_default_model_info( + embedding_model_info).build() + + +class WenxinModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_wenxin_provider', name=_('Thousand sails large model'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'wenxin_model_provider', 'icon', + 'azure_icon_svg'))) diff --git a/apps/models_provider/impl/xf_model_provider/__init__.py b/apps/models_provider/impl/xf_model_provider/__init__.py new file mode 100644 index 000000000..c743b4e18 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/__init__.py @@ -0,0 +1,8 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/04/19 15:55 + @desc: +""" \ No newline at end of file diff --git a/apps/models_provider/impl/xf_model_provider/credential/embedding.py b/apps/models_provider/impl/xf_model_provider/credential/embedding.py new file mode 100644 index 000000000..d945da82a --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/credential/embedding.py @@ -0,0 +1,50 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/10/17 15:40 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XFEmbeddingCredential(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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + self.valid_form(model_credential) + try: + model = provider.get_model(model_type, model_name, model_credential) + model.embed_query(_('Hello')) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'spark_api_secret': super().encryption(model.get('spark_api_secret', ''))} + + base_url = forms.TextInputField('API URL', required=True, default_value="https://emb-cn-huabei-1.xf-yun.com/") + spark_app_id = forms.TextInputField('APP ID', required=True) + spark_api_key = forms.PasswordInputField("API Key", required=True) + spark_api_secret = forms.PasswordInputField('API Secret', required=True) diff --git a/apps/models_provider/impl/xf_model_provider/credential/image.py b/apps/models_provider/impl/xf_model_provider/credential/image.py new file mode 100644 index 000000000..e952ea349 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/credential/image.py @@ -0,0 +1,60 @@ +# coding=utf-8 +import base64 +import os +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.xf_model_provider.model.image import ImageMessage + + +class XunFeiImageModelCredential(BaseForm, BaseModelCredential): + spark_api_url = forms.TextInputField('API URL', required=True, + default_value='wss://spark-api.cn-huabei-1.xf-yun.com/v2.1/image') + spark_app_id = forms.TextInputField('APP ID', required=True) + spark_api_key = forms.PasswordInputField("API Key", required=True) + spark_api_secret = forms.PasswordInputField('API Secret', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['spark_api_url', 'spark_app_id', 'spark_api_key', 'spark_api_secret']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + cwd = os.path.dirname(os.path.abspath(__file__)) + with open(f'{cwd}/img_1.png', 'rb') as f: + message_list = [ImageMessage(str(base64.b64encode(f.read()), 'utf-8')), + HumanMessage(_('Please outline this picture'))] + model.stream(message_list) + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'spark_api_secret': super().encryption(model.get('spark_api_secret', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/xf_model_provider/credential/img_1.png b/apps/models_provider/impl/xf_model_provider/credential/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..ccb9d3b20359de7347b25335b935bce65e2d899d GIT binary patch literal 363045 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

  • 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�P7{G7XP!=inbiY) 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 x&r+ulC$(dI002ovPDHLkV1g~*^^*Vq literal 0 HcmV?d00001 diff --git a/apps/models_provider/impl/xf_model_provider/credential/llm.py b/apps/models_provider/impl/xf_model_provider/credential/llm.py new file mode 100644 index 000000000..5a3de3c1b --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/credential/llm.py @@ -0,0 +1,101 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/12 10:29 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XunFeiLLMModelGeneralParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.5, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=4096, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class XunFeiLLMModelProParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.5, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=4096, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class XunFeiLLMModelCredential(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)) + + for key in ['spark_api_url', 'spark_app_id', 'spark_api_key', 'spark_api_secret']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'spark_api_secret': super().encryption(model.get('spark_api_secret', ''))} + + spark_api_url = forms.TextInputField('API URL', required=True) + spark_app_id = forms.TextInputField('APP ID', required=True) + spark_api_key = forms.PasswordInputField("API Key", required=True) + spark_api_secret = forms.PasswordInputField('API Secret', required=True) + + def get_model_params_setting_form(self, model_name): + if model_name == 'general' or model_name == 'pro-128k': + return XunFeiLLMModelGeneralParams() + return XunFeiLLMModelProParams() diff --git a/apps/models_provider/impl/xf_model_provider/credential/stt.py b/apps/models_provider/impl/xf_model_provider/credential/stt.py new file mode 100644 index 000000000..56d697b36 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/credential/stt.py @@ -0,0 +1,51 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XunFeiSTTModelCredential(BaseForm, BaseModelCredential): + spark_api_url = forms.TextInputField('API URL', required=True, default_value='wss://iat-api.xfyun.cn/v2/iat') + spark_app_id = forms.TextInputField('APP ID', required=True) + spark_api_key = forms.PasswordInputField("API Key", required=True) + spark_api_secret = forms.PasswordInputField('API Secret', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['spark_api_url', 'spark_app_id', 'spark_api_key', 'spark_api_secret']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'spark_api_secret': super().encryption(model.get('spark_api_secret', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/xf_model_provider/credential/tts.py b/apps/models_provider/impl/xf_model_provider/credential/tts.py new file mode 100644 index 000000000..68a481b29 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/credential/tts.py @@ -0,0 +1,75 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XunFeiTTSModelGeneralParams(BaseForm): + vcn = forms.SingleSelect( + TooltipLabel(_('Speaker'), + _('Speaker, optional value: Please go to the console to add a trial or purchase speaker. After adding, the speaker parameter value will be displayed.')), + required=True, default_value='xiaoyan', + text_field='value', + value_field='value', + option_list=[ + {'text': _('iFlytek Xiaoyan'), 'value': 'xiaoyan'}, + {'text': _('iFlytek Xujiu'), 'value': 'aisjiuxu'}, + {'text': _('iFlytek Xiaoping'), 'value': 'aisxping'}, + {'text': _('iFlytek Xiaojing'), 'value': 'aisjinger'}, + {'text': _('iFlytek Xuxiaobao'), 'value': 'aisbabyxu'}, + ]) + speed = forms.SliderField( + TooltipLabel(_('speaking speed'), _('Speech speed, optional value: [0-100], default is 50')), + required=True, default_value=50, + _min=1, + _max=100, + _step=5, + precision=1) + + +class XunFeiTTSModelCredential(BaseForm, BaseModelCredential): + spark_api_url = forms.TextInputField('API URL', required=True, default_value='wss://tts-api.xfyun.cn/v2/tts') + spark_app_id = forms.TextInputField('APP ID', required=True) + spark_api_key = forms.PasswordInputField("API Key", required=True) + spark_api_secret = forms.PasswordInputField('API Secret', required=True) + + 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)) + + for key in ['spark_api_url', 'spark_app_id', 'spark_api_key', 'spark_api_secret']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'spark_api_secret': super().encryption(model.get('spark_api_secret', ''))} + + def get_model_params_setting_form(self, model_name): + return XunFeiTTSModelGeneralParams() diff --git a/apps/models_provider/impl/xf_model_provider/icon/xf_icon_svg b/apps/models_provider/impl/xf_model_provider/icon/xf_icon_svg new file mode 100644 index 000000000..b74e351e2 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/icon/xf_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/xf_model_provider/model/embedding.py b/apps/models_provider/impl/xf_model_provider/model/embedding.py new file mode 100644 index 000000000..b6e7d2867 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/model/embedding.py @@ -0,0 +1,49 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: embedding.py + @date:2024/10/17 15:29 + @desc: +""" + +import base64 +import json +from typing import Dict, Optional + +import numpy as np +from langchain_community.embeddings import SparkLLMTextEmbeddings +from numpy import ndarray + +from models_provider.base_model_provider import MaxKBBaseModel + + +class XFEmbedding(MaxKBBaseModel, SparkLLMTextEmbeddings): + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return XFEmbedding( + spark_app_id=model_credential.get('spark_app_id'), + spark_api_key=model_credential.get('spark_api_key'), + spark_api_secret=model_credential.get('spark_api_secret') + ) + + @staticmethod + def _parser_message( + message: str, + ) -> Optional[ndarray]: + data = json.loads(message) + code = data["header"]["code"] + if code != 0: + # 这里是讯飞的QPS限制会报错,所以不建议用讯飞的向量模型 + raise Exception(f"Request error: {code}, {data}") + else: + text_base = data["payload"]["feature"]["text"] + text_data = base64.b64decode(text_base) + dt = np.dtype(np.float32) + dt = dt.newbyteorder("<") + text = np.frombuffer(text_data, dtype=dt) + if len(text) > 2560: + array = text[:2560] + else: + array = text + return array diff --git a/apps/models_provider/impl/xf_model_provider/model/iat_mp3_16k.mp3 b/apps/models_provider/impl/xf_model_provider/model/iat_mp3_16k.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..75e744c8ff5188208a3797561efb2e096d4fa015 GIT binary patch literal 17876 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 dict: + message_dict: Dict[str, Any] + if isinstance(message, ChatMessage): + message_dict = {"role": "user", "content": message.content} + elif isinstance(message, ImageMessage): + message_dict = {"role": "user", "content": message.content, "content_type": "image"} + elif isinstance(message, HumanMessage): + message_dict = {"role": "user", "content": message.content} + elif isinstance(message, AIMessage): + message_dict = {"role": "assistant", "content": message.content} + if "function_call" in message.additional_kwargs: + message_dict["function_call"] = message.additional_kwargs["function_call"] + # If function call only, content is None not empty string + if message_dict["content"] == "": + message_dict["content"] = None + if "tool_calls" in message.additional_kwargs: + message_dict["tool_calls"] = message.additional_kwargs["tool_calls"] + # If tool calls only, content is None not empty string + if message_dict["content"] == "": + message_dict["content"] = None + # elif isinstance(message, SystemMessage): + # message_dict = {"role": "system", "content": message.content} + else: + raise ValueError(f"Got unknown type {message}") + + return message_dict + + +class XFSparkImage(MaxKBBaseModel, ChatSparkLLM): + spark_app_id: str + spark_api_key: str + spark_api_secret: str + spark_api_url: str + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return XFSparkImage( + spark_app_id=model_credential.get('spark_app_id'), + spark_api_key=model_credential.get('spark_api_key'), + spark_api_secret=model_credential.get('spark_api_secret'), + spark_api_url=model_credential.get('spark_api_url'), + **optional_params + ) + + @staticmethod + def generate_message(prompt: str, image) -> list[BaseMessage]: + if image is None: + cwd = os.path.dirname(os.path.abspath(__file__)) + with open(f'{cwd}/img_1.png', 'rb') as f: + base64_image = base64.b64encode(f.read()).decode("utf-8") + return [ImageMessage(f'data:image/jpeg;base64,{base64_image}'), HumanMessage(prompt)] + return [HumanMessage(prompt)] + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + default_chunk_class = AIMessageChunk + + self.client.arun( + [convert_message_to_dict(m) for m in messages], + self.spark_user_id, + self.model_kwargs, + streaming=True, + ) + for content in self.client.subscribe(timeout=self.request_timeout): + if "data" not in content: + continue + delta = content["data"] + chunk = _convert_delta_to_message_chunk(delta, default_chunk_class) + cg_chunk = ChatGenerationChunk(message=chunk) + if run_manager: + run_manager.on_llm_new_token(str(chunk.content), chunk=cg_chunk) + yield cg_chunk diff --git a/apps/models_provider/impl/xf_model_provider/model/img_1.png b/apps/models_provider/impl/xf_model_provider/model/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..ccb9d3b20359de7347b25335b935bce65e2d899d GIT binary patch literal 363045 zcmZs?Wn7eDw>Juc3^{ZU-O@dzG)OC5BGMo+l!SB*($d|blypcl(jXy7$Iv}=<2yck z@AsVZJNw(*GhgPut`+}SD@t2a84sHZ8wCXg@0E(e8x)jhS12f`7nskG-^>?1t)rj> zp}bO%)%AXRxct#W$DnZcZ|g~C2JYC}%4Qtoq+{^{T}9<2mIO0w!f_YlgQY9#k{@p! zx6v0T;Sskat5))uuN>+Eg4g36Cso(?kLOQ*_u2j)4JF(|7%@Exj{f9okT5Qqp%uFE zm7VODRW}EmTFZG+sSgtKU5P!5hKSsLGQOmez)1(O`6+7!wN&DsMQ=oIgh}l~O<*~K z8{rn|f-00ms(g?D0ZVMR<8T{==IXFtzgl=Wgg=@9PoPii_USHgKxu(Zo?ODA`JUJg?x~C+oO;h+>|dYyT23~g;=Dz z#%e%q#V`&vec+ZIN3Gv^c3g@5WR{-Q#j@=1l2OzgCxJ7gK3)uSMM?fInfk`l%D(== z(Ab}t@foe0`Wg(m31tv`mH!W@%kn2=C5$AO77R$!h}91mzHu5 z<~4RE3AV*FqfE~}kyBs0I#S#%Ny>4(a|{{li9gCjai{py;rz4>Timxj+IQSKwB6ct z#B#f6dUs3c&hYR%3?F zmbTCCh__SNpdgJ{m=A*>k!?UYTR(@r*dAj&p_?HToy87YXGt*1kvniGkRjpZ{Nl)Ztb|}E@5q8 zSX^igi*{US8q46yA`$yQoGx&XcGCeP8Lhx_rvaNCAU6ljUFm5PJ)+7ctIXfyy54J_q%A~9AR$h& zchI7frcQmpe9*Qg$UBfif#@>t9kWhje;zahs7e+iu)A7F8{4k&Xnscc&hb-}P^Vk~ z`Hj6ShOQo%cX0Mb+6i1XA=F8Jw)E~eSbsmHN)}PCp@Wf+s)OD~LJJ*b>%~A%K?~4b?^4G+ zKsO8?G{9cbmq>67;#RSo8Xm#k+;vhs964o9Mtfh$Z`qO&isB)IYpatF!ldy7=p-b9 zd3}`Nqb~n*p684}Qx^;7xJ2;H#_kAHH->`0@o%&jq~ka*Vz}ZL>4L#er@&1-<$Msu z-T4-=eN7xQP#0L+m+}zIQe}x}EdN~E{;C4)NG1qO-y$(Ezeu&YX{o^p*j%$%U(-Y{ zK!eJ2My}go_&$T%;TP(Honv4=sJ#Ggia2T}$Trb`$94pjKx> zbuAdbaxLH}Y?Cst#V{zJCOfz=9p52t^D%>5U*je1uOYC`F>8bA@T(^7wXd1!#yBU4 zHYwi)TW_t~Mk&|i&8VI?mdnecbus;{p~soXe5Xbr&K-^+8%hLNkuln!v{2k3wMQ%T zFG>Qt-bKm8I9%hd;pD5!Foxm=w68mj5oJUDzR6u5-~60IOYD0nEbopa3sAKf+{~;} zE`Q7H=%9ptjaqH7gkA>pUJPj2no=}0q6*^PT5IuffE)5U zz$b=%ykG?>Xe~h-+bm2#I}x~cXGS>q#a4HRa6K_wB5XpE0MJfgLIcAp{#2v^LX?#O z;d4KvX(0^&%~rXIaAue*Q_K^uq^Ncx*T64#g=Ql7w) zuxpHZcg=gsNnwHBrcO~3F&js@{5lksEI>;DBz1EwS81uCWk9G%B;odn=}0yJW=cvZ zn@u1QM14ZQSVJ$7pv*@IB$ou+hijSjyasHO6-r!4+wE@TUj*aWf8|5BQ=jdA#iGuq z%Z%w!7xW~VpNH21&M_{m&g4hEMYjhOghseUz7?zxgp$TCVp>nwMSjI;%spHG*x70yMrS3+6%d5S8DY=c2U>A*>fn7SP4go@SfsNt=iK8mnX?#qOx7moJu9jYN z!b}UeHnJ31I=oUi9%QhoRM{f9cY`JwQbZMT=3Mg=DSz~hDq?-+^CoqG705%&1j*%x zl-KVia3+wYgKu<`ky&iW4ncfnXM&IkfbgO@A4m(5Bs|5e*LPGj#mUhM@|69QWx(uZ-ftZ7=+ z5}PzbNTG;N74HhBhN3mcH1*8?X-#pmAkPU(y}yA6k>>$jeQqjcQm+(e3C3_7)K!a$ zn$Qx)&dm1&V#uAm3LG5D!}0qImN0zBl@b`^BvYIdh<@2xvPurmP%e)bWZNaP`62!r z1N|l2Z)Fyl4$Pd*xMA_P`o*4=IkSd6xu}rugn#CqT^quDDjCM+HX@2FAmLq%4Y%~b z0+0;I(M=Hum3+-n%dXtWGXPuhXBq72!$nUidIdt@OUw*oSZSNSgHM?IBu1fqd^%tT z?x!oNfu@KPz`j>j-ipSP1;afMWH*#wndpxVfEy-!jgm);yXo4s&C(s`g2|4zUA!lX z^xZawVKqmvJ@r5>;|L<>ijdW2^HA0*2%L$==1i3XE13EV?$7miX9MC&6@%#d-;Smp zV0G=B<0<#Su>%$D;?+Rz(bn`hBU|)p2c-0vG@L(3XRlaOaT#YO^co-ZGLXyn_9WHfTB#gJX|0ho)`8M#p7f%Hl)&|3BT zehY+g^N$p*Cdfluc3KNGWw9xz*|JENQ+|eTjj}oc3#QNiggg)b%1LEVGFN=oXCnnL zq24-=+@yc3i32M+#BvU7&@$cC2j5AXYcEOEXdvr7|9MP_irDVIeK4?=;+ zkXC@*4 zSGdfUBomxqm?4e!mnsPV?3>;@66HW{cptOpTOaS7O+yI+-El@nV7nv%#NpEyaJQ%| zg9BVV4_ zzgU_J4f!LO@VgRgehmeCaThbBSr*}p;0@W0WDH=`eaVHo8LqJ#{y=q-Nekb-L8;LH zniQ$`CG8^%@6Fn_t(q;8tk6oQQv&I_-!|Y90E)T4_~83VpHn}@ajD-3X87UAgIdT3 zQb+k9YxAmW91Clo0ju;CK?GTAt{Cj>uyNk*sXxD-EnzaG$R7^+FhqrNC})8?A&4)L zz|PunX@1Qh8potho%6MqBQ;Hx%eyDkA5Mse`G^2)!1e+~P+K?~@NoEga-B0YHp}Bh$EI`SarCA8eV88uy%ohXtjZBfq@)2Ln z_#YB~%5;Vh1>B+=_M+H5pj8FgOLiFe_*G#e8ZpnB&#w97qrGBdTssxeBNaiA)+ z!5tNl)l119ss>wS(Eboi;K7o{ALVyp&U=x&&L_>$uXJ5sUvzyjcU`Zisd*kDVF`Pk zCk-^4!ZG0Oz=Tc0Vrepbk4VO;x+vgxD4SmI@{gire6F1~de_is)07|`Jb4w2{)K}6FQy*_2d$hK*il=vrwd1CQH>OvoZ`X6^%t?K zP;6ItEym-;n>>rK?F4XxrrpxSCx*}#hLDy?Mqm|)`+y+6^v;4Iz;$r%#SLg8kM}0G zaH624sLfQ&Uq$S0zDSd4&qd8f>;H4T$jU&r{>$oEiWE`_<#N)%C5qGr`g^3r9;oJn zh{O@eL9p96vvz~vCI8VSe*@Gl@4HOjdlQsdf=(nAE=5IMf+@Fl)(d|aP(BElYkh~9E%CT1I+Dfqk=_ASa!qRp;P6f~i zVW<4(7yQ>}{MT2F7nij$(Z)xOzFSl~)~{uO5-(TXxP9nqcx<(QgPCnsXvki9Zw-}w zbXwWOmsH?_W7VF;+5|lmLdzS=cj;A?ss;38)4rL{7aRX>nY0=^gaxx<{Z<6&iU9fC zcCEwuTf_Rn;R<4rO5$DDlXs4kY}WpVrxxo8g|QLh7}Vr7TAPYCLncYb zY+sj`s-SG)h$wkLm&z*fPdv}J#t3v>S6P=nAzD>B$nPYb-$qUoU-3YqU`M$x*)kVS z9v|cMp?-&# z>El8A51C7T;u8c(2X)v{MxAs0JA6NJc%?RBaOg+7`{kLBfx_^y(`Svkj;1 zJtvIhjSZ*ct@fp9i@j~{Jom`XHPOw>W<4zxBjPWq_t;NTCGWoFaT5O#ov}H7a`sa% z{PE-D1s}aEvdsX+lBJd5=s(5K2gJQ^vO?L~y>eVs%KQ2+gEJ`EA^{6@;}@Lw#*6bM zlm_vE=gW&di9A8z=d3C{JAAFoJ&rz@C=Pfl1s z7H+4dOBjl7D)6-z&mwKY^EY0fJ&lwm9SHe~-5De@AgvQU!h)E~C~4|Hw0o1bEo~7Y=LMg(KA4A|Jk*Bt9Q3;<(l%CvUGQi;k)~sN_emm9ByNTgZ3}4FZp9 zCN%@$MHPj_jlbfQ&_Xa6hfM0q@nP)tgbG;DBAk2BswVD+ESZ8Mu3FAW?josd~gztr%9ggoW zRo^{(UWN@Vi`RjeOg`O9WwaC>?iR%N^+JoP^W9Kbp@NADMtzOE6QUH5avG6e5n!qV z7kX!z^-0ZjhO3oo*2Lf#I(&bziLp$|B;3m;kz`wavSPB8cGsuTV;b`gAXGj(QYjR? zpGyO=TOADtr@QgN>F-@81kXEBV3XS8bm8E;6v3CUs6XH*Mtg#GVTzc<(rWwRAapGC+A9*Ch1xW7xtiIbPgv100VBm zJ5h)R3EN>rSw@Bt4*-k5#Ikz8DfrA_3^r-oav+U-f_2y6g6qg>hh5#A7f!Pj5&1xP zf>nWQC~4<>l;+Ei2}$VRDz)Sy)62sP0M$B61*}ze#}j236Oy^v#=e!OJ|aXr_$2>@ zWyqwF3L5PbP}=B*=~suX45&QY2#Qv3=JfukZOQA>4RJ8K;?#PcsrQ8H1&lF(GyO5u z5?e4*Tf;z4X5#AT8PeDxwRyJXF+)CNnBsppJRZRau|%!Pt|0GReKJ;&3DUEw5!rR)b}%9bI{UWvD;DgqHL3|paf^W&f#LKAZqUJB#bl5tjg)FE z4y2gj^M7LwE(2SWb)|?3?F!W*dm3~`75)-nl=nopw{o(kvWTKk6eG85U1Dz_;DYx)!*GH zn0)brV6NT_n4mKoVz3$qha`QVO#`DI99cXaK!-SXspVm>T~pDfn+ey z<6IV`76Sf_pE+Bck_lb_Br(m(^4!Ue6&AuKb%7wJ*iIv)sF<;iK>e`p#|5_|L|2Yk zZ+I@~I9KoAxqh;i6#nvTDeEPYJs|~FuwUj6?+l}bM=j+)Te-B`3;|xYz$27S!r4vt z&jKvMzZFti7|Gd{wY`dXIE`Luz9y{KZC{}EqO8ik?r^dM5abbc@-%^ZQ9(yodsew=XXjWQpl;X(tJs?D8A-`bg1420l00aRZw? z8-p{)mC(YwJ;gAhLQMxg>&I;?kZOWn945F~Ml=6ccjQ)QcT8ofh=MHT!-DOBwF^-G z?51}%cz)ZzL?38A)ghE~{oL8cRRtNnEi}_xZ&i^UhB%?*5<3A|+^nwDWLzg}Q#Oba zsz%(z^FeI*f(9Q2sk6E&jhtxrcMz9;S@*4;XzdZ78|n)`KjbbDS=eh@_Or+ijjgoR z<=)lBO=ZFNFJ8d1WLz*tP0| zR0n$rIGN8!>r#i8Dw8*E=XZ2?Y47$AntO|uppLpk?`S1=Ru9Ljn}dZwgMnH5e7~vnWUKz)I;uXvpH;D4Dx60WUSy zlQL4knMwM{5KVGS5Z256u-! zB^V*@X?y?`t{dWAgLcgmDm!b2W%*!$gHb3Cd{;k?2OiX| z@*y4UYE}63npW$k|K-Ya0yc0z3(utcV_Q%Wpo;|Zjdl!o4EI$EktSW&tKqNDJMF7R zD|p9WbV6zo+doIhfu9gP{rTEuiYlOrv=p%M-o5#Me7!^d|9MTMiIx=pKjjS)6&gXT z{HL)3bGMt4Qw2o5W8L5aGSvW?sULnY$3oP_B0Ry5kL1^P=xAdzW0Yx;nU2<%TRA1I z-J*R}mrU6hE#$Tdi}aRy*THisLBdjppO62&Zei1YRPOBCfU#S;uGpxzG_+eppgn>w zYwC5DE6|Ou;6WD}G>l5LsL;2DL7)|=9(a=n)VxN~oQ_^YcNH*b&*8&2^Y@*c$m{^! z;U;?+SAe{|pFdV}X@XhKugdnm~qf0LMatsKn?E}OQ910agHb^f`M34qrvl!+*m$DxDCY*QTI>6o+2_$7Of{-gG( z-K26^uy)HX6YwU?E_`U4ABq(16@JoODv*w6mN5YFXnMt?5DxhZFxgdqk6z#oup!Q+ zSZa`M3Ajn4^v|07Z?;dyid4?I{wJY-g@N_M+#0U8f!?9q^LAmj+k#9l*;LAR zv*HGQl{^UiyEO`FqNV|l=?%t>z2OrQZe-jL%&lEEpok2W-=u)iUD^OP-RQRHr*6UQ zepi)k^Mu(VEj-Z)x*J;wF?{9kiAyFGS3xED6+-DwM zm=%Q7&}78&hptkwq#Y9$`R2x%;i5x}RKA9K%iJk-voEauq-5gr3%+L=@Gq_UK8;p; z9z4ofW5R!qjw^flqLXhKiIkRo0KA<{rnN;rST4f$23X zom3jkT*J(?(>s2|8$DpGj+yi|yTo^W@%rFfc%K*V%<;pgIr&SJbWcKq8IMi${LJzL z24UH^66vnav%A<)oz+hrQX=C?%s@rr4`U~mC6IBMyWgMqF2$f&aDpD3%tG_B9m0l? z&R{^;eZn6rdfU?7=~s?tDf#AsaYG;$f&P~HeVjo0=B11KJaBHyN&D|5>`We&&dV6sRN#Iz}~TO3uW)StA$z8Xs&$K zcaZ1(M(+IUC?9eOxAu7d+LKBhj*g8u%j5g2;7IeeMp+I0m(KGLs;jtKw1Sa@Jz_Mf zsQSTPLQ61+<(yi2jdENi+XuMVi?dpfRC=rBqAYt1pY=u&lkV1W^!|{1Tq;gjHZfZ= z%j6wqgEAqbx`yYqKt+(7QfsWG7WycHn=cuKt(=WLDf94@S4x@8;pO9i}I`-REZMv zm`j3-cUvT8K01~=s`H@3G7ia!V1y77Gf->TPp22vJguoo{@bug8kY`Az>~hr_Z*k+ z+`7Qh)B%TaSg;k+-Hp4IC$!B8MTJj{`O+a?q2vMX1WS9UTAn`zA@NIGCKxg+1d*YN zG@^{Er|A1%PGW-y;Ovv)_B%3B9-eMO&=V~w1Mg;s&k-k1>y3QK(HN|VtMM@EcriHb zHE?zL#zu?)mk*CV4lGK|u56s@5(Ex?V6_Gi%ax_+O)l$Vv@QSPvFF&cuh>r#%9Z%R zX9sLGn3ve$8@5F_u(|zbKv%L9|%m3i8a+nXQdoX1@7 z2b8RzWu!}06A=1AtQKHjxjKEhk9Nmvnp{ljFrmaG+lZwoG?${tn<*Sc*%f|;-QrLO zx6kSMvH(v9MCoiay&()5s+GS6iwc;Tk>SsZDim!caPlgQUM}Xb3HACW#$_u$hQ%c( z$nKmQ0cN+pSPtmb1$a)|%h^gt6=ilXc0ykOtnpOo?i|=)&$WaqEtR#L3zbu-qw3lJ zw-an97+6woB#+buGRJ*oNp#rbPi&CJiEal~&wbRWL- z?JMEla_!3c3J`_L`r~~S>Ry;SZWPl87S?aw{thDozw(}*mPI%Iu->vT-zmXbZX*S@ zEvtu-IYJ*d?z*}LlT8uigftADwRD8RdN5vF^16ZF<3<))1@r+V`}UZC{~1RhX$mLr zWMcx3j(Tw9lfNPET|yt8}+&n!APvnSfA5a9}U+;LXZ^K2bu$B;@aSZpa6`; zjA5Wnph=bK)Y3S|5GVLYfZ99nkZ5Bu6$Y$0kAhd4R$*?vv*JWwg^HwC427E%gNZ=z z;WY0z4vQ>56f#!93lC@eC7e_}R=;dEZiF_P2iBd|U(eDmJ-&7HUs*n_j%>Cns*B@q zb#;>wC^^hX{)p#w#(XHQ>GVq3owT{O_$zA-*WcmOK69se5+JnR*OJ1^+1!50K_ov_3J3X%X9b0RcQR>H=}C z@g5bcgsLo0s0V^>f6nz`ZYALKBB`O(I+Pz2_vIFk4uOl5yd)!?*27OS2yHXIZV7~e zf45ma6ZZ-XN8OTve|oyYfXHK^d@e3TA&SiMR7_9f=GGww91kKz2)tLIa?tZU ztPhhM<)oj#ef*2i99@{PI2}g>r46tEkFv^{6D&O#gR`K9Kg!t`2VD=Ke9?n^(Q~d9 zFqN!W$heC$V)=3?g3jv$+R@!bo?4VST(MQm0 zI%Cksl_M}zvl;Z4-#I=K)L=0I(^$vTp@J{0@S+Vvds9(g)ypji2=fp{S6m(YN$}Wv zdbe9rBxuC=t2EHuBuOiCd&UvZyahSID?g$HpK|$Dp>=DRN-{CzCXx(zZ|K#Ciu!fb z4{a>R(yj)Q_#IZ@*(*yAi#4AQ{xRvgxAWIO>$HFVcZcI=(IV4zKcL@=h@QG6`Q;QF z?a>M7fHeLB3Z-^~n5dDa8Lb#*8D!^q-Tii!$%YjNAu#_WV5a43v;EcX|L1|q-f!@*O3* zHU0Y~5WR2x*I%(nr=uN$r^*EFZ#?FTVa{3)7eTrRYuln#M+RImVbN>`R9gjD5%tLj zK&+?Rka48Myu4Xe=$q8`=y_hJccQB|YoRR&_8(!5&J2l7-`ChpP-xa@DUg$Y@xaz& zLrazK@us$UEh{YDw)R$tVf?1sqdPu_zig*$orZzi6?hqdl~>ChQr5o~cRKwy;P`Z= zFT0ed-kjy(E1kq%tx{9BOL5U9y;8pWf#E07lecy?3VrXZQku|kp7~AHeY&;sg>+46 z!_7jXym?onl>PEuewe~vBx4#(7w$>a46wjW25Th&;qM~Drvbr^8k+x^Ysf9gYr%uc zk0`N0Cbp`n0FgSuxQTe8u>Ptf?So6 zBYn|aApVHH*8*Mk@m_5LL_3rb|9w@+d7$2@?DdFmiQC33_1~C8lal;Px|(na%4$j# zCJn_V)CrV_oVA}}G!ny5I}4%&S&vaPJWgddh-g13OkqQ6Q!DN-(0e-T%)XeYrhbv||k>_=<-i3{kjXz^9+_XE#L2r{B2XXswUUwNV} z#SE8hpmh)rcTPU>w6+vgwU!ptV!7>_|Hwin8;c2N>LlapEy6WTn-!D+^h{~q_bE;foK?k#>;L8~p`D1b)22syoH4!{|K4vh`L(O4}KJ^@Ic1#j@EH zoZ?734}z4A^D_{58wjrYt>&V6>&?k!c6$P1Yj>fMQfF3hwf zV#+3&6~6lVt}^=N3;ReIWs)1RETQ!(wSKQIt9ox!URCfgHQ=JC@Z6+7Vo@Q|Fu40@ zP`l#BPDw6C{zEahjFv|-H=njgvZrpb-iP+1yI3BRCSRAu{l{5qX}WeA@0%Y6zYZ63 z-FA;E04w8+MmoL^N&888YI_KunA-X-|f%%(H=S>fj+_%Yfw&hv^Q=HFR zT1%2c=^?&~PE&ESJPbcfMvsm^9HqGKJDk-N=eD@6uLXxntoQ`_n-=mSVgt*W5U~c| z#|oK%7h^g^fYLNDh4b4_@Le68e<&CUzmnIXMij%lYJLsVGf0AxHGQTh^$(FfVMa`o z^}FLQF8pHHyj?rtQUytR6siIsgKM{O3?5wEX~xD+GgJG#3fq5e9M98)k4x#gQkbiU zb$%zSjGRTL&-7KcH7((;Q5*TU7DjWwoQ!_^^&Z~Y+Fa|(E0f_`DriSHS83h>VS(sb?L}DHl3CA$lSQQUy{%FqA ziiH2;M{9uzsn&_FF|@BY0g@Nn+?L1_B4Uvu7E$I;w4cVyQJ`jfge z!g+r(p64)d=V9e24cKm$StoHlz>BRFpr6)WaRwvbGyl-}AS*XM{%5+=(A@iCBU8;U zV$LTBd~Kbb7a4wy@v}K)IqCC?2{t9B-uUt)gRxlX<9q8o=)hNw!5?m$gQAP8Z>e+E zh{-kxDUJ`xdoiINxSu^wgSz%)!aG_A~9$4 zG9uS^EZ2a$0NO7vLHg1Z=y*~fD%3|w$-8p|g13n2WjwCjR*a_`;I=aOO351Ft}r3Q z>YRCOF*5fP+j$7>d0AeLf$OQnTFU5jUK#D<+O8;d4d^fC5>hj6()rPxTJ~2vqn7T$ zD91Cw=iJb2cD*g8PuKuI)pRMv{~@VY(A=8t4zb;HJ)-45@9J!O8yxQ((7_UTeHER@ zYW)}Cb+pxSU2@rlcUIcAPxs=sEADcMDj{yDSm$hI(Jchr48U;ah(eA^>c|TOWhDml zQYw_m>_MIe6*-PrP9v7Mu%4X3eYr(7F`@G?mIF3! z3GPH1Puc^12Mn5r2hJW^O4(sfJozwgXAi8k==zc&8Y_MQ$@Xk@VsRV=P*rq-ohb+7 zWCYN5jF`mkPfqwMFQ#4|GC*E3UmJx&30OpI|vI|8WA$HOIExcCp^ z(l-Zlh#JeHm+|q^6tq5I0oCh~8(7iMsqzNXHQ>JNUV^P{AtJoWzp8P#ssB`S)&t+R zG)rpfweat^(vwDydCiJuqmHD1u0*zJohrcipgtZ+4A!spfMjeiuoF#CgJR7p8MQDk1xU_a)}v{hkmey5i#n zW5h<uz)!VXV=c&fg=(QGJ6ZkDL`535?~0LR^a+ZV5~`zQ-r|B>%FzRk zQ)MH^nZ(!MBbSuehWMOQd8YsI1SEJGqx#2RL-W>MEC0>b`E;(EOu4d)?hrj;o>Si$ zPaMUVM}iCoVDINuInTZT^=r~ip5}jgmNfp%>wR|kM(Z$p4?d1*NNl=|S#g?c$j1c7 zuQS=KEXCpvtTT}mobdaoEnSqbn#yTI5=L~47axq`9OQY7&k^O%{P@^x-EmFQ(d_qg zgJ>losA_FC9^0syxoY|ifn#aemgdLloZSA%v(g{_o;jXrYC2s`LoZpYJB1c83hKyKh!^!DKfGD`x!xZ4zuCFP7-ygs+W|C?HQ-c)F-&4teK z^F0B?E~W6}r+hzM4od31rQ;?8ZKhQ(;fEGX|SDQR0rU57GKQxxNmX zc(ILcCxoFFNchKKNrYUxS6}o<<+$8!>II|%C#Q33X!q+gOA&3pa+Q}1ek)c6pWig;fEGIwJPb}2%+w{DNJ@%n#=V%RO7KliTJtYS7PD;cN0F%fdx zK}{bO4$i9dGHl`)zqdJ){Q|BcUfNzyjQ|i$T2Z^C_fhWI(Vc|B_PRt1Kkc!cofl8Ap5t(eP*CS!&_Nr50 z?yq^r-#Vvt{F-ZQzEvn_do~IG^;SD3^jhJ6HCEC#`sicZ$=-(WR6DM{>aEZ3ymORl z+*`w7JJIdgqajW6+M)g1u)CS2VSw+IV)~)wA(Z|^{o`r)Hs?K6%|o~P>Yr?%uoVBw zW*+(3g4knAB`PTBikioWe=ikB-;6a|(hN(aydGcOrp9%bRD=B3(>hzgC*w zwzF@e=j8aGuC@*i-r_~QIs?XfM#4~U8HR6}2qTwFkUM~h;C?bSCHPw3x?IG|q#f!` z7Q9HLZgImj>ay!7+gz^+4^n@HRJ(a<>u$~+B1zY!z4Bb*YsSR^P(f~HsNlGLro+Y( zyjd&yrdfQbsOJ@Y>GiPl<+bL8wiE>|>STp$01fq%(}&Gwh%h9gr^wT-w5_G+p)@hB zpD^GNv&&aOTa2eV{rFq<@e2SfBbg*b>A9HdTYdA9#_EL;_`a3VdE+Y!!t=ZJFVU;d zj#V7q_#RYy>6OjAo&V$ZXlQg+y+7iYDFDRzJf4z!o9X`O?Rb?4*D)u_zH67fHQ86fDq z4UTM`%a1D&V-}N9Z-c-V5Jwb9$a^z-$ga@!uARQ8^w)x+!Cxi0YQ5`VWiBij3fRl1pF=wZ63XUp(o^h0{p|C>yjiY zWG!&&I#@>&BR{lt=M^+G;2E;1SW!lW;z#08KB?!r+@uPD{&A6bzOI6r=gZ@FP)iH@ z_s+GW!=IuWZ}a}lahsFxdm-wpDjynt5IGb&w3Mt@mmdY|K79Xi?i_!kpZ4yX$tQ!! z`*$i=u7@hIH(VCo9>fF$p>Zu2m16z#+0Dk64+jXqKe)^i_XTLAqUrIBY;~#O}hP4 zytjNaNLUr0BmJML*d3#=|1I7>gB6e~g1eR61`9?t9*v?nSKs5$-5*@#4bQIWnwMH0 z)Oi20+20H0`K!rzbgA)iuU!dNq-6I`0>{rfW+l ze~dkw_t%(7Vmx-0(CPnn?D#FY_65Mllh4{Q<%^EUgfUN%he)f8F_`kJ@qK(sW+)cHU-MwvLc(Z1c|QNp&uv0ZHCp zGInKMFG#kDHnLAQetn4<&AS9X<8LwZ-mX}p>nd+=Pc!k>G)>|uZ~wjQtua29l`y-K zR;Pwi#+i+eebqoPVvLMvwPrnf@>$jWD7#w0Y{XC${lcU zYA}1`4eXzrDL@3E1Ck3oWJsWJh!6TgkM*8OBKW#Odn%PYpPCOg`PO)_^&SN}Vy8UW^`;>J2=V@Ho zZK#iyaF@Il-J>S*C5r=%qYKrxFwl*fq{`FHE9Hxc)Ng%>-}?A%+!nD-2A9G+P{*k@ z>S@2s>Wil0i`q94-UdH~%dENWLq6*?Qu?8I++|HaiLJQ(sL<7hRPPU8Yly~K(FSyF z_KM0^i3ZSp=v+0E7;F<89XhD!66xVQta^1)(Xn8bqVMmcr7^scdN6=qGK6Oo=~zRv zMw+A7@RnScHPl5E#*03jaqQH(Jkr5L{A>&NRZB;&4MX#{#@3cPNv6l)Q?u2M<^XwD z*L2PcJTXPZ+wCoe@d~RzT=;~av)w8e@}hZ(%#~Y0PnMA`s&e-~Icudi5%oWU_P^bN zU!tGXsDMc3f>2Iho%j>tY-=oXfrs27ohEJKOlwWg2x}Aj)W)&CHc*4M>gvW5GRzCI zur^N>N94Y~FwfFov@Db43ZjKnuz$ z`cLuaFv8mb@u}ON;0cS6jBtUwsnl4cF|lq#3q{kqD-(bJe+YZapt!3&Q z^``iQYhGS145PO(e9~fQ&*jq0++&J94r5IRemX$k@}JNyLsQtYsO$SoLHTbdR|L)? z+3Orkx*V3^BZ+@*^9!ui`2SzL+6G+yw6L}E+==?J#p-mC1UF^ajw&owDCB)S6w#vg zeH?Ba^LwD;z39(Mmj@3L`5H+*CDp@~+*Oo<&SAk9>w=RLqEYy(p1AKSW=2|pMI5QxJt!dWk_ zt`ddv{JH|J8h0&&t~G{*Y+rnQYG$%-^8a9WWO&^Vx0H*74s%@r^s?7xnA2H{{$@~J zMIeBwoPR!5Pp+(*{+6+$yck-DZo6!wYaR2wms;3+3TSH>`#tiwu^L!iU&p0sD6H;r z*FOtk`?LQ}QgsHdtyt*xB!==&&3qW(I5FLZZ;$v6o*Hd2RS0S&+?1ssXnm|jgluOO<3gX#nEt?jS+;MeQFP4uO zO4F9UC+GFuvwsYUe{%ZbV{iMHgvl+Bqj7zI<++coEr6i$lVrXvC?MR)B~4^A^QB>Z z+gD>UY`@|uKKKo&+QQ$^S9&lb_NVOM;Z%O@Ldlc^t9sdRF1YEnNYfA#J1!0oc_sImFOQIiA+`@&b$K7T}DZGL125tK#~fX~=D zu@+KFZzO((riON4-7J-u5?D%0x1w%R+kOek=@;oE3vxsivCTI+9?1lj36_BK2gw9K zcvf-`z!l`QIG{U_@$0|w-=iQIw%;~9W`luPbJu$EuLXQlLL`O#>Ee9L?`OTYY1C2n zivEGab}JS#ac4>MuZ&EulJfibinDvP#(ntRw`v0G#QE&+X!g2d^&XiQ4xP8d+l;-! zzxZZ*-7Y$1+oA&KB}r8w`?*Eu$LAl#lwHji6hQTQvs#GWynV)sVLIk5O*fwIIQLD) zkFpGnUrU7S%gT3Ca{q$C*j-rK!8QAQU!&qPgu9v@tE+F5>4`<*g91G#SAQoLJ$EM^ z6+V_Ba*NS@OZkM^0(cc}LLM6)|0ueSg_S?7R?4k%8Jh|1+dr1JR&@&dKvGacr?h^!(4MqbQ%5yQ5~!^B6xv=WkymhQiV}^C1}ITr>@HyWr1 zze0IeW~WsRXU8i#rg}T)di*99&uTiKawB;;O4~Uq({YJefTTF6tRZ)ErW|{Yv+CNa z7@t=lMPKzvx6Bb&KgK2vEtKlJ5F>y+GdN=!%wsSe!ze**8M2DkN(WK!ruCQ2E2AEV z7_tJs%n!<75++L0r!d+XK^yk6p7OW%-7np*RIua{?j8a$GYeua3H-_8?HebUp|8jM z!naBBZJV;Z)1#qqZA8o5#Y?|PTyKG^Z2zP}{qntZh`XNRdU19|M5tqUC>TAg;F*#d zZnY*X&LYp7g72c^2LY{SwXE@_eQzaVrar98kd5k$v^hVWf&t#p+`H;XWKnOCNQurv8?x-8}WZU zJB{CAtPw>1KsI>;xHBR3)>Egm-EOqkN4F~bxdygh^*2(8!y|CsQGsNo{P3whw`eKz}2;S%@ z3jySHoS3o~RM%nD*XY#OXS85Dx0$Dp6CbBkSEdp@8#~|71RFl*m)y&1j)E8jecCqe z-vf+8-F`Oiz4ON|-o?E&ge&rr)-5&XHhxZjn`mB`JdS7{T1pPZt&hVjl`VC<(>618 z5y8-{ln=w4S2G4j>|e6p5kDY!r?K8Pvj5P& z>@`-+zNrE~Z@fS}uq}k=y1kU#N%&$-OqYuLNG*0 znIop+gUN5_Fq_kau6rUb#mT+0D=wWmg`=+#8B~YT8znrOV>p-1mif!3Ayi@uQP$HK z%VPQ9lXWBFR>iEAT}24+(wV4~0>*gSHqi-%DeN=K=rbi&h{(kq985;AUH;5j#KP^jnQ`QuDvSlPX*}Y1J*ZFSa`wBn>wsM}<+|h2 zM}#C(+!BjV5`Px3={g02l45}`B>Rs(HBJ?kc0u~XTq9PW1(%(#Jc9=ihEhi!yDY@>&65Ex19kXwE!l8UiH#fk4T5OT-1zM`Up4@XDg zTS})6@?8#z@C+q4K8oj&9XPwj3 z9{O)ExqRThz&{$Ic+%Z3K1!F3h?6GL8=4W;WcJe|74x5X8!~J$YHHNGm zi-0qJDQ?312lu6#kD{#4{R3To3XzXxyP0p#5WszRQM`Y)L+X+{whyz4*z}`P1P{2J zXRE5xf zAU_8B8inRRX{zPM#QcMVY7=+xw3BxFoeHQ}3?!h{p&^ujq}$)JHn=ZJ80H?LJ9&q2 zW|7y=^c%w@71+V8LDQa~K@WcOMKjm1|DRt=jzku+<*E2Ru&(D{3dH|b1XrVFL@ynV zciDOnjmoGi;PbMz0`V%djB8(38IN+D#<>>d}ukAr^@90OgJU zg^uXd&~8;$FE#1u$d-ZvnD2t<0rNyYj^r*%tw zYIZ-qNnM+^q1pF#Yyy5-;KdvY)MTg&XB zose1B$S{NBsr$^hBeGBI?@LE+#fi*4rFiJUzklmRF6hW9gv(9z^M@Z&$KKvuVdGA5 zN7f5J|5^1}?keh5C)TyFS}3Dn+WBm}ud9KfTv5eUdIdivu6e=6Uj$;{_IVJEQ)mLV zO5IdfHo@beE)0@&f8kb$tP1;R`NSaBOG-noJOcucde}`4_8p-5cNMto^$rT!hYfp3 znrGNkQQLC8UOp&O{k55>H?N)4a`UqERxv_0#G^Uj95cz%>x1lgQsH6zbivX|r3Z64 zQjC)0nJ!o%z2I2KOL9M`!S%DAk9lfw;w(U|0Q-`kZ zkb@TU-~mr1b?~%CWz^7oCbkbIH!)GyZR!R5?X0APB*mP-Zj{%#fgLBeq_IZ3=E3Qk z;%~X~RlkyDo1F97=-IV}_MFwe|Bp!*?iis0{MY%HCjgVnpJ88{r~79fE2HAfv3+&2 z+N*gUT8BpTx8^$9Jr76Y6S9kQRA!24>ZJeE)SM%$Q zbCgS2Xm(3@0v{6{p1qvc4D$nDKmuqENY9TCu`uH1OLOfVq%;TFKhxu^__KNhb48!Y zIAzGD4YX zMJvf_*8*lH6n8bdzZ>D|T!x*$;A$#i;xnr?;*gOUWwY<)hS>Gh!}1IQ-^f7-B6yQS zdrivtI+sOx@XNWUDCDu;klP-Dt6q#=HiZZ-x=)9jRy@hHqZTrM%W@W<9^UDPS+`rj zNZ^kx7OG6G5LhpT3Kjsuv%WHb^wtKiVL21s#Z~a+$7{9qTdmZ81G9gd{4wOuWrmNZ zZnF-%t1~hbY*U&L+ms*fi+*Rv_Z1Od2N8f}+bFZvHxG=@TMMdkfrgOcK_F79?=IqCbqIU=P z%OPOopr2u84_@yt{9~v;i6Eeh8AgQ6)uakP1y={maRijX^!hu~L3sV4>QukvS&(cH zBaqCPQXW;b|22qXwe7gOut24cZpwd+TU^DtaXR@$pq=b9mM=FplvV9qT+*QojcH(t z0M)PIrGLf8pWlNcn)>9$&ECG)>kHKO;59Os_E@{}r%t2Gi8?B$sCHdk`;EbO8}Wga zgg4^i^W@VXTI{_1ZgyVUJS4maF@drvqZi)UAGsOiytBML#|A+T`i#W}h=75Z513$s zAHer^HpJ1~CQz(+;Kkd>@4P*u6?Z-Bp6|w(e$lnqKfhOPCnzTPfD1O`A~myGsl&_3 z8i;^md1F5ES}bXWUL=R~^`8)sakq!`Mg0I)li%yV+)2EcdEs~%^1|MbqT!kE`uk~N zy>uNHBr$NxRZ+%|#da_V2)t zIw;u*DG8K^-nK91x<~6i;4bsrFkp@21n%B>xBsoXsA^I1L=1{KE%qmff~sH`63b3h z7?QWKB4q50yurSOE}A>vY&#~%%NCYXVN5Z`?Rzuy$fn8VIy7w%3q6&ZEs7Ypbm6&c_ytj%uM?P&++DdR|J(~*9{NK&TXaa0l$A2c^PdUk}z7qgEg!FYilwG9>T zJr$XGxgzx)4CT?-*w6{qytR?tJV?Hvcc)~yrDSfPh)T$HDnz!5k2}P{UJatpN&fI% z(yNBVUU(Q0m+rZ}PJP&iIny#$MW$j5F!VAesn%JPiV=4>|Ftqd^6>Nm!`fXz%?eWc z%xtr~P7d65k@CAWNfcznIRqn8h*_D#Lz|r5 zU9W(+846{kl+NELnVPT=)4{e;kqv|#-FwRTvw`esUpwcA?Dm3?qc`G)_5}Ep41_N8 zX~D*N3X%7ImstT!WfM)@oB)BKj}` zjF;7Q4~+#I@qDx8ab)j@he!ga_SHkDO4E*Cl3XWUiDyM!jz6PQGF#J*8M5;U`({ss z?g{bPu|qwUCr<9isqX=cUIPz#p-3E1Qtq|vY%QFXHQ>eeU9}3Gy%|(*ILwLjvqK-f z!8nFU3WfVewo7)Ft+zDTjm&7c(i-taTVKmxZ+<|pERCFGE+$Y7RDD%ljI#Xo?WbX8 zQXM-ZA{Po_2BR$#lgg%q$D01#dV}aG8Pv!(wA$CW$}~a1+EaJIxI>6f{l;8r(ghu$p`pq?uj;`TpYE5agCe5?So6PR0vU+ru3!ZyA=E$DDf^L4k6J+_5n@__4pCy=Qr_X$;O^Dxy^>QyJHCJ^sSH%<0I@WkeD*pliiPV=A@m5cLE>!dl&2}JPi2qZ5c zv_Wmv%b+-hLZ{WJ2huB7nJW`zx@+lay(6Z~E$)g@Z%>3Yce7#GGo0A(b4TR=)jqFKDxc z$1^{DreeKhUJJJgyDT9^4I}IW;xC=gC3pnoLGu`j!qn7Ug$4ef3g``#BU4FmLi6*O zFdUvKA_0v9<-jL0jtdmF6Ksx)x17h0Vw)b~0lX|{A2`>^*jDA_u?x36S<=wAAoNtA z$n+D{L|B;2sRB-W2JuGEK!oa_i*zFA)5zMD6bcba_zf3p(rN8Uwf#L9vFt6ORq6u& zyEmu=MLCRt#Z{Z8>KApTd%}*6712pz@{YtC`Sef=N;Wy|_z{lCk+8NSUn649-p0cC zryMbQlPwi=^f$6)I*ObOOrFa^wL9ge!zo{W8S8gBJ=Y`dh}R2Sda z9+#n@cQybdnzd94!X8_;`uxOUBQ>O9Be%RLFlRGJW5D$^w@n_BSicbosxUNYgQU20uLpQEakGsP~>PX(Di5`^%!2F+zgDV{~RVtm(L&8Bg&=fKya4Lj6L;WaJYff4U4B3kw>fd5DT5oh5wT zQ~9H4QYj{?0BDqKW5-*opY2Z#=4HU|J{}cm$vq^Fc1eZOq_=y>9y$ieS=m@j`A}tE zAITtQ519Z2>QRn2=EOm~=RMw4R5Ye2Kdl#RKe#4A@)F|NK0zH3t7O&V<91!vFMlZ< z?D)q>iBee1lug~c+g4`g9u7=MwG>l6?Ls zj+;&Lu1HE9O$EVjDD_N5r!Dm@8*|i#Ql55!Mdj;am~rnVg&jCu@e^MX(cW`!cp5Rb z%^F_(rMJcVQbpOm zP>_FC2B`Oq%$+;@RFREH*Jh!s9gxZOO^K$^YIi+~G*u*kzx(|5g99FN(oeQw7v(vD zdGcS{ub^qd_CdAH`Jy$BMGMidyC?Q+UJo>hIL`UR=2pgf%|DO~X3kMpdf?&10o)^l z#J|U_6iVN^_R<+wao9-ZM@&Q1k2wlba)tfEj?jcDr+Ku z2NpW~fm@L_9Q8^)Q0r7E26nTGnhr42& z!vF#f(O&E6x^;5scGO|#lkAn7!{C)n)Z*q4>yzg&F6ISudpm|h(p1OTro&E)hCr!K z53^aTozd!qx`rCBINsoIbX}BzJhqm+GjnQG)Jh1pEs>kysjbLDX#7y~ZCQ!|(x8nN@eVMd6=jrO`V-dlup;N++pee8!K zIeRGzKDht2?@=^ZbI|VUHFJ=CYy~&WY{Nte(8O($o^US_hV;`rfgdd^c^?9~uM9&v zi8H2&e%keL&Hn)ZB_i$Z`Fr<^SKx;+FfPGQZ6Ps&TO2N4whx zI09Q*yB++=t4sGdRv-=9VQNKyC2peZyORhJ7jMKjEPWdQs7O`Dt5OmqMV=INJ48+JhGCrY4iKePuCCglxSIgMaJojjaDX* zn|J%`Abeu|;SR*+6qr59h;-1TqAgYLK^Q_5$uT1i4Oca7U`L>FAkLvk1=3(KA>m4o z$|9%8%QGi_2ZG|N=AmA)W_I=IC?={b^s}J@Gvco%RDRSbi7E^#o`3ZEHr3*i&8~}1 z>1EPswd7sQEucup0rhBCO57_I;Rb(@2IK0kwSmBIIy}TE$UlSh;cN)C_FX^8CyJ2C zf&7YC^LVd+;%;$-J|w| zc=Opgzn~inr-MA!_Ine?sI`d?J=oc<85w({;y3!~dYSjMKTv&w$Ret96{!!B%s6_w zXj}e_)S=@lO;)xlB-~|F!&hlfRfFT+u+y1W6~Sqhd4h=3NMr?R<1E2r! zm1wqzJhy z!MgZ{w?viN+GPbhXEk9q@*hDQp{e^{6mRIvP>gsVpM+vgmW&#$j;2km8M*Y(@yw6U z<7hTN5uF2jK*Rm%`(gYROI?X#Tx?QwMJZobMR)A-ic0R2fh9@5xzH8Tvi5ahK=$mf zCl)i!SJ_7SbnXY8bBx#&=W-isj)&F+<|`Hic17cq#u)FSwYmCSA}97JCb_lx5%2<) zBWJ}v^zCL)p<3p+2<*-XSj@uJ24d`g$l?E3WACuhaLp&W+)!~I`(qJL@iQiw zu0eOrl&_#HvaO38VAjIIq(AYjmSG0s0-vTM&}QE0CE@CFFjv5ccRiXvDE_?)dRLi5+3nzDx?T#YxS)f~>*fFx1|)9u3{ir&rNhGC@ShfKb;Hbc)*0-QUp z1yU>6IBiYlb9wM?beJ5K**W4M9{U5x43`Jed4#UW@%kP7dkhnS`SOM}JamfYbfSz0 zU2|A~==53#dJ4~=BDX2|MsmyKA3(p_|1=7jp z)hB`ShEl*`dO*y$?@a?40GFy)yWZp+&<~Kh%^ZwwMGjSsKfu8YiOby}+G`4uMU>WwPL zWlXwAm}yINbd^Qs(jS(Ys!5o>v>*}^m%-#H4k?j4=`oLth*20vC{7&b25prmJ1VyUta9XPmmxF1tw=9nY_Fk?RKFmGO0+?Jj<`@6WGG@$H`F7zi89{= zB{|7#v%hKe!_!t4*0r&Sr}?JWNu=jXCi>G#8sM5bgIK`<)F{NgS+=^IuL~Hhb0V>9 zb>A$Wwypm=kE(E*2F#^O&<#+i;alDs@Q{$65_`5S1VEg`4l{H zf``jDn-<7kZ)_hijx~Vt6%;Z77&B-}OwKtjpuQOp%5m!9Hwz$IaCDepwKzqC4V<5P z{Rg-GpMV5e3OU@(^WuN=aO3B8n&THmbr*Nl6hk~h_S5*{)j0VB?>Bq1{^j?0(qdoc z&fb5Zs!|a}5#Eh_6Kah1_gYHqi^hAH`lUMqhoVeOoXT%xsHkf$fO4bz_XC<-z_3;v znE^y2Hiw8l$qBy;C#4?0Jc#t??@L*OwqUBNJQS7)q@f5T^-X`C3VTvvCB?RCnqcJw zeO2$66GL9h(ZiV8vAf%eds5A7&KtNk2b(>6NWlr#7kuk`q1CV$<{qOOcB4qW`oV8- z+HW-bG$vt@lD;5lC+4RJ|Bw!UY`TSo(G5uxIc)u3vt?s91e4feJ3(X#dP;EEH9r)(6!fg0 zhg&q4D#M>%@2MWA1OupRhe;g+$P8mvr=p`%3~Elz?D{1 zn5nsF@P5;fr76!d`bDe>kKoGN2E;}_ASD)|Hu2kZ8h2MzAwYz9r>EN!Po)}$-FnqXWDQ@o^-VSP`esP@nR4%5R-*iP zji48RlZ@MCSFp`UY#z#D^_e zWuuH=p9W=#;|QReCc>O)srXWgVJtFVmRilydN!4RX&`BgQP%ryJhmU0AL$4h&y`tP zZ)2f`w>Y{VbyNzpiZsGM4KjMoWlN!{yC@i4&&|OmrL?Us4nvWlAOYwXL2yGX$Rw0e z(8)|N=M-M$f|2Tr2mq>L(Yi$$;&4d_l(yA`ZgsakNaG$TyeZ%#WcqmDNPCNCz_2;j zyUy|4Vs|JO^e@X9m6`deJV&1rnShCi9iYX1;wxvcIHGy`yjvKI;jOD z58=*f#*_|>UCrjH4mo;`_4(k(zQpv)SoAaLz~Mx>Iy7508|>VOJp@W z5T3`-Or2BWl5y7{R|jDqnH>&kF#p~tDNn3>F2WM7T8Sf!)*-_9%4D|MY{1|=|5)fi z1lwTfC>9Pz2lxs~oN-;P-(q}s$Sas>mpy;zXq9lV7jdA~OmgxRK44r~VvUl$uB0@XF0{}QXgDjV? zJL2$>zT2>Ba~};sG%Tg~nK~e3V_c`fG4oJKgU_vQG`~H{Z)Bvi6XX!V19(iIv>Ruj zU9KLA{380F-q01au6%6g5qe}tJ>g=(-Y}9#z3ZO&9k3&!#(f?TWApQ@xrJ`PbiCbq z(b2e**1Vx`4~f|T!^q+YQuuvVRL2#*NLI)!TjG= zbAev3Otr}bYkK5V`bJFYIp2=6DI&H?V!zMKCJtW*D#_tokf(!ch*H8H4fJl#&$ zUPwi@+ZT9IK!2k^&3+ye%M!sz6if0@Jk@WemjxKU$p-QE%$iIC(+;XbyWmq8dA|MU z@8SVCELW_&tLg^$N`uky34x|TqAZ+BGT4;`k}P4}OB8OhzkinK3I=f6wRxGOT^!$; zH_Er(&*wiPqRu%bP+Ohy=RG>QdXmzC`cB+a=fr41gh2|famvPn zq*=0NDzc{@{o2&NP6OPV#|AOO*@YN5v;xHOW`{0~Z`*;(lS8S(Bp?J>yWf>p=_2BSEC4KKSLV$<^rK7uR;>3Xn()QfsmF zh!qwnpwI~AjvC70pR#=myOgEq*@q~0q}CTX&0v4rH@4{xwUZd^E67%MC{p-;TxKa} zq^Lgc-Q8b|^6zm-E@O;(UhejoBq)fr{W^}PoI*5Fn{M1&=jLu#hqE<&&2CCJu3JP~N zj6G6Mci;uAxlc!NwJTOM)y|Dw9rR{NCwoykVRSUGblU=&gcf{o1PAWR9iBfJd@H#5UOlVOr|nf|1HgAqX(m@wxI+13#alqh zF25;#B@H#MbpJSVDw2J2`auS%=h<=TPq5pK`8}oEis?<^a%_F}D0WBF%+7S*t}ZI% zrjmxKMWi%OsLFVE6M3+9Y_7^q#N8c4Gxn;#R$%zeDuP?O{{RH&q#@oe0DjZtVGMJ= zH{?ixAEZ9+L+N4VrBg9rMzSg-0boX5DJ#>|q|1vm^lmX0Oxm`d2LT zC{LsEuL4X7-m+%^{YrmwBiTJ;K))B4j}X()F;&VB#|oMvz4(LUF^A0Nr|)`NM?aC) z#|q;9wl0Kg0RNXZ_Wzyy*VB+E)Xq8ku9y0{{l)>KKfgwQg}8CGdpqXeL@oORkED#((~`6gfH|a{9F>@37FwIk_cI#Bk!_)9aUR{j#is6M9k8(x$*= zJ9Ul-F55EUpjKt;KS1_a%dD#vON6wh-bUm${=)q>hEZ?-CxO#&5p;8H@X%GCVGOxh z6R*yES?XZkpKbiGpWvyXt57N*OGN2$wYE-#0*h>f%eGp>K*X3Xaqht-pW<})0l?2U6|`&OpDF)-;wC>ZDgSEtwom?HI}>E#Dp7@ z0+o%q`A^HELnJ{81n{xJ9X>7-JbjoE`;Z(g(^PXSoK3>ahzhSdqKeOUam0;XnIUJJ z#2#p11N^q)#5D%G6{M7a->ndc?nuKy!>;}5bMZG$zvEiJv%*ncPCE#dgEtZPAu%$Nl_rL#>{p})OYzfu8Gb@SpuJBf zwMSIk$#CyI%Wm$-Z^1(3Ds(YaV;^k#TS_jOn}Xs8g1nDa-apN@Hfg<*y<7bDm{5~P z+nT^eYhhgAXHJpC2tUh`CpS#K@C#AHSshvnB_3<~-VcZxbpE0F1lwVA|CS6Jnt5bKc0OI@s`K(9afy^L`~K}ZE-|$js=1Ni ztX%xKXms})30$%LL8tu#c#TbfjVV>!4@6OgX^or9R1jMvW_CJia!IIu0@bdy{qTX_ znF8-1LQDt`)uVNRIok5>^h6GV?mAp4iUN~Q?VKK(H_QT}1!#pD@6Cls$*h`jrlth{ zhM)7m#L4bIO!%+Ot#WSgV&5b1IqahLNc&m{fx8+?k%&hqJGw2;!+fGScN)_}>1d?7 zJMdD1;GYl*eTH9^3>*ysq>OYAG7c*$A5F+&DNfX-now16ePmUu3p|f7S{ek-_mj&F zM?lSX=U4g{##JZmXh4R6>oV3TYgiHg*;EEHTqrw45o-w`e8+kj_?A(XE~#*`>`xPq zPJCh(W0AQGNi&Zpr_un9%cGS+Rev+b<=Bl_-T?GA8$pl=AEk;L8zuRz{l4PIy3vqRRv8n1^|C|JlcEryY^{ z)mjV3q4tQQ{rHoNATnh{DuR8u;&XN`a~(Y^wc5rA7WJcoIU`X9nyr|@fYH(fs4V0K z_Rl&{vFKfABlv?)HW1>Iy%%umZsQ}gga3Vx<&y42IQIo*U*%J zdy^=UP={jtoLaE0dVG3PFO5T?y7Iv24K361=G*1`ttLkdA%;yF5QAC}4jxQgvgtFo zaG8PHt(M%w7o_T}>IUyuYPP9v^$5azvuw0 zm$FQGI~`L!?E)!z``WuUAj8+Dh01m!8a&w=U}`H54lA?r;N0!a!QBNiF+7??gUE8+ ztv&h$GH_iek&f&sjkzICa|g25%SX0Bj^!tdUpJ+&0bQoK5q*$H*D`^j!-a82C81Eo zTe*ch$aY zNV?*ZJhzD=WF>_>-@J6h`$7fgY=rfV2Q!R3m3nubp=KIC15;PeZSe25@DaU_;%NkUg(nS6bpxVkOQO8%wtQ^a4@Rlvuu0! zP%B-R=a7srMqPFMVL8inH9PtrBGkn7HjBX&)g?DiN?p&Joc4!qNyK7}VErfdl@n|E zeHS}$Xe3d2%Fc8thOP@kWy>D{O7dxg!MzfbkJCiu7Tj&`L}#m2h&+V2^M}OmrP)(o5&bc5eU+1u&=t5o4b1rG7f-vF=7UHHxy=PfBJ_MOExauGK$$C8!P$Q_hD(~I6tfv+h zZ{Bx;DQHZ&xzr>f{lE3<%fO2e@*RmXhz z_o$FLRP^!5U40DyEUPA3;5ZiOw#-Q)3)DtOS>h6O%v_}Nv$F__(C&|g5o7>t^=shq z9(Qt;2J&ifiOj5;M_#9r3a3un7`aYg2)p6y=q>WH;%8Fq$wPELq55sf_jbO1Fq#y4 za$a22EZ_Yw9--wGVkoJ7qc?@c_l@}~C@EGQm;|{Fw;w)^gF_d3zE>;k@JqHf*Kj?f zUGZ}D)Smb;5(VbXXR;{`Z+JkLeI-!{2l+PD!iMwBP?f{@RAKWCa+8*9u%YX&y8I=U zC?sIl-U8&N<>5xMP!%ap>>5qZ^%7k$P#(Ef&WIA9$Hmxsg5LRyVa!yz;DciWJYWMtk|S3h3!|;?W-yM)cgaTQ_wkxk>OdH z;h~d~XgqsX#82tUk7H}Jtg#&d!5=~9lbG5|X^JH?sjN2zQ*Q>Y`}$H@m~PQykL97D z-2;W=qoT=OwhDgFWfx!KXQ|}YGJ1-FoM2YDo|y_}EGDV(FC}m3FSx%K4jTNR1yBu! zzf>z;+EYI4SUq=I2aU6W)Ws|O<0-b?)*tsc*fbU0w9ML_XBq&E*t=JuQx~bDtC7Hj z`%s`+KG2>Pyhfe+(VT?r!&{h^r_6udbC0#~_Q!|qi33k*(WFjAKEeL@^|U|dyTpAO z&&oMQ^;=!eqiZ`Xw!GHz4SE>D3L4&|Dsxwv=^ti%7DU{v?z~{R6tEwuS!$S^8G2@P2V zjUA9EOkqhhAyo<=bZSp2$jRVf6T^U;I$9}1!?c#P_T`omIYZGWRQf>aH>A znrZKaM*D!-&!4sY8t?5UptZWzQ5Nx~Sz#C7B1ASf)lM*@48a`O>=|~Qe0CU?l$U<` zD;^d|o_OFdP2UfO(q)P!(!U?10k?;vY?C9Ao&=!ZvA2T)9qJJ<-uCciOy-nc+(!cS zr7UoB(tvJ3rGpnz@5ugXF`L-uEqHvVu0WIoQn(0yg8Sbkv&WVd0oWz=<4RX9btHx0 zgy)}$1*g6boEg6!h8a@6*kYXl=jM;> z@eV(fL{dlAjpwii|9lzYkcb5P+Kb~s86zF3ym+$mt%8{_%nZU!`1Dh9)JC~pcY0|S zLkvC@P-d~43+p06p!ClYpi~_lRk~2&TcXF}66*=WgFV%pr6eFl6G|DrC-U(!H@YR& z2*%`ndMh#)Xt>4!y$?rw{#X&y#;3>i`Dg-nQ7y69^ZDXBeCLZ*mdM1j9+OW&-V9GY z$kKr1Mb*fBPG5`ndE)o?l?U4}v3`uFPHa%?`F7Pp7^iM&ZSjI7aqr#S%%_`h{A>(a z|6oT$Rf;N(FjaixLdnVQ6xUV3=1TR@J7u|pSiL|fe`{^CrZCD2 z+4{Kjg)HG0qcF{w)d8|KV;d!un+4UU^#`z%7tAEKhA}ZEXwZdVQAn9Y+y%a5UolaL|xb z9z<{dEe{~j7nMZWHE_-;(+S@l9~!#JGizCgRqsgZ$({?QANsL#oPLrzB{i#xL8Go> zRrpi$r~I|YOkN%tcZZaG1>EU4y(C41Gp)yIF(*T{Yl>68nIp|lv$yd)QzH+d< z&~K@LZC01vp)`pen+`v=7zn@PAv-bz z^z$f^_W+r}ty2Ur32k}Mrc`QL4FbSgr~mY>og{c|y?6Qw%J@=XwH(7frwtYpM?~tk z#~%JqYU1_3+~~h_(VHpq%JeP3U+^H;eBcAI5w{J9s2&q?=Dw@ zi(W=22FBivY?R$0N%*CfJ746Z?H=x>cVna(ZOaLm25$!7a%4R>NIgt<+a($=7UnU7 zF=*BnH$B~g&ahTu1hQrf7PetMJ{Mp_CQX#rIoe*f{SNK+w*QTh+cnf`teV+9fsWqZ zI`VY?IK8Ofoaf5tU)*DG7up=~*997<;5 zHc50+opCpZPo9;qapW3semM}rQ~<>xyqYf#-q9%$@mjjP>CQZ{)e<5{xPCu=c5d3v zpj=RY?vQQsOwIYLJaP#$`)S;nzNi^1(Gb9(0$VN7BV77>fIcQAl z&z{_(`2SIYZ+Xvbb;hqy>tSHmO^gU`ZD5?^x4P;|=n(U7Th(L$xzMe`bqaS-F=@{@8ztgd-@|E_x3AL0 z$^fcJwOogO0+vn^zJ9DCHa=J;5}Cu--t<88%-qhb%1<`7SqpT|5BAt4f8hJ}Zhrq^*Wps^D zLqUkevd+6v-^8oViuqPXdGPqNAqh=4gk)Zn1+IeqZY)~yjUNDu_+YgTsinA<;&rx~ zXBS1ZOp%Jk_TkH)TI zYY&|h9%lfFbj6fSJRh+U!-dMel@b-OemVK+aAg$!YFg!ApFWfsm@Rf951*FJwHVLT zrU8oa1;9Xj1<#G4T&IBV>{B4`mBWC;8t>V;?)^fZ!=-lb#*VCG zqolbnp@FX;O1pY9a6~Kiz%&IS>+q1okBdY!SUTftTnzg%sq*0G*EeH(ArZlA0o_4H znVET{7~w(LCx$z)_I{WlvJ;5j9{<-zueh|POC7rNK^ccy|ZkDG1 z%QfUg%s3&D{K(n}=PgNFc8Jin>x#XI3)w(KC%6<=8Q;Vr2R0mu`1Lyr2$YW<@4bT< zh_uf=4Oi(@*Z-h7n~o9HQi{m?B(4PHVXhleGWeW8zz%p5O8%3N0 z(WCbiqbFJ`PM>8CcQ~mE+a-&X;WpXwq03nr5|SD;YMqm5$p`s#SxW!94~$TT)Ei@g;%e8bFC@;UaoTiSf))?5E*ueICjwG; z{kS&nz8ZC9+hwwWykF$|NA>xw#yLthGNJObO@3X@ zzS*CONyb0A&<&Jv)B>0Rt%ur}hz9Ho1NM>ZB$xV-usnvcjR@ZJO?1&1S-#J1{Vq(?)N3IB&zbHf{5@YYcHmn7+$n5Be&vIG@gtR5WxJ!kjB|F6$$ec&N9uzG@ z-yCp#wPiK&2h4(5@g8%O+U_kE7wpV2s^t0FXpa|x!Rmhi3vs&}0@4vJ3HWZ5_gM}+ zx6H%m!s9X?y}=;48q(w9)e^WXId{}+$)uU9Ow z1O0_^ttnr4?TN;9-c1bl>yJADz?B_z*QIxizvqDpTf+*bvV#fwY91vy8qp;b0H{z) z1#rD2sg96XJFc~z?<^d*Ir(Rk9D&G>q5b~S6DZ8GH@yR1AL)Hiqo|U9F~~_}W@c1v zS%@N%MqDyVhNe(eC>iK5FigwWYI2a2=mF14%)~Wkww4*?S8igo3>ivlL29(r8c3*O z{LaQ$WHd!q>4~XD#BwR>>bmJjMykp0@R^vD)TXBAejO6&g{5W~ipi_T<9tt-@zz2T z5Ydwzvt=ysE5{csPY{}m?ySTVoORa6{WVd>H*w9y!X&F0J3HA+u3<4Rqn11OxuY8Y zWwqjd!{g>Ed|5ihz@_Z1FMDL|gHvmxW&T-J%GZUYo#SvXM{Aw&aGNIJaN-({;cZQ# zVNHIT!3W;HRpAmENluOzs#7w)TJx#>@}jdBv#nP{c^=2icKcl#Q4no;F<(WFS(EqA z$|NkFu!jD5sL`Mit!O4G3Y+IG(p91RLMl2?evG+A~ z%t_+JD6B4|?5;3*)#vx%H0ydYE4do7TDH=&1f=ts0(*otAwy@2_Hi69IT&UzDlwem` zWM8$P@gSO?G{H=QX2IzoXp?N9ytm_58>QyJcp4U^q{JlJue~V{G}_8MBc-=rns?8# zFIV)6Zw?D@D%IY4EDmheTrwAo-W&oOB5ar9HjVNjRHdpZHyaqZ_glG{OdWP=A%EPy zNsa~=#%EbWYk`lx^Ce00R?QB-T@7DlPZJbz*YSCIJ`CYFvYS=Q)VFUN-u`t35$ySj zD#^S%mqTuaxHvv_uz2XtkGdJyH#lz{gvt-F8L(hWajv^n0OHmvfDWOff zXxU9Eyq~|}Ii#E9z_3bn5ICEUMx^6+Hxa~@eV!e{FL8|At|21o3F@~unJ1_-=Eks~ z1VZZ;{+j=VIR5*Eb#l4Zxy`>!Pz;$>paMoKW_IaSl}*uPNv;Ksgl1O`_qZk830oda z1}Ee4!u;M53Ab(x*1`_xf5_prIX4SG^zp20b2%MZN$a?5asPa~PH6BG@ehT@*9aV( z?UY|Oa#CSo*#$TskNTP!BLQ7f)Psl>Fj8BX1*KkM2I9YiGm3C_OImD*lrqDz-b2-L zqT(*iKkyXU(9E_@T_d-AT?%V>rM?1j@CTwlU5MKT$7j;8D#D~(p(-?JyO15iE8Eh> z*?~#odgLf;#l(&$k;w>CFQM4xdGY7dS#Q{z&+NT=Qb)VE0ewM$HkI3Qzp~c0502KqM;{e~+q%K4B*Ky(2lgvyjKt zvvzogUdO0ua7b@+-$-RzRSu-arLJ-t?)%SStA`&COmwU|e8{#p7A8bocAM3}Udl+= z5CqahXh_9#%zV$m$|R+r#Ihs^j}n@t1l{#GMlpCffIYzcbRz{>tmG6)DqqCi0j#$8- zf?_;s*A+~nn1e(&Jj?pCnZk0%Nkc5x#j<8JwCy!{0^ z_BAyV7Y!Laa<+q@5<6sDHP%ib;4Cmp80=!Thh)^wlQ%T`Y4uxN0)Y(RDf0hY#*1(`$ z?NUPj|Jq7%BPO$)rPgJKv%N)EAD}6}KfQcS}f zQ0-nJ%<52@`|GmZK@S|0;R0;xZ_@IfzjDz*M4`=XhZ8veMb$w?|4=*}_8Nb>qLO>P zb_>}iUvI9yYdzgV%+`-Yd;NJ2CGY|tKZFEV6HB}Zl053hOjy3E8Bar7TSt#C12Ml% zRuJ+(OYH1MsJ>Ltqi1v5t%lv)?ko9jxp-BFUj_Ym(w52aznr#!7s92&pljn|i_s_FC5?`Iho zOkp8y$w4H)l2t5H4|;s8QM@)w|7=99CN{G@UHcCm2BxY+_3{z98_R1w?mv}!m)jv_ zZ-P~C9fnVZx-Y*auPQX}Bz$h-NV!EkW?!!J$gchD)YDr)m0S5Fqxb1-CrZn zCP~ZDT892UJ^-WT{^+`Lx8#k@iwIJjk!@DaI`a2=KHrGr_doiE@UptTT9vSHW%KLe z&N3t6-Y2aGXiu}wm*DHKzkJi~Kfts4W|da>=`}yK7+;;~2bYSN+BaD-1IOTrbqD#> zMST_{!HXom5FSVU&jF=#Ke=bL9O{>vM@*l#QnGHDmpz;xg6gy4j9xBB|F-Y1VSrqh znx3x1{z_{*@HM;5bV!zaF7i8OH+-AX_|q_d4qYH_M=$E3i;BLJ9s%3n@qQ4Fv=@6D z|LZ{*Z;d_Xq!%}&tz;r1#6)o+Zwy($lbI_wHTd|VqUy96Hf|$*%c^_0Hcx3a9RoK! z_zS!D7V)?2`)JI`jNQ-6vnP_~o0}rz2D~U$r~teeu232bB%=%g(#)kGix3(NgEYx* znLLV5oLe)LYdm(Jkj64un-5+5(o0#vVl@a$I}ZhllmJdIVJKGne`gN=vIioVHA~9z z@VqT*kuYMu*0(|i>b=ogy-x4$4D;Nl*6axMW(AqM-6ZqSLY+JkVTPQLXiMBamy5_Q zp1Nrehm}86MM_JzAQM#%_m!myQ=@wb(Pq($$OD*~qCPY>b>(vNd}3?lfi6-k_ih+= zF5)w~i$Qdz*&<9!vlcL1iwPBF#7hPDy>DnlNs@{i$0o7WuV7vEal2`GGHy*lyK3*{ zBA?ZtUejfM%SCTh?YB?BFmiIP;I5S7%7(Zjx4(0{mEMv`O3Q^~@v2KE~Si zdydudOV)xiZq&0{CFv{FoO8J}U+$KA_eI{$w?nihl}RfDc{j9D-~K4A3>Mp-+cXCf z&(^F!KIRU(u82?AqYD4YP9-@vY}nBak?W>Qt1aDZA(|6EXC|U#zh1{3kW}AW0cPhEq-R7+g^Vo8_m2a9*BNpTIgWjtOjOLRT6TPT$hZ8X zRJr8i(VN40?3rWGvU}VIG#-WX$Y?h^xu)$c7C_5rv8S1gd1;jm6bMj;bZzsT=O0hD z97n`&aC7=wm$^P8e2Fc6P%;;lS$(&Z{%9U)-lKT8;RCJOP3O}8?{MKCWQYws702F( zf8RPU1mNen8_TsI;g6ORTAmx z!=qNoN;s)Zm0NzyPDX=D-dJFor-ljj_{!^ID%D-=81HYq0rHhUD~;CHbzbviuSz7J znkygwAi8e|eY-yuo4Nat)xA5D`iOYb+IVz%yL>5qgBwT8l+&o;G0XV+RJf&Txa7%_ ziO3B>!Fu?EcXL;X>!4|=tXJmmHGR9?l~gOI=QiwORNDNJ$$T4|dd<_xce4Km>QE1~5E5 zQ6%8UZuhs!i}=HG$oxr8li5H8t-eU@%4b4n>%B`=S1n+@?iB~bz78S3>Hau@SD!A^ zWtIOy61t>$v4Wj(;G@ge0sEyf)Qt*)51D6N7_(5ZCXgc(I(eVAKv67D*PiT$HXJ}0 zzK(q^l$UzbY7y;Bj>J3+T6T_v#es{&O=s($Y!<_^?jiZKy-jh%1X0lajIw0$NAGiBpZGPyDFuzt*1``T+V4lZ{@xc};5Vd{p250M&u#zmD zxR_cGu5Cw(rOCO#hVH-tY~%=;^cBY9r|Rccsj?%!UB`ya=yiv9G^q9zM-bc1)nsbM zNYS>7iP@>Bh{ctCkGl>=)C|6rGs*&@p3YTp(|(&pIpSW`<0jUq+|w%M75$;=Z@mXO_LulO`gvElB&(*g|4StZ!}k;p57ny4&ZO&RDt8#QLW)y45>GR z6<0cKZJA>~8D;gY8S!Miz0y()NoHkcn3dDfBUA=cgwRhTm2_D!%Y{W>)e8O5I^(PH zn88QIug>g%)bnNCu`YSJ-5ehOVQnVkJ$#;f%)TvhQl-=)QBJS-Jt>x#9`zuxthZ%0 z8BUxiX4`l@9IoE0Sh4%#G4ZTvy!7ti-TXU97mkkTmvcD0X0yeSMG)_|12@7cF;PVE zAz~~<7Gll~icYHaa4@_2za&M7Wv&SfUHHjD0o;0EQ#Z1B@R(Gb!?6;5~7N0G7ws5-?*t@yst=%S?R z#I*8Rtl@Wo+;kCslU7uw8zc-xEIU)XdaFqz&3(4Ke9kxCh^0aUS(KlaJj19PUbFQ3 z9EzAG;XQR{hxmKUs}j|>ib@zVZav59(r?wQXOrbCjLrAL-vfHwV*p{hu9_Olr!D;bdfA6LMnH-hTtzR{-ryoH5heGR)wPu?Gn2vC z&4`sWr+{|%>g^NSZE3|Plh!qvfiYK465V>Q366nESG=lR;ejdJ@o0y4mG1ynti;Hf z+gvW|!|aWA0~5y94bC%#lDUe2N0-~owMGgZzAttan+e1-grM6=Ko(nbAav)uvYxwc zpgmG44vn2R6~?8a| zBq!eWQ1Dcd)hIa(&zMK^(v*@FSWKC*oe&U17!iUd3mX457JLorUXqFNMaPsj54 zi)A;#S4_A6CXu0pYU!c;`+t5&2UVQ{fUwy6cb}|hKgsZI51neMkB(+B+|PS?*ot*)p-%1RC^+b_@80D3t7em7(8DRU-_0 zsc63Xx&E#9_Dhm`ER;g55_~U`#{`S;zT+Xk@>Li0cW|*}afP~UndNgMN=v4cN+SDi zp#d)4kk4CcwB5-emeIy*!0jmqZ&ZcT^Xtu<_S#)T7fN*B+L~cJoM#$5o{mWWXe<4B-wtBQ*6)oC+;H3b61du8<53xMv{W~Ac~LjTvIE5QU+c9(@&AP z{c9UrZ&F%KgY>Q;KS^2Y2S?bj{kP1dH<~{+8CCu`4%}eMQ`|T%ZEF-S#gjqmX*dNO z-d@Mm_e~LfM&|o}9+m4QCohf-Hk+j(o0HuwS3%%z%b5_TB7M_3LJP>fRJyHr-P9Jb z5C9>RmuTZ4@~V@agf=9r(c}?LBIeXN8O@Pb4uOXRM(Cz9nwOL(+!Ia1?|nj~xfRFg z>i6{Q2WL;j5I|6)C@6PfjQ?t+)n~s6T7XD$N-10|vJm_<5jBq~EiRunT~C25g!q1A zHj)rs4KE$RR`pU}GuSF6Pj;wU`|{|c!DsYADOxK=Fj`8oA7B%5S6L zYAn}sNsu1$hcf&tu$ygC`UWHZ!Z~9;UPg%_Ey5iI!kmoG990oWo7D^tl@aB~E__4E zZVDlC%+7v_ta2h7$vU^s^VBZqFqx6sUdKl(A@d?Bb?gELn~7*OGu#wQY%7LxUPC=y zwahCH%DuWk5x>+ z>q`QDz_z?c=9+d!`ZQoNib)-kZzUzc5*xOp?~8R{+TrZI2_(le60SR(nlm<97f?Gt z(z7`trBL#RjIG{h{U(7(F&f$YtYKFCs!0FSGWuJ~oVZ?P1TbQzWHZPqb_6H4DzOI zGGuU2kI3A4uL6vbPthm~?;_~qkSYyUWn>&nO5KULRFltHf3aK|&Ay^OJ6k%xycRZ+ z-)+mHzVCSS@9$6-aTIBDeCQ{{x@s5V&mE9e5YnoSn1Swo7TVd&iigfBahttM zI*z>Jp#~0Z#PWirufbEr1ry<~cRry7JJpK%f_`E7)+SrcggU%tZsF9To)*K`^bQGb zx4Q=uv*_yKw}{yO7HjUzj56Mmek9k{C5i=P8NpHn=s}nODPnYZNDR@7ahUn5gQMW+ z^0m58{lviI3QGpz^Gg);;GBG&ZIt)w<*!K~x~et5ibOuib|{ZTneKLMrqaLw;C3^5 zqL>MpjP-OiXiMHf@>;kziGMN%@vDZ&Af1!XNtQwH>Qr3t@hqaw;eziL9PQc9FqHBgq!zMx}&X8it}LpYn!V(Oic zxK>NNCPH_$sK`4~+)lMV%V3wX?OdvOS$HwS{Y=BzPfB&_-Q;NxxytkGBrc_s+OiaCRYGu;8RNzsvr4nKVc zxre8|(kXeK<`wf(9J>kSaM~AT^M56|ra`31${{}y^>4qb|GHBgynW@wzi=Ue6Xh;x z)S{z4kiCM>#NGJEk&6ktV~wILd%2d;5CaX$o1g$d1QJ6)j}knLkH_ z&g;@`_v;z%f8AzX<-O0coyOUxMwN!TY>Vdx8`PqS&lPCQBuudXc?CXzt_*^Hi8slw z4gF&9NDlq8-FyX+`G!Prt~L_`q~ac&)O#r#t#bMFW77!_>#byW8qe^bTiv~8Q`!$F z*l^ogVTKuGTl6!?dxw37hkg24zcYN2O`hcVb$qAxz7>KvWiriL3QXD|qLX(isj z-j$^QVE8rbfPuWWyGZ$AjI!npZKYCE2IaPPDp;_}H<$Bzx^~%cw&hq@n(}#jwl4j{?$-!n_T4 zhbiW@P2+_ko$9*DN^A=7kV)uxF{AYW-+ebP0OW!5Pm#jygVkC^h%}Fs(jA#VpWkqB zAk?WRT>1L-uXIIX=525Cc<)_KIyJEwG`YX_+~82}`S>W-IHPJrf&NMPlr;N61xz-xb{?hD>7=lSK3ur(811 zQNUzVnW@lLL;=$+d!o!3qEQ5>^pMMW>nVg-x919)OUgZ2N%`)MC#0JKJOOWw=&07L zM8`Z#UC>F01HqV0xJfTx*s|NKUw-!LYZ>x3@AhZ-5we})S_`r%#A4N6CYhhDwz#1xpgsz8hxcn3k!wX~O` zEa%GAO+?gJ(#2~3jM!*k%O0LM!8#_na=E(GR=AJM-c)dA(Cqgxc*FU`Mz)4Lywu?M zaLX-^@!I@&UgYFRvi5L2oG=ppC7{y+61F?z;i%(s)KzqBefO(>ZR9&{3U+{T*KnuD z)&g!conMm`u(PQzj2tgVu)*(mygID{)NHy^*Fe0loW6_uxf{bG#1}`ncTH$^*18RM zM<6I2Kq9#2we=YBcVNEa)oZVib{*JlynOFBzb%@Q>H>|dlAsGbZ^&~7n6j9DTpGGS zer;>;-fjj@A=gv~8ep{${_`qgiuB)?nuGD^59lH;0aWCs;B@84S@~zm)fg@kaSOp~ zd7Oi!1iIW9c0w}~LGbi5s($^1AgPoz96xgoUbE@$wV#NeyV=IFwyJe{3p;Mclw_o* zT;f?hs#!Nrd>)PJSJ|XVjv!ttTRJRugA{sGTqfRRJ*;-saYxOI<+6yB&`6nPo9d*_ zQf*PNIi4*|VEVXe1x8)oN!J8tRyRkmV9xdBnFYVFG?OQ)>it<*F zTl{``uFO+SChUJ-{8wMgsNFYDPd1Bx$l9PpT_?W1I&P`)FO$={P^0hIY;|XLgV)0F zjXs`o2Kh(+F5X27vI7urMJ`PK7>)32MUu?izub%`Rh7k``hdH?;G>#45L5Wk$ zGoQErIetd)Ck)j{(USym#6#aEv2=ihMWzxYUEmQbjjOfF^;(>Sm>gDdem#t6J|0<| zPKi9uv5S}nLa`bjIpYT-dgFOb`~M>}8f^~V$DdQ*(O+Uc(qM26YbcULPgE#`wXDQN zp*idDQ-9z0L}evk`snv!GJ4b7CwuozS9zF|Om&7_9PG-&Sr93$z6;jEYcs>AbBqY7|3Wnb*6~fB-E@py3YDZ5Djk#u?rG9kzoKk2y7( z0*>@tvA`hMcP8Nn;&3T;-6Rb$Mnj+~2p!BYAsZL)JuNd^EB%s@jiFJHzTxvOtvw9? zZp+YU51PMM)$c@|C`?*weAi(Pibb3Fsj?rmG1K7DohQ#~a$z6al~dAk|9i*oT51u8 zrRTW{!XRUo?DQ;B*NdKxNzCxGj_k92r>$M-^$Z^wPv!UXuT8C%_^#XY0YX#qM6L)BXYQ5Bcf?k zW9A?2h`asa#j3@`2$#{*rL}Nlc9&qw$KNX)=&NA}?IRq5w?RHPx6xuO=Q36?qWOsx zjZ*dHRVFFSgOtLHG0ysimz)ES3sUC;sx2)EH9Q+a@%OLWH^C? z(|x$ltbFsp2v7Ut_;Nz%`kvBt#Z=7vFVC5t*X>aU@=V)_;J~O3$>YdjY&p*adhXV# z(7fI@aT6xzKBoDdGw0jTmwe7_D!k@DaolyN_hzet`_RF$XuAwYz2jH=q9|JY$qOu` zh2jw!ETM^fut}VptUk;vkM9~yxoA4Y!T4#&hCx;J!-Vnc-?U1B&K%*Dg|QWrhcX5g zmGo@oz|f?XgTmiW`>cNf7aSn|l?^sk$v#d{gZ+7KSYVp#TkBXMZO*&w7Q0|-d~Z0#rVwKoHwur??KTULeL z_(#Ij93(}+E_6V2^VFm&U9VIo#?!R@pv(+q!B`kkCfW<6K7bJIEvl#dhON?`qx$43;PMKNP9@*Vx!R?NZUae@~zGw|B z_aL|d+bAV|uHhR;wZU)!5>xaDO5*nzUn5I@cv^0rD_!s9-h%;rHlyXID`1K`)KwUA z1D-7whB(`y^=x&{H**XRfeCewBI^&gBE= zX$k)Pw^Vv5=cyl^;m=mT&#*0aK7Ny#t>wpnA+>L144}(2e1p*|50S?Q-hn#3oR5k!j9RIyyZ|#LXKt#N}!Zgo= z?UJvvT@l+`R*p^=zmg93{l668N#CEmIZm}0+D)83@7)fu)IulSbTq3aelP!Yp(Q(P z2u&Hq4&@_^vDp~qu~U8_l`?=UHQ@ktmZRe60HWQmaBe$-HzcNJLmmFuh`uv=Y)%gG z*u#`qh?U=BQg1>??^PKrMjjMuqGKe|Ty5TcjQ9?18`5N=tx%`7l4!t&&J8sh)}|Nl z2qJa&y?2QHxCjpXFx;#GQywod!NevP%2G~M{+XUt`1JP5TD#g% zd5Udc3mU`*EKTHw~2#IPlt$!HGuXf zUBn1Z->ikIS6MTag!o7@y9@3K#Fnf<1S}S0$mI%H=js8XIY3GJ8*C&Gq~mvhGWZ6$ zDseFO6(d}VrGZssgrX#j=hqlDn0B~ED!L0%7lvYup<)lEihK`)94t%*I0g3<%yXnN zY)pjrB3Q#)>XHgQJh*)}vk(h@?si_0*SQe^o+|Q(zn4KF;a}_*+ng_kk#UMz@8cRS z(Y_#qOzKT`7l&Ni+r<_BwoB_b`*vJSwysHcW(l%Q3XEFyD3C7#J~V!=d9~Ooi{v5C zeLvp}m-%DD*|^qpZ zu#4q*t&Lf1eh|f+B}NHsc`6|zW`^h_QhiW@hNn36|2yin-Q4_75{CDn{z5-4qYh!j zpaO>6e6lXb$|u9S8t`#2623W)Gd5hhdr7)_`Z<0Y@yDI!X=yh&ZK0cI*X9H7gEA(7 zLRJzRx}96;6LcMp$sRRi7WpvN%ql5)j=~aaG66u0`yL%&v@lE&h%u|jH1kQiolO$P zG1FF3#WfpkPKVmD1aFQ3UXvroKnHm;;o@5$v2ei;2_{G?zDo)G>ZPxvBx@5@mhuE{ z?InDS;NsTQrAo>!ALe=J1T*$$`96?K4F-U;1XfgoA&#nwd1uOnAo+LC3I4+pThyfy z9j#!q=iO*RZ&tt>6e1Giv*>+s?0^4up$;N#m*Rf{UZdBicXS2pK}63~I0dRz$Nj_V zQ^RuFJ9s`#FL_pww)%5As78du<;Ok^N?13Y3f~F=#^_duMW+3`Gb3kG;ldT%2?b_m z%$Wfj&E8Z%29+Y}o1^|uxsg@}wy{>N?0IkzI9zMv#zJPl{=r!&@Vz7w^ur#IQU=BS zHkI)HA4JjrkwW^PSLFHCA4KoHSnM4s!r>@Ij6u)wXs}j%gtQvNPKE#jh!_nAetEKp z8R{Gx6G6XjK)$WZ*)->L0u2PA%lp5W4uu-pZC8=```?H@7S#VjZepze*#ghr)?;lZ z2TDe1mJQsAp-};rFv}_^rcBo-R|GS9$fBpIO%!t@A?=F(aIsjWYsr%ot5`110aAxx zx^eMDl_$BaifdAf*QqcamIGI~;+}go0xuIdo1Pp4nTxAFT^c$zjA&9A|<@)6}x~Jgi3WYe+@c z)K^K*P3tJ>dPh2rEJwu8c3?{)tI^@bd+x}FbMJmkpGerTEK570R64IVY2wr3xX#B- zd`FwVUr2~})?QSfcL6i)RxIcrat^Qwwve7ElMGA;gcc#s*|40QD0k5{oWI{JP8YTV zmOF`9@b<|Z5hx+wB&^#?t^C>H1jx2yDBUEX@*9;R;v^e!I@FfQ!&{;GSYGuLZg)t( zRMOj5J9nA}lcXYn9=eZq&JyzqY!y>vxzn5dEVD?VSOXvDvblxA$!cZ&;eDhuwe`SL zf??_}G)fYRXYwRJa5?OpA=g@QU`l_uB{$)mnV#qp9%vCFPmCuuX$EKCi3kWS5((4% zcS`a1?nMITK-aapL^N9rb2#x58c)MZPd^~Jyc|%|xzVBJc9gd$r8otlK z<`f+T0n@V4{AYGJAVdqGxXtPyfr^3*nFO;~!R>AnA+P{EV z{7VjH3v5$`XE#3xGt8wSE6Vp(GtoPR0KQpluDlSu4SrCdEC)%m?W&;i&I;nuCP8Ov zrrt&AVJfpk6ySFL#HSB(U!3Jy3^p_wDX_jEToU(YrKuWCt3lnAma;Lkd5!KlDTXJU zQc-#=*h%Bbl{5x=`5q4cdhzk7e%ks250wjpc;}dyr)?gCyK2=9{Cb%Kxq(L1q=M34 z$FGqhf7b)vrsrJ?H@>fMF5He9V1Kcx3Q#GmS8iq*v>Mmm7UNUukYf5W3O5g@;P$n< zW8ypUor>$iXLSmoU32Y|4$emxv<%))YpCyzA~`W3n4U83_j>_+fG!>|*6al30?s}; z@&DoU@8~dE{X-&PJJ=f^v7y7Jykg?gENkM%qP!BU1D29WUAG$|rde4lS(R~c!Izi0 z@(jo~DY~(%pv+;n1}E&@-`d!EZOzZLG|bUBY7p9821XA@?xvbBZMA`d`3uH8y&ye4 zGXy1)n@|vNUm>9ylGnxm!)X>=s*Yld+hfbuNu7bD$%MZ!6w0ke)|wh(wRQf@Yjhe- z&e7M@3Qgu0uRtr!J|XQHymK+L2;Se%46^pNDB+nsjIwaB-N)yRZNE#d!7B{aj&yxS zir+nUC9fY!1n60_SNvXAbJ~68tM!D;r!8_LWa2iRpDvQt1{B>GZzwf(;fOPIkA<@|1xz zrT`Fkfcp>q2tv#O^NLVZkSFqL9x5NX9B92~YDSD)?LUd(&Yc@Ms0VQ65AH@Re53(- zE0l$B?;t^bWVVK;=M9#xU5E2VaPO=~^Aa6gC-EBcIK6d)4?RYWB4!*WqdtxkL9Hd& z`5!=_{O|axm?f^08^9k{G7Bc;@AoC(nm*Yu9}Q2}_d7jyz}>XucB*@eV=6Khx}Hgj z?o`1(8!zElIsL{ zHqhT$@Mt_iUpt`112ECYxuQg}*6g=3%p$^K*x|x>-#@XzNU_NJt>UFw)=?BxBRd|! zy`FD%OJ-cwvJJN{2(7a5&Q-3|Z5Fqe`}Q~pNietf+6VGF2JLNIe@9eyyNIsIs{4eQ zjKep9ujIeQxw_z_zeA*8&~riJbVy=WLO)I}15!~J0zflt*j}tWfoG%uH;YN=j6#<0 z@Y1~lDgUb2y;1+F*#DyxGHSx5F_T6eevK_FPAzUV86CY)FbLJ zpg&ytW&HuT_UbUvfVxr*T61|ARmN9x;`1|`nUWA8S#0uH7}w0=Vbg8$=)bI{DptHh zN2ztc;<&u#Mn`gexBKHO|9e1BF&4bkqED#aJ+MBAg%?3#8{*XZeJt4lasDB;sp*CQ zy#jE_9h{vf^S$0&rI6a!qIcX9wIc@;N)QUo`u2roP^uel&K?VmPty(#x-!>lWbf;! zj|IDD4o)7%3RqCxD>r&ow*nA}`^DXb8*i|F6eJDjylPHIL8sq0um z``q2{KdePc>21UjfQ~Y?DpD1ToknHcbq2T>36c+gB1G|XDrW9J{T4T(6bQS%0M^7| zHUZ6f;yo?TX1Lwc(kE7iA{X7{y*RbW&A&9qsaw8=N~Z^3jkW7rrhxW$K4OvkkXQf ztrX#Ge4YjSM#RopcXxm_LD|Q6>-??Uua8rs;ntkwIZCa5JBPc5HXoEfbDS{_Z-bNZ zS9|C&PXe93&honu(R8H_|p&SS%yYL(*~iaUQ<$A@ayw0Jhd ztES_^bp&1!>#W~-(U`Ap08NSDcbScN zmU{k1L-G7xW#+r)3qq&ekKan@F{qTBhP(Z)V8Dk+zE_?U7=(yWjwSgYq4Mu?{+a$^ zd0Yx_m{l*@Sh$;o?Da$X8|>3KE5cSchHKG1TQ*j!zZf{D;cCLs80Tpvs7{;bdrRCm z8_xqm&mia`JRi{WKp-(-iPK1xtC&|$G!iRCL^^WB7#qB`M+MQiD&ZDv?dNgO6z95< zk|0s6J}%$jnr*An7IPvh&jTWA%rQ|yfyBHDdc(YSf?bs+_ou#pkua*@W9`THMG=an z%oC=`g=(nS9EoIt?Vdymu5U*j<@-(9{LgDk)Emb9wTDn+?aHS1(fa#p)7_}(>fxYU zV>j~iZ`PjEN}Kc3guMTHoyAO1=!QR!=i1{^`MiQ|1)Y*fM0;^uOIBQIcy4HYTx5M* zLed=pedpBptNG-&Q2qE)>j*xTexb#l>6?+EtM_RE0_J;(pRGmAP0*p3>O<`)yUeH0 zJ^q)1o$$uWi>?qv3Z+e&V$H0*cYwA5U4Dd^3nZRi0Ox1Oy<(kz^&@w>BxL*vLn#3; z69C!F>z;u&sVOe}HX{WiHwGezlK66!DMRA(L!J=mmGFsPO(2_F;3-l&*Li9H_;yzIBGCDM_vG2@5Lo&D5cbwVZLZO~Xen*6;#MHIYl}6(p%k~` z4lV8w9E!APaCevDR$NPgP~4rO!QI{NxA$+~IdkqkGiQd(Fi9qVyzjT{d7ibFT*G~> zdl$BorZ2ZQP#FwpDjHNVd3Dl_eETe*X^$^`K!(p~FYX7)Cn2;zkJQa>3TtP^F-L&f zZ&+zZ#fwM7LGI)*WL>J5uT?2PN?DA748*+vij{Y!%Ab!3%tw9_JtK!;;zud%wvxf* zL8rFsZ9`+!Bg53lAKWtfsST0FGduhzw$`W=rF#g{DPVb*m}2QvXV{F2N~>A7Tlm50 znGo*)%GIa}@WN{j=&^lh=Q3a8|G~kTAe_^@7o2=iq0J7!g411XNES(|7dlBA7WaEH zKdx#jE>21#JI={*qvX5;;<(O?Ev3t)DtAPT!T`sCHZfprgTaMPS(&T~t&$^+>`dW_ zi9#p~y(|c-S!RV6$$_RWb`Q1+cMkJE%vzNuWhNxtZ89HS1di?z)F{y>|fMsH7>Hb#@v(38VJ5Rjw%!`Mm`Ld~}T3)g3n)ZfC z;BC*plRrN?%9V@K7q5)gpmXJN7A)`}kVS zl+b>i1})Thj!c`9BFdgzhsRBEF_I#)+TYpCJ3mGLR9t#Yf%o_82lVH!#VWot7~~L5BnDJ0Rx^FhJVW(cJW?bhc{S|q{&#peX1E@-fNuY@xGB%*3k z8O3Nan|cfP3sTI@c%`&kNGb0GUpZtF(1XFzxEKctDB;xOtsODvqY8^VRFN53Aq7i~ z;uI8}#W?~X(dMqGYyl8pivJVP{>Ntmm--t_l-LCVN!{KiD}c7D&H4f`2MYHMz*qaj z_7v$0iJngc^pE_EcR5u|L7KZSYEBWR~j@`SSnZzG;s#8(Q)#xbrvBFSNCICNBVs38A7GZo@VD(0FQ zgkyxs2PdZ*z9w0U+q~_1|E_badpUkR&CU(+TibSVB+T50*KQ1~TELFRfM2+0IZKDI zNE}o)e?h(N%t1K&l004D0WBEdC=a(o#A&c79({%cpv(8hFmXktbm7jZca1T@Kb*0Z zcS0tz9RAbZO!Q@+o8tuwvQ3@g5Nh+0M_F$NLKrr)Qj)R7Np!Jz8y+W@z69iTNek>L z>DvfnK*5=0tIca^+6|Gm96la-kVhTMPE4KojQv9MFuV$lO(~fHV{1O4Q4QSYR$nMXQ$lZWJNfJtShlK(>H$16w(1j@(JoTw|+WPt2 z{gw;UmzVo&;L^CC0jU^_V&nxqm)2zpEC6M&74f*DEXMH!fv^R&9G=yr`O zEch=^1t=x{(@qq`i7D!G3srBnh4}lw>WR1%jwD0Qdpqg<$^}y`1&dqR-0Cu=c`Lo1^7iO9@TWqLnUrL#@t5kUCoXH@&784&pX>ZVM|?kyL}!3x47Y0 zKa?AAJN?;TIhJ-C5oKkh#7D8GBQw@@%Zn6zoqf2#>4-_SxuDe&fg0}cI%^l}m+~@= z9rj!P>o-=g2{dw5Bcb1)F+L3k%>5EgpGw>6LYop9Uvl$fX9|Y#5EmM6@blW~bw%*T zi~(Ohhda`6x!tPTFYH7JKYYP_tc<2r4Qj0vu$^lqh62P!8?e~3CHIyt3~T%R+9w^N zGu`1mG1B|O!?12;8_0cM0QZS1(it3F0|2)MT{r-h^dGCF%++W)u=$o|7$DL8ESIt% zSrMcUzU+e{BxE)I$& z6j9IO`IUE1f%ixuqp?l$v_PYT8gS<{SgB!o{t(iQ`;osyMfM1$?h9EYS!N@dIa4oB z;XJyGSh6fqx1+-;+U#^EsRr`JtFFg`Qn&kBud^VD<%+BZo9_5N?gw!iy+!}B-{9ZG zojiw<@9ka{QG_7B`Ou)Cki6q>f(-xu47~oG1rtgIS#5fi@-^r$n-=#?3;Dpl_v_Vk z*##W{?H;Z*Ju}BeX;+*`(|kllta$AxnUq;9vhZ#L+(ZK{mN3D;kobd=?t6bF(7_l} zIwbX`n{?q^8e}{%qA(~jK@(XwI3x@mfUVY3|fHwkX=H}mY;VrgPcJ1Vk3 zihL+i7@6>oeuZ?Ke#!$Zi4qx#L8-a9Y`(E~uu6Mv;)7azKecHp??S z)+tfhE5Q&7%&RTM|M7m{>~Cq2y*2Cd6m)qKO^oko*CZP>`?xfUl<~x)E{4kgWLZk@ zxp*vc5b<Yh;caRys3f&{zOZKsp zILf!hW#_og31t|xbh|fvp$q2FZSmYhNZ=QYpksv{A7i^YV$|+qLqAh48LtVGGOZC> z@p>3gUB9{npcz4Nwy_5d2oxqz?l)-L_D2-RZ5|uH{*X#e_;KgZj@ohvr~crtK`8*m zF*@#T?|xztw4HmM4q2ayh|u>7=fBm(Luro|r}#ZCl>CE?rtKf~$2Bfc!O`BMQZ9YP z1?W}av3~tuiTzLS!F=_NQ%K%f4yF)3ii5F@Ai)PXC-`Y{{CwE}%TN*X=Te>B?U3=Z z5MTSwA1H+*ugTD+@oRTuVBNDxf1@~+` zE-iJ2i%-+n;%mhN(NL-pmnOd!;}czKCpcqxQ93EM;GqD*KnbI;0E8 zn6b3Yy$$bp3a99LdW?L`f$v1Th1k_DP2{OLz|Q{_#Yv6g7e-C+0f9hCj@?`Gk?oQA zQsngN!AN@KVynF`IG;UvSonbcA6ojq!1`Y^h&&JyrVSC!qQU3e3SQa$qt{No@BmbK zahx6p)E9R|M253k-bipX2eLMwFS7~$%aU17H{A*BW9GlSJ-)1U(UwwD+%Q~rZo*6d zavvQP!ZVBu^z#@EOQmnhX)YW+=5MOQC*s46O;e;;t0PCNu@VX?nIe}n$XaNhrVe9k zk5+sPPB(*IA%Gkz`#OrhQH;;bxhWk+N@ zi736##>$;$hRFqM-PW*|*;7PBjJ)!dM3cibquWv;3?>WCI8xlqQV*=wGn|PWc|Zv; zam<{V*)jc_+dBc0@CqLqQzT5zKACK_5(R1pB4yZqVusliD2~leA03nTg z&H`id)tei`qhFI>Mq;954x?xlpTzQz!4Fi5BbLlP1$XG3*JUPq!=tL)LqY-+MIrbf z-5^FwV8E&%8EaD4$ACgL@?QJ??Y=Ki4T${9<@Bu-uqO*oOv^`M3PZycg7M*;4}NcQ zxhSlF7N2p)#_|gXLrM%^rHSG%uFD+g?Z*T2jKuZyOY;ftE_+>hhRX|W$Cnk|W5Js3 zf>*}77YE&AQmlv(WyDBgE5B~6a|zwfN9F#Q+(|<*?#XcE@IgaulJ;?xOI5tf_4b8K zvi7!jQ2Q^ZNLKS`I-4FEkC;gGvfYXnIA#sKa`Wov@6}CPLX`xka%5)>X!nj4+z_p{ z<;WX2#FoI;(dzH>fA@00wl4NMU+J-ahGwTihNe{VLC2Us)nQiog3L)ApH)20jbIXe z!*U)m$LAbOTl*)Ae|;V|OE>T|w>OKNI zjEjR@{S!(_{$|x?Rqz9MeqeUqDpu<#tLK6s{^l+yApOo#N~d^L17OspozT2H=Sa%cu75-yr{*W^RZl><@bquF z@IOsg|Jd34WH{$L5-gI`$akD~Ayld0F^FBTWS`L1(wJl6vz)Xycc2ZwDzmEe$xdy|sLDalD!>~$(84oN} zC!P>cVF4YxDx(E>PX?9uO2}gA4GN)d?6O7&!n{zJ!OY`h{bNQm)^e+z*W&auKvaaH zKQ@LVCA8DHsvxeQtFP{q0D}2jeF{~U=KG*Q0xu-gTsJpi5cI%xdFY=L|LXSCXg1VI z!rkfrVL|Z0Hlu9_2l^WzH(mo6EYfo0t?o?#U-L_y67%&p+AWdK=YB_#`|IU536LUi z#6K@dDk5z@%SbU)NNyAC6zTNnv@vY>i~)K196AcpBH*W|orVOzvF>d0&z?Ybdxrxk zwO@wHr!Q&c<-E$l9Os1o1BHy{??)ET`F{j?osgw7Kr6dD@UwU98!Z{zGb8(SbS_waoH@4;Ooc3(2d9c7{^ygFc zYVZ=c8efeVsiX{VcVQCRwK&aq;N0>JZ)-d`S)Bq5i!uGL7TyKx+6}Yi?L|p0%6S

    h%_hNOhQJNuuY-cM!i}`Jss@Q`NSjhIKlLFhVD+O=Kjf2^5A^@iGN^fxcmJyf4eEc#({6ZQ;21Ybo8Zcyb)KkX^ zBJmesW%-+ZgY@z%3Nuvs!}DBFGwj&(2AuYeS_xe^B%zMxc(wRJ63Iy+1*q=O2$JiBjY!!DDur~OE9TRjNB|bSz z=y|EjH=%_vHB2oMCN0wD3gRRR5yT4{5igq)E);Hwl(Bhs_C{^0uEn?3>OfsH=qi;p zCJj`V3`X4b2W%?W?h9p(E40s>BWE-QJpR&o{blo*p|Q1(L2_HFWm>LPs{^aITNZy8 z6~y7poFZXDF<_*CPH6?gCURpNisC=js06|M%fy+adWoF+`D$rAWn8W9*IG1{D3_il zW?0P>Qb8*R=6ma35ZjhU*l;LCNHtQUD2K6)KBkR+#Qvf@6^fgx6wvNqXwCT62`nPV zTBv-iYrQqLJ%mLOWYIHiTie!x^r8}w3BJu`77$*&44aWptAF=;0iS=4_Fd;3>tsIw zo=FHU)^IyBFwA(wGAPb&6DVe8%5P!`b2bGZud=i8)C9LG`Mj*ubo3To{H3WB({tqc zIwyLcjEiaP48)(c`x%MvWitNEU(YeL$h51zPjTYkpivipGAK3~?$TzjNyG!}y+YW$ zUMs(F^ZIknjd3(KdWVNXxjT+u_!mt#41)X#>+$U_R?m>39O#~G=O01LIh9`WKDBgjJ&5yHwq#8+0EW*!mE)(=}3Nk0ym+Dj=rRsE5;?^ zWHP7CjR#0cPw(@NIkNEROWfHCT5;} zPA6LZEijC@;Df^HxS52uF6M!P(47KpW%>7~0-5^)8CgNo8krE7KMnqtt+jrOXOjsp zG`_s9r*b5py1>PQ^5mH7lgz(Ho_GeOF}rDbrR5$te;w&^jMhMZ*Z^exTi&!! znAi3wM0TX*4if-=#)Oh5Z@&}5{o6Tf@&ew`9`jG{41l9`{HqXyZ`=I_76W8!L-G_P zp8=E9{(Q49v&B5Hl@jiHndN0)ONws!zvtXV?nYHNiw7yxGAxi>nGIu(3MpwXO>qZ@MOL`bR&m-5yO| z^zk$azV*}>O}y!^`DkUdC`On)6#H7m`&!jgp&79*d2|mt%DeVef!>m<-W>EgoiO#P zwbGY;7l?Br2?J|&PFSIYgmvjg?bPY~UcT*xW3BW`%Vr6fUA?_w>apV22*}6AUZake z#Q^L)sZ(lEn)uxjbz2QrHLB0DVgts)D#|xlz3(@ebY-6*;^pwS+H}J2gZ^k9Sd8%- zA!{Knxe4_PW7O}CqmgwrNb_R9e!B4f4)Ko{55hVbb)>{s4IU}vK-F)wlI^8)BldL? zjWVy|#p&UOWqZYgd%FMgjofG=YsRy2Nh6^|*oT6kZIcSePG zGEe9}Fh*KO)64J)Me%&G@~8x|7L*@%`4x;_n%fA(2gOm;aZr7uNF$a z%1S1E{M%>LtM#^Ph?S;-{E_2idNuxQ5565mNK@_B{CE3G3N11hS6!>gzVq#xA2JPq za3MDIh37D>;u!ZVQnsk9h86qU8iVLLs=*-klE^HiDU52efF^dty@pJOwIx(Dc3cB0 z>SRjVfiRY|4WIu@!9dAU1n*)vMs+8}o z4&jb%XqlOZT^?OKVZ7>)k3PG*W}*AvPZt$GbsmmUUQRB()+M}0{bgB^R6C7ZH--Pk z3h3IYND%v+kw~iTR9Cz9zI51wk5?u0!zbn~Zo=mU$*O+}h)wc^M4(19={NX-59PeZ z>JTW7T^XoHG!ZNBi}itE>YLmTnozwz0DRz>$U&R&AU|WT-E488c8qy%vG?U!Wh06% zAbmx$?vo$L}(vh)fd+htU1gH^?~rZSNpI za&2>jdiXBr>A|1uzgua;Md<;0!XN%^D}+0%6f`VFL5yFE1oJe6k)?&d<01cI@_u~w zTGeS{WS>p=qL%WPd&$-&;hK_-?)mX#LBJl0f&Lxu;rw@E&CBle_+Qe=5H5mb=iX>9)P`@lt(j zT`8{H$!|M_SEX1ZJ6?pt(|d!-^`$&OlXg=h48j7XpPZ*kkW0C_spK#agnV7Pj)oo{ z3QxNZ8&l~Pjxj}J#>B-WGUD65lF~;B*g6?XFqoyUfby%gt~tYI6I^jQB1|R zI5K@9!heH<&5d&C{z@0{Z_nI%2WX7VrFe4c$lQ9nv^a9i@@p9x)|!HK&tu;+@OLd2 z-#xSiQi{z$kK5B;XCYHZnfHZ5}@Ax5iNW_tp53#>@4c- z71gL^pjhKqoPyW^`K1zIXh4hGO3*)YX1hl%L^2kblin_W{Mt;XXeer+Q_0To6mfd# zw;i|RhF!QUka)9g;(E>}yHE$v-2JMDYWYj1CPM3k!pO_gEU`G&pTqnfNEJ26-%9Bl z2(@R|JQiVEWO>Dy%VA{e%B}e)vpvzbjvh{_yj?XWlES>idH%uAZ0z z>K(A8F#}ZQC=x!fEZ)faFG%Xc`AoVw%9$YufV;F(zN%{7YjsNp_|_`x&C?hItlczh zX-aO1NBY~_O0r2_S*rE4L>;7g5B;!MQCnUbD&XEG^9g7WlX5pYaNH_6)|9{vQ<9Jn zY~R`Asrx|u9trom7xMxwB!BrXML|RKAO*lbWfZP9?g)!KRSygGqAyxu!M_uew?-^J zXQ%!MQfM1_POTVCK1z*g%!iWZIyK;}Mb3DKMV~C5%1zqAjD$>Z)WqfJ8p+XedV@Z{X-Jyc{ zVa67M=|GK1G>nQ-v${fNFVG3r)l_OdA1Dbf^p*bA#CY~tSy^$Y*!GoaYiB2Sb|Ab~ zfUvJY7T=EGSF=XELq^Jj^hbqHT-@qqZ1LfVEKJ_a`Eb@ZJ9G;KmR2iCA0j@;=6#!I zV+;O_Vud@FqrtgW7eB$VR+1ntx>cSorniGj*iJ;;7DLpQKwKwRL*^b58fxC!c{iC4 z%w&;@J31P6Kes*%TZ>DR3Jta@-fU3ANK^M?pl?_1P{j|{WnGDdN=7}h^Z3Yl%#7xj&g zh~g`z{O=bO_dl{OU~N?w^ehH%%gI<35rVn7nWdY8J7aGafb*-#>$6K|xi?g^K}4v5 zdKcdOwP&VB@0w4$JUu*^u{txo&l^fDatMaSTzrz0s~}!KkwaY5pWpzEGbwig`qz3Y zF)!ej1Vp^3=)9WHU3aU)W7$urV9a|i={CbLwtQ+ttxQNMyW1NMwxXucJR+tGG)>ulD)H&pEW5`YS>GXX3@GlNx_2mZE-XF2U+I^4_#(d9 z>@P-qw%*|BpS;9a{)u&_DesI~^=wY`8L3m@+cEy{mj;q>dJrZ4xfg4ETNEer^;p%J zViF;6 z5&&;JIT3bIeue+%^CSkecDdlfb7bHd)`XxNzK|#W&p)FpPF?#+QrbIIq{!!zYgyJtSLhZ-r}ImLkMi@$C-fn|!G*=ZE2c+X>oQ)nXk2 zQ@t`WU6NApB?f#@$DAlQohSz^LK0poeJ-S7MK{`z3L7wF*52VwFVnGm&-peuDQ{`h zMbZH!urcpfv$$4aqP|W~4iCDVPQHh2Xu7Mq@NQjowoKwgLGM>x^>YpF*L4HvsSR?nPak&#$KMf zYV;xl9c9)64KyYeA2iuMRt<-+^?f5t(~tmlHuEHwCR0aOlHgRSxBby-DxYZH({D_z zO)y@0H~5@{ljJ-;US=93avYkuM!tHF6;vMyG`FMj7_aiYnj(XaR)1%5Zchm8LLCuJ zTM4nlt+{7Um&$(W_>FhF4_lhg;-$%uRWCeh5hgzAJzBD-gVoO&T}0+Q>C)0HZc?P!O`*hBApOq9PNNhfsTuH8@SR?`6y4fPKcmvT!s^V}B- znb7?n6s>?0WBU!sq~tl15uP< zY{Fv$@AXYdc}xoIeyZcVOUfH--wf1#tm^eW8((SLqp_pB+1wZ+hl1-x+G;m8BJ`U^ zlr%RNrf>?mvKp1sRnOHVebu`Z){=x-6@Mv5AM0Aqmxq?!iGp}gNMhCKn3DTsHA+gj z%0n|k;CURxQ{CitxM~B;bgtjT{Ni6h9peP^BvPgd`oHR{&+6#U=CW8R6V&y|W)pdk zOtP`s#S%@ys&iPxnqeoQ5+TfQ-R0l<3>VU~HonB;U+rJRtoRSg+L( zm&EoOMb{tLY0aH*VK1cRg&sGD^~kG69gb1#(zJ~a(YM7Gv6jn8@zaP)DW!{~K(<|3 zP#FjAF54p^*Pr46cKyHmFhQ({D|;fG86z*xmxgQk#z`I(_aA^%-(QIuPFfp9F`1nP z0VdH5^lls0Pd^c^@CgoH)YNe=HuwtJ?S)2nkrnpnJ@P%QcJw6qcshE>d7iJHPA(@m zxrO1qRaU7Oud(}yeh5^us7#y|gz24dR+*^CU!nYZ(GE?5d+)bVj5b8@65EOp`MJ71 zd)Fj&t@3Jw0{X@oq>7QG{P}VM&zUMEqm^l;{dc=l6slXzJigTA=1a)fs)`b_bLrHi z=R{8D=Yv@+B0^6+wPnilMVb3!PHNWu^b(4SbUqX(O^_0VOPZR-l>iwcnm%)OZzDaT z-wTNwg{SL>Srv0JPH^h;jwqaVK{7Ao_Lm50R$#8lxD|D|;-Ki1G;G(@=8LJNnaX)L z_Z`??kI(J!y6WfQ<995IU#=V!iS-o>?}z`GOlvhpJCc{nIH94yjIHio;`EG7(D95T z!`6r_LL`en0f($q4|Jtt(_xYDlZ#kYCW-%&phUr>C`h6BDuS3rNwKQNct4&0XXs75 z?Qe@-{^%kB=}o8*lVL83(ol-WM+T$BjN(bek7)*kiR^0?P3Cv4cON3 zzSbKT*RBDwmQ2HCpS$(jbrclYIF9k^3gTA&V3=x*0*=&h_*(qOgna$JNP&bbheP$r z4q~^<0zp2Vc&{e93x2h9$B0yDVf8mHyidb0&FWaS1ZFCV;|RZ$vXa3fBlBs8>e0`M zX0OAN>uA}^h&KTVNDJONFD!JI{)a;KsU&lqjI4Dm7pY0OBp-&=r>fnJ*V*!It*y!u z_G;=p>SLe(>J8EPsBv|GhtxqX@BpKaAiheqP7cI`QxjoIdUb98IDv*tb@NA9m7+Fj zQtHcoWy9gVR|qwXfi!}24MOaCVER;%Y;QEQc(8i#ddtn#=EiIX!8}nI{88}~gJf%m zcpq_SzP0^DOfaP8EyB4acTJGgYmE%?&b7K>86x#9Ebt8V9#1g}{_}!sQWldOnE;7= znbTzZvw43cU~&XB;wW-o(@U>%Dh$*D3Ll^cK(fDFrKvgpq;d3ATW>FHhj6|IIQ#Db zZzF>{mB!uIRk zO0h|bI-x+9NH|FB;}Qy@h_>Jj?Ls)|A{ZJznL-j(_49VTW7Ekk>8-h<+hV=t^vOY3 z1*Oi}IzkCL6PE_5PkI$;Tg-GqNg`&z<|Zk2Hvc*AoR-6XC~lSs_IyfMPi| zl#Ya5yZia_`sDE$)kOkXQIUbx0u_rT0Wnxcj+8#o1>wgJRgQSZQcRdQ9a`)Xd+Nzd z4%BhRVwjHaKknoAf8i;f6-}wvQIx|>OV-mHQa5b<0-nK9b%2rd@cIR@AU-J8C0F37 zN%M6h`aj^(N{J49(6swxwQM52&8NPA@Z_W}F(bjKM+I;Du+HMCW9$*=0m+d;BDu>J z;_X*(xo(U5N%ams1ImklW{+18$XD%@`KWhJZ(gPdL{5jRfB(v(X%}t7mM$e`hD8h1 z@{Kn-D3MNQ04sh1Mp>}fL|Et#h{37Fd!{F}SnorzJY2DK*do3BK2lGg>wRldjeg0z zz(4xPGn^ENYe!IOvs3V!+TEO|thT#wtFu{RU*E%rofL!}7~9l)VZAVZ@OK{nC-EMwN?lxIVPZ+W_uQ@I;1txbW;Pdd$LIgiC?p6|y@BIb~ zdJ4U86Xmuzex?5^FdI3-!Gz=KFkyjsl&ba#a~=!j!6JE4Y}UfD>8EoyXGmH;BTU8(Zjc~Ij`a_Mtu{h>jdPie@Gr;#_Ol`x!hDVu)c95X^& z+0@euRcJ7IiC)R-Cd&B-?1T6-y~!(i>91I~t5?z$&4^mLB*rMGW~zUF)q^8jWQ#lV zkjxdPz6Rp+ z&Iv@XKne#X<32D|1WJTTZZkzNEE_;Jpnn^E!$Gd1!$h)?ZQ5JJ2OzVwm`@^71~ACB z7TW1#f*%AOA&>E&eQ`k(zcec2Y+d1+GY6%Kq(k}<*2Q%F%Dx#NLG<|IycpaU>}lak zLXE4t8^*sfXH@X@PsbNZt6QzOfIp$^eu9Qr$W|*;OKtX76GVQXPOu0c*LQ1Gf+_>d3ChX_fEagmTJ zhAT7-DWBHkjO~xS&G(ww3vLswEJD#~DhEn(ylMw6Rt1{L2b)DEqw7)&%xUPSz|GZVBTM0FlDU>|Xeu95yy-`6zHmkJ9DJcWjGoz+ zANK3xFvt{^lGK?D?~tSDJf7h$Cr?$ioq4}Z4GzoTXJWQybKTR~pO#EHoQ`F%9}gYO zAQ8Wf;gI=*6u@oaiuX_#az?Cz6-o9I>Z1gaB?5z3F)*b1^i~^hCUB+n#X}%jVp7^t zni9HlGICCVzv@$qbB3*)B8Jl9-RyL49(8XGz3ay|H;I=jwtj;r@4q$cweLEQRDWzh zXi3So{-uMK;S3k8`&+y+7%rK75f3t!Z!1-MQ9x#Xqqv~|GZ45!l-Z58`$wa5YUh1Wd{3Zkp zw6zk`mhtrT%x|b%yBQDx_J42xv=_NwnLs|GSgPAPP#|04;WaGupC%Y!3YG`WF?a*v zCH>J27sVVHNOIa6F$tK^V8fh1?k?NXaiQN}i7~9E)p`>I-SyJ5Wwm@EOBPfD{2n33erBPCigK3vQihnN_+guo zAl>WgCN^}t7HcE1_uiOj-^k45_*=yN$Z#VdUqQIR%eu0YQcNhA>)9t{@ebJB zIB8hOP?*+1HVA?9>U%Qphk2)%^;zZGTbxZeF@bts|M*W>;m25!HyXJC5h z@H28QXvu^SnbU)S?1!%`#x`(DZDe7zsRM{+E0wQ%+^=-bNac)5&bs)E zLIxA%qlZ3Q|M_f9l$uUzt`UwIj|_bw)a|8%wh}ZWzUrD(^;K3&zdOCc$Q5NFr>aHH zqt+40+YG2GZDBhsC$KP+pT~68*@hDG&gqTrYG%Uln0p$!C`dL5s!+{qpb-TISs@!1 z72pxIF8M)}&k)=MS-#tB>9VJsZ@T+kuxM}V<22F?xTj~}SDPjL46>d?{k=Tn31B88 zMXNt->iIEE{&^^3!Cp8onn*MbvN!q=3H!q~?w(nuMQnBlzXmiSx|D;+> zurrmotDLYkNwIVhn)`%2lONS9@BTgJFtW&mz`#5cXqR#|Q%>Cpp(fI@>*$D_dUAuW55AE$kxm#!$orm{Jddhw!52`(5&GoDV!mEwC=; z8ttOk`>^ndtZ~TGDgoEIonxY*l&6|cq=@;*($^G}$MUOsX#+POQW5 zyTgJs%wIu2=gvZ=ddw3u#bky}DQH59!gD?1wpT}r!Hcg3qCGb#pMDd7gm?*~%?KWk zl}f*{7lF};h}<0Y4>%?rriaWdOII(H(hhlXeZqoCHQ&1H%&rqtL{hX`zNQIWQx{85 z0tlS{Jsu!}_DqyJ)6tE6o;~~aOy;AQT1eiHrVqMKB>AOC>@Szg-W&Y#ZrkiAm!v$Z zyT8wH5%Ia-EY1@s^X1z&eozhA+qh-~cV=+Vc`PuY*0D#q^r1HWr4kcI)x<{2XM#}qH)Lv|n{lg^t2W%-B8;;bNEe;NB zqX%~4IMrY0m#i<%=P#ua6jf4(EyZINAk964!aC9-@p2Cx?UY_-Np{0%mpZk)?w_X? z#a^2g9l7BLXNP+saQbM6iyT5B>E9KJ+Q@hJY(Agk_RGkkxB2XyZyEYl0T#oX7QsHf z7aQX5#Vx*FSo2=Mla4jM@U2NALzFEPBvE5Mu7(0)MXF&++{b?9Y87pVHyT91wDE4tp8n2GIF)9(C>?g0DI z&F*;}*&t6V$=h89&s{Um{=GqsL%qt|C9E!`E8RzT_kkakSIZ)2Ap=uye}kzn3v(Rq zxN}?!Re{mYQI0nK=Q5YAnLEA>?>Z|4mk3Q%2@YO#WNdVS~(_?@U{%_ zjueMp@BY}lPzh@;#b_>STIA(}L4;gV&V$$_Rk0Q>r}JGj=TgA3mt9p_F zt)b`B3mwBWc5efV3SZ+L8Om>rmA-u7&HetWZxievLA#%b>q>Sl$raGGN0A2VjiDcY zM|Nu);2-ZF&czXgb&46VSvYFuy?qk9OhYRPkGb4?M+|(9l5>aSMaek)*^sO}@7Ir? zwQY4=jEsNg4i0sX>}#}2k$=rq4b7TFH0S(4N8^!=V}_m0&|o>Dr9j~1T-J}f@RVQb zD{7}Oe`HkO!P=@WF+#Y9%<8{ILBK!m=jVTkEO6|HWFJ91q2i#@;-=0)Ewbu&z@f`2l=>7CdSyMPl9%jU-vn{AjGuXlmDPFXJwY*;V|p1sp-< zWBB8n0sV`)cUB?l5ac>kFd~jOg!X+8q&%+`>o+F9xfSd$U*{f81s>yj=1d)R@z;L_ zWkCoHQN0frAK)<`g5}e3k&Gn~&ULDM{^aE32{OghR|ge7k3)9?SY?YY4nipq$KX(43bvjsXbONhC@&iB_e=0&8BNK%hC?PGOF@;S!o6;eMx?#mVJ7`pQ36TGK{>5sP2g z59UR{bOZa;RhO#T_aiE`c5Y03GqM%Od34$Vm4DsgT)LA%coSXZ_M0??XK!N)Lvq{v z`1H|4iWNm+#(*(Sih$$aaSv|D8D&&dA#ZAd1c zZMyVtsHYI9eb^JXuFQh0xL>Fyl}bHO-Y4&WtzjDfMRlJO4}tea?7eIpI&V%na3l6o z%~)3qar~C`r^S%zweM*3;aKXPCh36|#hyyS$}5p_nTsO1tCEpRtKPF8W?wZ;JvQKw zY2u(yDs9{}dZb~w{H3vXyDf@U>Yy_?s84$;68?1Jvv;Q&&#UG}eI{)wnJSk)M1{hiP3|2Y;W2$;j$S zQY&XhdG&K_Tjd9~T^gr2wXqC8oY6&AgoN^dgyPOqNXMv;^4Xm9ZwcI_3r(5-0k8ZZ z6T+t_#Y``uz5isZ$C{Cy%PB8`v7$gF@&6{TVPuMl#1Vo%c{#t-6b{r~5;SXh47eu^ zEzR=F@*Y_h>=rG&K5y+tEqR~ye*GIoK7TV6{MYBkDEOcoyS40>YrD_n%P~l-86p zZ3EsQ;2(?qm?%&59#*yFz}yRB6v%-n?P! zxCJBVmMI#h$P}y;%;fjT#i9Q^qfg%f_HmFDH|||Tc4gv>+{UHa&z1pgeN*I&A2_8g zv41tYC@o%We6t4(?0D#0IX&bH(YZ|krp#8!GoCfXR0r+zKkp1%5n4Q`zizJHJo)~) z&IU<)1*76hHJ=6zl3K2$;c|%YlKoat`fBT@tDtG^;-qgWqip46lN~mYk)=p3iN&I#F0IDjwtVjwI&Ou)p^Q81UXA|M4VE@tu+&`;IergYml z)6@#?DK<$5l~SPliL1ca2Y3eSJqZxf|9=Tn+=fKl>(wIm?>v)9MJ2y-D{ir)BZ^mL zyLH#)0$Db`nSWo2x4pwieGmFx~{~+$4yst?L${SSM6aw z&RFqrDz!PI@sDz=2{VSXUDg~h0)2+jL~xvbtM9U`cM zb{#aK8M;|A`jIfnfcf($w2gOGKYo32(RlZl`8@H1j;fEns*j=E$!E=%g$=J@{u=Aw z>m$C!$e5l2LhEyXH=9`v+$9RG7eq27$|r7wBYeT2y!h^m7=w(#}}W@ z392nJH=oDz0pz>o#i~aIfM zH=KgY*=!3d1>*iskr+cbnBIrjyuw8x>jn_O$`7CvrCa(+nNBD$0NYBKvhVbN-A!6M z7}_gk-w*0)V*39W6DB*WhPp2%EU)0QBW-vf36eewMbm16kB98Mbk61e@e zyx%}t0^efJ8XKSuJR#(+)n#|#vo{foSgB1#dASVQ4%2P)Pw z%Bph8nA8MjUfc}}6UYUs6^AVG58^j`Vm#gNmYx}5k38@d>+(9m_bRo|Dc_0j7ip(g)H)@*w1Qg40EF9gE+z7YBf=!M{w^dnrfom&{wsMmumftxMNPMBR zwYc(cpMG#N11>Tm0M3|q#J)Y2IaY2#I>Lmkrb<{0JJ?%Yk&-y?X5#YKtC*3dAB`pN z0m#49@jW!1^IyvXbmX`EoId|C1XSGv=VB4Oon8itrs*xTORO4`+W7^oQz9pBIKf-E zy@Tb7$-HUIt1n?AJC2SZH@lP2Y7E1dv$)}TDkZJXpY5mdlgY}04gu>fCkLLf^48D1 zC_QAaz>z&i&TlH!1gogFLgt@Qn@S^H6uq2T`d&!2i+RS|1r~&a`x#I6+Pg6i+TvP2 zDLd{6gf6!J8AnG;_cthvZ{HeU{xI&=?C<9m@hYPi_VYLcn(_F%;k=*JhPZ%2XD(5A zMw#H4jV2nV@&gc7P~B7V#)^7Hh{Zl}iG6y@mo>s+VavW2Hlqz1lUl!03r`!y5rFNV zu^U$3hLjGFIBbg&|N4RGdr@snssx<1oni2!w^{5XK&rQX{!6-)l7o@iq(}*Mm8iTr0)kM{roJQ3MW9&KwRhC6udzr!r z;7iTY_=z~F?U7&OB7Cz0D~S z3pW*Bj`exE%^tZMxVFi*ffqXnM+-XF9k>I=E)A$`rgm zr|vE4VlLT_;yxU;#^u;p&B=k>CTy-Oc$Sbelf z9gb)u8TEaf+7IajCbe5v@0aejONYV57i2;m?p*6vl1V_tJ9z>Zy#29n6TtKS*JyJlrpG0bl($O|hBk}QsCsm2+=4(snhd-vj$E%?~u2JU%* z!NNL)>%#xxT%+%sZYC#DZT(3)kw8@XKl)jan2hZoHd~b2GIt5bLJBP^2?Pyh41K;z zu}!DXte9N*IegoX=Yvi*e?6nOO-9P~$;0j-m<-)h|Bh?n*)^&s{q2KM_mPq$Ulw}P zgI~zTp2ACO5N!n2inj0CfP>;{4-m2dD9*$8MF1Zr{Mtg6)LJ(XxyLy9>L@u zucs2uB)Did1zUe$NKhqHI)ZSiiwUrh=o(^Q>(;N zU>`UmaZqt=$1pk?apily)*b37^Z=_2KCxf+`?AsCXUprr%V|(7N3YvVc+7<_86UeGr1^; zc{wY(b=E#;aE2dYmbBW0=YVu}Wi+qyN1e}F3^>?|z({bzhwFLq61oct{?GLfZn&5P zd=T!G2X2ZBKf(!Frf-57wG>NnJlOAXBGZmC*f}CpV3i1T-^FzUE67KP*8{-dp4HrW zfSqkxuU0wdtMQOBd~C0xwk8~lymzJ@Tn-J%2hN1ElK-e!M+zmt-ODnhJBE&WRl=*k-$V1WiJ*)J@dY)j{GOja=Xw4GQ0;M&+c=y z^D|%#BdkXw#M>u@RF!e*+FRgp7dDWCgu6ab4brl$8@qK_n#z9hQ~jdo|3zHY34=Rk zKB;%F9}WNyBA>dS<;GuuDL#Okn><&A--Ery9cfxyv*zWDx$q`4fruZ>R86`=M^aOd zU`N{9OLoKDG!W`3+8Sd07oZ_x_N0XY;rtGsPwh@99bfr5IskF?4G&g2XdpsV37ywp z0DSv|(df8XXK~?v;ieZgJiAA}3ZVDS5}?HZdeSXyX;XhF1u2!m7^kT-gh~~_cx;%@ zczC>zPe5`0>pdVpR+jn^y`ch7YZNL`fJRw-slWo<LvwIm8Gd#6Z)YR-=cf2ua^O?e6r`<2J`J+y% zd@-5Ll!~5}YVowG{BxmO=P9J4xu8x*qpb7?PE2Ws9|P5r)ac){;9%%BKq|;&8f7?W z!mFwjnGFH_+;rO6)UW~`tX+`pfxw>=r?O;pmqL{vb2!E2lE^GrlV0~9^t|!qpxg&)qd-q+v3$k zM)#|CxJfuCQu^<5j(j+KE(k^F*2fX(x6TU9ZoaAHykwVwcJ|@5Z1QbI(-ccKlm~>g zO7>z~27?|BOmq|(j%eb68bJ#kAEp_|*7)S-Mq@Cwl<~ORP&*f?unK$MSVru4s_0^E zg+o*EoxZ7nvaFLelw8SjT_xbI|CL*k2IpWI(yD@!ULM$i$R=u%PhgG@YKJ%p9Ht>p z!zJro*I=bt_(s6%69a*391Tr4dKmLWrO2$IpuB@p@HSrn0AUO@A^P(tt49kl8G%<0 z@orAA{wNqZERL@1FEcl)LNeO$6dDT`FqcebQd;qwgi}7i4TfK#5zq4gL6;NFBJeJS z#-naNK1Bp?Y@a9+6XVeTs}cZP^8&)sDR4YNK>yRJCrtm1GH#03moKb|f;45>gx2=j zX7E*~r!(5GRy+*HAMJGTJ6&x0@K_36HeV=0h9k85>M(B+wc$FxQrh{yYq)>yv;pwB zLw~5gYWArMR;i1TrGyIqfL}E8GC-Q7Zpn#t5h6NpC3JYrC)qWVT%)u#e%&m3lORwn zP|u%3;#(YyJ*Gn_Q6N=AIJqEK(883M8Hpp85ELq?2&Xu;vXF4R-&VE%cP28V1vb_@ z@l(mh&e2}XNAmrYxZ)SCA7~BRfK?>yo++F_gDSY5>ad7ii8F0GJ+8i=6eSv5FNt`VDJ^9 zOv8s^AvaHmu+XcZoJTwTB-0KKdI-e76$+G+Ble_j33cpN9?v)369WWWI7rW~J;3tA zff)xUx_g$$gxY-f&OLI81)bK8Ds5LkPVlOZkNJ@l7IwaYP&vE6Elwiv-Un`?Teh9iDnGP%m!*_nHP~@@z5EWtF(Pg9-~V~^$JAlA z)8BWa9KX13Ahvq*ZgmLbr`%0b&bk-X)j<9G8>Ui+)I@#OARkH+rQokHeYW3lj0GeI z8wTZ+M<|ac|DB~lb_q+YBBjb}vd8bk1p{jV`^v+L%z4LB!p@||^)J=~Sr^q;#Wcaw z36rWRQ)04vg}6T#_tS>!pU{$@q6?$8O{82X+8HtdP2Or`*HI2(*Y&fcM^ay)^DY{} z;k7SCr9QNTl(0gY&Nc?TP}o6$a~bu`s^D=2%WxF4(FZb#676Iin5l}6mVj-UBp$wK z`G(d@7VXv|0TkkFL5-f_CU%3R`HIUbf}ODK>%-|V7CMfiyljLx=?Dc*_|V~-RgsEk zO>YlBA9ZU}MNw5z-!Fk1?LL-Dxp$8nR1ZRro+mzJ$NmpvBbEHKfo`b7Q}t*`Txcmz z)s!(Sj2~p2*QW{1t+4rOi4f%~3dqrO^kMzy=0d}g#BTl0xrFh=TaSMr;yEc9evv@g z-h~MF08Oa#`{xmv6|ztN2`{lSnw-ws!Dix1s(NJ=&wLqKYLf}Ky>l|QI}L7RosAop z9^u4QFKg}((qsp@;)f$r{5kS~tg^V{4*tnPb7>s2YVcF3;Y9H*raHD+G%i*jU$&Ba zXWD^F9oMD5pWBN#kJZGuAR03 zcP4w?{5kjND`ZlgK)zhox`6suY9Sg=^3Am7HTM+iug{!|W$BgX6%8B?S`3Y?kmVe> z!981-w6f9QEVoQZ3NE{DiKE}mE9tEjAL^@n*94pfVmPZ4EfGFKZ zF^e7be^YK}LNkCKVKnRx6KK~Nt%6O(7%6eH=em>yoPz=N>o(eK810J;RI)SX4GWes z&9i}PSkLBbba*fb?nv-xx?bZ`QqT{`X&=UM?#mf;Jr?6+~EXrT?$I_<8=f^4ffg$N9%zK10mRgR!Kow`@z2ZH8X)GI1Mf&FH=>wPIDBH7qNy zHnYob8~ptAe9ARs{N2S?k-FN%2jgdj3e_T;=(y_Wt`f6_}ywGE~>d2A_yYAJC$fU zibX!&IS)F1AGV4FlK^G+zirFG^Xg2*7AxreTzHp6?ahnV>!xYekYwhnq3V555`5(L zxKh**`8Q`l-$IX~xIAq-Wyo&e&{3gynue2b0#rG_G$JNcDdF4VnGE<|e%oQ9@nF>P zOWiF4Rc9>9)^8yJ#mBw`-tB8%F(H!U*tgKZ^)acr_K7gWR)pYeG0hwLYO<*}x1JQR zD5o?iEb}WVX>gO>yU<=ospep-9{@h|bbe2WBQ~kW0X61aSZipF))kc*(3zH0&)nG|g9(qH9rJPl1D?NacbyAgR$inD*HWeBlf!Fue~-uS^0O zl^@)EC{!aTB1k=h-D;RSj*`U0Tp)+j2YLYdU7uns|3Q2Cc7cHeZ}TUi^wh%o4!7e%+YO80txdkk>XykEV#>~uZ3?7kBt`N^nQq9s{MaPYd7%;G_QKhYMN ztbSeGV;?Yb(Vls>>79SkoX5ZFZS)D|v7wzmls+hvc}a%{BMyl4;7i}}5S+cSDSMcg zS={Kl={Rq_N`E;reeEF}{^rDw!1*N;{)W>_wDFzzvDux0;w2~*=F!YP+R89m%{9O> ze&b*r!INgAXr#kMV!x_G*0;KAu|$xI25iD;ctVlyN{3>dN~Wxu@m3BK+w`L zpiqN}Kg1dila=gX@F3400oIewaQtShOEeUMG0cF#w-r=oQ9$(J8@`Md?3z{;F-^MM zdBj12^@#}1VySU4(~wN|b7$fjCFr-9KS(xW1p+1+$l1RG-|W6cdJtjhX%qc|woUPn6~|; zSvjL!uRHWtt!GX_V^lk$Uxom`R15#$8}Vo*HTv?I9Rq!>ViF!GtWlf($B^2FwapQB%nuMgz(JOZ<>qbdbK z$pTAjH*}K#^M)=&A!*nUCB5R0$&H-N0L#141l$STFR;g+*I$7j*3JL%8|g?_${M&R zP7xo>2yxWhZ);H7(fH7=_4(>KHs?%u#nW`_yN7}@@uKrx`SNu!#>288!G<%{G{6G_ zki%xJo$NKQ6;&4Vi5|kU<|o5O?5q)?_NV%XJB$L-oc`-3_^msA7PNXs^p@;BFNUMD zPe0Ky>Gsz#RIi1@gat^=u0CMW&D|ep?gBuU5qJYI7S zU1y{P6r|Q6kSk-XYM&|3J48LnFL(}(amvB{(69Vdl-svsuYg5Iz@-Is2w%mB8~v8&QWLSR{rno4Wk?=oG`wVy+*2$;8#ptiucc;?qoJ9VkV5#_e>4(HC$tdYlpTNe4ldZ-JB-QoGF2zYP181C zl~|n94NhI-2Qe`dTP{eKmMtTJ>u!mno*X#DgQIuX#<~C#1K?s?#74@E1m4(*1Tj3J z06Z1~_`kC5NT-g8WMmH!&!Vh)&M4|VSZI-J31fs>Ji-o~(SW(=;nahJm)vOER5Jge zOW2K1r83ER`-S+AjY81u0XysLMB53i&q~x!{={8x_N##pCTEIk?X@bc-LGuPm2RI- z{HgnSoo4*vfqwDOyQ2uO*_g%3zYl4*i}9DUp3zAQ)IoDd9int` z1=b7$0P_{6(5Z-uBa7%f7vk&*)&D?=KBcYxIUzSKoiHV7?c~7T=W5|hIrezI&SN?( z+WTN;Ph`Pc{+C;7kQ=!@9L?@1XJjz4#mI9brgT2>nKVbFz^5xMikL%zDKj=DVx0pS zQZ~flh2R#B2xU;NjT~?~DxCXAuHWX;q2F}@*Rl7lQLf;l(btUYoVK@Mr)*|9kckCG zU*8b2^Z<z;`+>u0NVYkcbN zQ%@>8eD(z&P^863MyP#OVgg)Q_68~+F&BeWT3uK5B%SC^4O1{`;EOqyzJEM547%8p zNad`#rW32^h-r9Yy*O(wdwakfdjXCSrftq1JO$w2t5#*Ni*VO0jy7*5&`IhVWCX1;@>bTiRAym70F_%-52k-9pQFf#6 zFbmjY=d;*o(95~Wu=S+%rLn-o8Xg4j&~?;)Y|D^YPTIU^d3B?nN1cA*|ptmW2&C1Z#3%(*18eC;%~W;!qU6FT}E3pScF?fyUo~ZQ6@z@ zh*(rM(acE96d(-S)mGxRH$aczH`~Q`Aiqz<32&oEqD)zniK)In02o=vZE@frc;t7HK8z(7@PeKH`B(Hn2BzOJnU>;(Pa z-Yb#$z+DFPHhSS;Kcl+5tn*#R_!O`cSf2<|ckp|`;6d|f-n7H9c%Y-w`+?Wm_v(0` zU$`B|B!I8$$tIyMn(Jio)7P82H^*BDtI~ZOo42dM=Z_~E67zV7I_7u!-`s=tK*jgWxvYcBW#MlU*zEw%4=pYfYrkI?)GvIP;_w3Y$odO)fM|nVx{i z?s3i+u4toQA11bYNbFH4N#r%G6US7;`>FT>cwjQLWirt8UP84HJ9iwG5D;~TLcw4E z;L@TR3-*ZMj1B>>NYFXXOUkA)W}ePXicqucR=B@8TWMv;iU{vNUj> z_jE@DSLN%L)=H@B4L}uauWZ`6%YP|EC!?VyEB@tT)+(*mD{uD7-oC!dAUovYKO4|& zy^yBN8ZCZU8n|p6^s^Cy3npLa4cn<%N z_0xypdDgkdKGKwgQ=F9cZhlkR(=Fu(FCMj9l7*{=xtoT$tJ-8s*@OtsVS`B^dL>U? zHS4$WG$bgnu0&$F@bK6fcb%v#aGKsM*in~4BFor)@ol4D(kz9h(oIO-PP)n)5+LLm zVx`|7IEEj4Tjop%9!Q3S;=Ub~CEjBsmLUEKcm@goP`LBa8VGX>b?37GLQPj7e*Fz( zt^fP@_@HcPG}5J}({J;%!+>A&WXYk%XSNJOc%jC$cx&ISr3iOFgAdvez0e{bvB%i{ zA#RE{zz1fPUKxVvO4}SPTY!%MJmqgz z+nQBxDvvC}Zz`Uj$29xojU+%u%&2wB*KNpy186nF7!D2hKVAc_B-cQNLJ`J2XBRFJn3yks2d(Frc@o z%0%tzkKS~u5_>mt4HhwWm9lPtjW2_LN*C94SIWbdBVnXllC7yUh?hy0GdsOJn}4o& z-(T}?!r8v!3Rb*>yJ~Kqx+FcD&pK7yB=T3L0jyG3W943#y5nCq-Zp-~1}~8Rj;2-` z!Y_8qT4e7!pmvBDj=8R~Qzz8p2K+#V*j=lZqnw6_xpftsF&FdB02&MPy8eLXi?J>ZUC zuy@>nW$o)uJiuZ6m}oohfOpWHTQb}`0VlL~)4e!uW zx)pL>x$O$Pb2avQj5ek@V}l0*i2} z!Q#T5#u$lz$=A9bp>xrJrQj7PGDnO9?R4*bGJq5HNe~S{GdiX5jSg9S-kZOxNKy`F zte6;{QX@*wfie>z7VP;zZxShIgnWMy1^W_|uFNJv`~%4HlIGL8BBL1DGz6(bYnHBN zX-8~6>8(A6L#R3?_GvJ~iO~t)KY!-`EP-Fg0Gk6<>Fbi!yY0X)lnEg%2ZfR-qZ2ev$8 znPd%TUBoIDE*|w;w+S3<(8d?JrI0 zF5bQ9=Es$DEU9Jqa_cNmd>Mfm=b$ao`nL@?RPbWqAVIoa)G{*Gj>8-9E!SHtOa0$j zm}oKY(#JmL&T)s&fh-|tx36NzVUK)59O;`Tj;HVP%pgOX=wl+(htI6;0kL0x2Bh)6 zQiwOCXRRAdYkLk;0NkCBH4jNMIg(zKwV}wna1rFoQz3ec%H_6M6GJK zAKB(Hf*S5I@}S^+F}x5ruUSdZgB2`I+Dw6iRPS9Z6hf=sj~C7M1}vj@HylXBZZtzS z74F2BccNUxZ)tY?h*jw$uSGbLvDbuUWD^s^wp_`gC`v4QYm^nRIjj8KxRKY@vs&}V z{@O!ZGz$++8u?wo=3#6q9?+U4N@smwtS&2I86Veiu=;6tqV(-Vc-7#?k zeYVmlN2m#Z%aeQSEL;C2+)ETgo|5>Y#}0++uqOaM!9`l3_wue|zU83@wnisxE~Kr&1IqQl zd2FO(M)-fuS8(*ht*3 z!2KPfo&0oQ$Wbgp1s1CTXl}^Vl}9smN)tEGG3Zf0`^Yit)7{<7aFJd^8Vnk*PahNa zHC|Gb7y%6QHRAty#sRHUn3xP0Cj9@OKE78{m%4@?Izu^|bOA^WX18i`xkblc_2k?C z4BO%D^qYN7HC^FV4ijvOTer{NauF7us{##{GLIArO}4h*S}R*yxkd7;mf0#s+WHMr z$(B~tBadKGDyf$dQKGLQLez`;QfT;kH8+A>jO&;g2ghCEd!!+3J~>vUsuZBX@QuIqSFVEke72JS2e2TE)Ad6A;8{Cfj-j;+6-RXvYtA!pq9wF zmn%54C_6EK^C*e(sEBeafz*UTD6*($w|_~O^uC|X(Dpf11dknc|JK_(-hmw|ODy^$ z4KloIGW=kyh}l+v<%=6DOXn+o@CN3T zE=W!)K|C|7I)7_AQ(sC}-Ikt{he$SpTNS||5@=Fzz;$d4@E~_~?7?86bP%xzpZ>8Cu zUkg;n&Pf8mpf0@ASr#9_iUF3myz{BNclY2W<|t5YP&y~-KPS)sQB0Zh5d~#pKZ;uP z6Yk%AKsy6x+Q?6FrR*MbI@q)=O?ckLL}y52RPcFu9t5k0`#C(lKXK+9+7SgqT%Of^ zfeZvtB-Azu$zhZL`~I=(6OqK^VjY#&Xp$mc-ff7 zXG+AHK~LA8Ue0gB$${ps1d`@2_Wj_u^dJ7!DCgZ0b_ZduE>n?>H?+EV3i_{TIx-?o zqvJRkVKHbI!1*PVi68XC7X0=s3ep?;NWuDzUj%6t_0qkY#VI_k5Qd}vc|slt8aLpA1zQ!r5^OxHzwyG+3fj=%-XTlsFNu8o&&JfBl{Xe5M(6(U;P&!9O+wjM{? zC{64iHQ7I^1z$!_Uq*dab-A<9H5;?qWlrTG+x@%3HM7egt3H_<`S@fVJML$&z;zs# zgR?HXC?n}}p6`0jpp`gAF=14%meFNnPI*R=oha=#=o~KK7FtxUz+)={%y9JYXvaSY zLMiGjma9tpq4Phrlrt*qw&jV>%8XHYv0=m?vusFbx>PxQ%xc@LZtK1m;Yg#@E>$up zi4v;;r7rlGEZ>a@2&nvZO#>Zm1GBOJB%-^J0{Bp2=j*;9l)hmUENO9j1Tbatb#3x~ zwbg!`h1KQ#0=JFcj4kn;!|fgaK_G}nIDpv7jd6G=68rcalmWF?oQMD~M3_117fU2X z!hIP1pJtA(9Bg~I!P3Y^M;RB}VfT~2H#WoGY8d#Db;Z34$zghyo(7Ti!dPc(t?+$` zDk%@46TcUe0`*inX6w0j>io50LUqVu!Jx#`TW5nJ4{_r!fU{(9DKH~wEM+3iLXH`_ zN1s1k#Ebjqvl)D0c=P5T@88*B-Bu1rl5^_l9bYBLwWqok4m1H!cqw)K!h9f&Xe+*JbWCfGfOx9s$K*0q;5zda$Xs=mPutf(Z7+L@lJnT@iP_{L7lLcl80vpSpo`@`!{O`7L zzo8LDp`vQa0j&>uiwZGPcw2wd*B9d-0~;fc;}_Tw1^MJ7sQ)HAfE8zWmKXtB2_KklZ-KHY`|S%%M|LhT*`xfVB2O0*! zv(btvWOJSObv=t6{4E!UjY!o`r^g$5tCG$7N;l0~_}ghm9>Q#6MbzCj%)QM5EA^D) zh0FuUjLJWyHK;#*rsG#lJRI7vmp460?OQ6Qo#Xz|{L4(xX7q|YFY&H~XS_pTqG)Ze z(z7_RV^%oY`_>QoRBd_`V<{w*dfW93lc}ShU%Q=irelJC&kQ#TT1{O+Q*pz$}q&Mlbwx;SA6t8(FLNK;QbOn zZ!D^lK=#8LU0-8Iv!9PI3Qp|i%`6e`IjA+`bKm~^@WGI9b1+Zg_>9IGhHWB=$fwHr z`$Z+P}0Y8_*o-TquaH;?9p!f4)nTvj*;i^bIfvq-14&q;X4B%Y~!ck(P>lB zo0uK=m6JS)H1H#Yw?Z z8ZJWR^>LG`&=dOYdc3n%If-BAjDgXx%;3XpqgNz@>aI5E3E zWGLxJM?3{OcyH3o3OOdOj~Ip#^Y)Y4Kim-w+9=3Z#R`CoN?z+f7X_=_x(omM#=bI(7KPTixBrMajd! zLSDzjK-E=Gl98J?A%Q@QC5mXbigKTs?2s8@^&^o>+Ew~tk)4A)Ya>hUWCqF-!7)of zZ^={iB>eaCzP{TdsK#fph262WxsM-1(|9N$RBOVyQGZEXb>FNLy>?0CC5K%C8UBGE zB(ip>C@BH=KQdS(s|ITwsKCuyKxh^1{UVH(*JH^UjsPX|=25V_4Cfj7`us5eHiS4> z`$D)=DGzES^e|h8JfPes9Y8`tNVfhhS^Uu9Z@$O%)vsSX8az{KZ{U`ca~}8SYQ%l6 z+l-raF`TH_qdUqJK<1~I?`dV(Il(F*(IPN>FiK9@inH~mC9QN0q58BAqv8q^g*cLd zQV+^R@<($KrMrPERF~V(zUcRSU+%PZ-mxRbsEgaMJH2U=!92xbPmpUHhhLl)V zw(f-H$wXK`G~Pop`4V-+D@V27ef4viBelFMmI@}x$6*DYSb%|~tZRI>Z$}6hCW>s^ z^&B>|e=8cU?fr<|@SKQ;j(`BhWkLX-{_C;ROf9UzkbUu@V12~h++LZKlag@U8?+K8VBx}A{3%=`ccfI=oVTckhy(Fp zf?U&&w)2xmNxA2*M!OJA_9Y`hzB&3J&ul8n-now`SeDM4Qmnv?lDIw)T8gCob{-Yh zP5;_f3MSGco{HrGo9M4lCT~W7+_ys{_4luj?Sm%q(CHuan?`Y8$KEH=yc7bfAz=%W)B`QZBG>~ek*Qf z%26(s-G=9-w7fTYTTC-l&(hZ}c)~uFaip1QVnk}Hf$vSlyUVzwNh4q-}`x`K|bdq%XEAN-49`@WplG7y(K^#K`c2yH0|ZxW9zBv6-a1jJYAG zp1h@l{(C8PGo@ffy+CE-cgT#sm@U(h1LJYY(0-NTLAAW`4DD62)UTa8-XA{Gh2ArT z-r~bLua_m)qsnf(GN@l+HnJGWFyWz3z0)41bZ9hl%w&McWI)j(1)0CR^FkF0A2GcF zBl#c%Lq%c)U{NtB5@NWy{DClMe1Vx)gxv&0l&p_Z052{Iz02Jx=;wMl>C6#TgDEgd zFI5-3z}sZ_+F6T{FxOD zU5yV}N2EjV4v#$Mur`bNJ3#m$_!DrN!!k)S;_!J1fvL-uwseIo>m@)hK?8Ea(m}U0 zGXMgUltS&zmZTX8^SIne4+4F|ea1UG)neMn{1gG#aaV_h^kO-J{~Yt(41v=U{~x^z zBpLl>ualr15e~or15q!NDn|b%Ft^NK3NP;iN~>-=FQ;CtD;^eXdS+*LpD&h&`7DK2 z5BwiI4}mZJ?k+j5B$7pc^Q{J|gO-XdSiOn331lg@bbWgze6;T9GY%MRNnXDg`c$TWkjI0dp$Zd(0T@u?GrznpaEYNWb6CD$Yg)0Sp zy$uum>|=t8ZRE`TxY&egh~GwP5kQx@yu6}$w6*7X+rQ{@?Xb}1HoadTCbg{_fcS5b z`2z+{pV-}~bk<%0=*1cq*G4Ic$_g|_h5}3P0$5;iI(6sMr51D}hwCR3-9YBsX#n1H zwbc3vzMSSqm#}}l?!PG(VorH@ux3dcpXqr7m5>gOQ$Y(n0#w!TEOtx&#rE#trLf_= z({<|ExTw>2!)6$+^{=p(Nm7ej;? z+`93(uQan|-p*S&3+fJAVd81%!beTB@jhwiRYiCc0{qnq!EA4$X`OGsY3IFIIoS=Q z1ciYjHmH5l??%#1b#TQJ3H+R&UoKuybWOTg*N9jcmL*1ky+Kj?aWTHacwn<56V`>t(OK%Vq9NZcBxooEo};7r~hQrZiahkK(#F4wSFM2 zGbQltbCg!nP&&7VO*&w^iXs}Ps)}(8$y&%3e+{1;lQR!-RZ=w5YjISZs%mq|Ez0(! zTVKmOLwZi}-8_Q!I8Of|Cf`Q=zPBaGMeN_D}M7T zyb8TJR#}mh4yg>0WL7FH5G11|Me&|bt2aCxDP9MJ*^ihV57&3L{*|eq|J@%>Nbs!% zA9(3u76^G22etx=uYPY#UByR$=@Yr-K~)Euoskp89MI4{U7QbP&Q=)SZ>kgV*c+bI zJ4cjTSKP%zMPGyKzB1l2d3GldG$(FpW`xl-=ED3lTYN~Fjy<@a3@A?eo(#My4c~o7 zLG-RP+DA?IKJ3t-dzXFE3@6wyHNAG?BEj$X%ut+W;!If82MK??-jj(k+%_{!g1WaYef*RZ4>(l+VTU5b+0@F&=jE1My| z*Gfqdzu?luvVjarkCx0s%^}BDPn%uU_N$ZJ$x=MaNYDEtE6q=4_Ppkg8QPh06{B{g zGJD-yU_M6kU$khg2x_0;R(Cz4ZFHNP1jQG!WCpL8-aS$qzYN7M=Ef{N+d&&F^E>@X zBj@h*4|B9o;2PG{V>pMpf7lb~dZ%kl?W>JwgoDGJRhC3>-~@x+sR(Z^5W!Wb7=~md zQV}lp@Ax)M#p%H+t0LktD>W2In3g8+^Iz9{Z{r>$vR>}5853~0UY?d4$Lye@zAkQu ziw9Jz`Utudnc#dl4Ppzt#F>@l~wN$B=pF_V{y)ixgX{OUXhHU;^B#wqaHKz&FVO%sNud1kU%VZE9>kl0LzU0;sHCg zWlcN}c1n#+hChquny5=vP;qagci6pxAS!UJ};I>!olbKe0G%f$dAGNg{g-BnYi&| zm(IbO3N-baT6pj|!e`q=Y!yZ)cXK$lJa+nLrV$(d8#lkc76ZwhXy4MBAD*z0NUC%n zF4f+Kj__e(H0?BC@5F2QTDV$5HgX=i13kh(%AJkdNSSDS+Q)x!c2c^Zp#;O8xt(~h zCE+1#P~DTwoDnhFL&W#t_kS_My85z_za=aZ^|HITwnAKZH|rm(riDC&+uXZ~!f^#R z{cdbWE0!+qhVWv16MW88+PjxPcr3O06P`?n_9WjG~JN% zdK^0rGeY5@s%}NEtZt)KccV;gyfCXNu$|x{GEKBKC@v>{pCFahDxuhz_BZ3%6rk_i zIm!z@%Py3TGEv21eammEY_|V^%(4ak?E9L)sGDZS3Us82JkLtAXuj;OlWMT?x&sTd z*6ND=)WR9A@dv3N)#bOoH^SN;XOrPlTrK_ed`YoN`1JYEtz zgsnJ8{3=^QD3+0+*`FPEIiLXPqO~T7%f{eZa=efyr$w+>B_`x{Dy26v@;obTmplm+ z>!|xM*0a)y3-+&Y_CJB@+)TX(XLY30jhuvoJobyg%qH9D4_kaUTFlqLBG;Q6&LfZJ z`k!P$<%{wJH$QVXmvVRPjD^>8XB6fi7oh1Z%$nQi8_5H>Y+h^I&Flzh^N7A>p`6vCP+=zo)0L1JvuBifCbZ;-7O!1I{mR3u1T3W-D8eAQzkWNJ73xH5*3lZu5?o z;7tbowgA(|=KGJ#MUyG_OkrnlrgO2geFtgl^M`YKTFlwn%pTOV~2pO zyHm_?S;I7mM62EzPldW(u8jW}|#3+FqRvJJ%%Fqz-2fZ_Z@uxp?u75>?Dia8(Nb#idTj zW;=A6>tIRFq1uay<*MW5L6g}v^!aEiR%+#9(sBQ$&8UL-NzLy1hDJK!-7~-YX~X zRmYRHj)9(G2NEMV!K-_W|3%kVMb#B8K@tcqL4vzG1b4X*+}$C#LoOOL5D4zB!QI^@ zaB+7C5Zv7z=H$IuBWun4o&VFjySl2ndvBchuJG(|{RCz8@5a+^|Ckw`_h^!UPIY!_ zMN3ZIk~ML%qdp|nhMH3;BYpeTxA29f3H=u9tK%Nq=5$43JzIW`kuY0)EEsd;U}Q2AX!34QU`-P=ky#-#bwI5Y`DN`e666x7?@6c*rKBt*>I_J8x0TJl>22)_$m-)jNhRt z;#S83vuJ^v5WN08ybs06ZAN-rmkYrstR)}tH*~oJt_C+JaO|WYYH@^c9Hf_-7CUyP z;HaFPX50q+JG#t*w>SiFV z*rPoa2)Hk@uC{b})=WG^RMN^?+@}mHea1>zRK)x zY{tO!vJysJp0 z=!173WZ;YuLTMj{IWJYqz!G;g{=;r1w6_9*cF)i%N$IWgO5MT;O`?7cgc;Uy(GmXq?R zzec#u*<3@(akx~Kku&v$Mi^WM7>)WEo52zlLUEcE#X7zAs!r+kVn0+Q8|m;R=~r{t zuMemwaZbKff!ikn&6-3h-L7KB{4n8#av>mEg6|@-T`` zTaP2$8a!36lmIJl%Kufl8EY;(Sa4iV9eh!%tc<>i=0#sy9AhX06>1^MhO;9%#XGZa zv1Eu`97uil+UcX+5P@Pn`zeyd`)j;|d)0BG6MIv>(WIvAjSr&JWsT42vpK<$!?KwvDfj7&FFJ&VOg}Ds?m*78*aWyZ`&0x7d@rp;vWI-$iD8ll2gu5Sw|F zh?v-YSF1x(1(+O8KLO0qU&9aOw}#E&)!eaYug;f#)2#Nl+0%;ee4S63i^xyTlu93^ zh*(jC65NfO>M~(*?-GK}oH2@aFE56RDI;Zyim5kx`l+O6)1V<6vjfoJ&nH81QR+ID zVHqifROZElsWz$v{IWk$v|MSRz<~;}`YY6m_6Suh^3!;T7q-3l<{(JM%!%~?Kf7XM zArUB4oE3@29CjT`aZZXvk^fRf{F$Cw*j+RPe)B7$1ch#-%6tWeo36$v39NW;PuV`N zir%|2?ofRkjpXBW4`bWz%H-Sc)!;V)!c8S_ZLII*LL8#AG5O@%OZ$ldKZ44JE6avY z%aAUc!yXl~TS!Je|D-1n)s7@Cq|x%*&@I zbG!^kKEbAwl8w9AJU?MtF6jenW}cO(&`L8rHlOpWjerviFVmS%Ww=kHSva?u@!0%l#%BQm{kJavH^MI8(hc%Vg@uNM+*U!ka<=vGD&XH^b46MXviG3;3JNp_$ zd>uF;l=)j!!2OdhwK2;5X8WEfCpU4L63$lo2l?R=C5~}>8U7dR?J0K{w~jb>Ya?4j z^b*10p31`=p6hTuxShZwntoTTqqI=xKB{IDjA*_5Jk5t<2|g3of=0sp7n-?j2_7>E zZ;q8b@c`#^OH#u+7~u znwyCC(K|Ab5|=IQXyiHSY`N>|3Z|_SYE$rPw3ktf#jwK$vEJzh^DHr(49EcSEkpf3 z31@jKS3;e!hQDZYMDhyDbxuSI)cuqgU1_P?-NP7YK_P2p+v_*4%z&iY-Q z?U4A@Dwei4Y);}wa?(WyT8wG`T=zj~HK&aeFUW5vo<^I#xgqfnLd=en&=y1cMQU^! zPd*3du7Iva1M$(hy%a~suq@e+1yWH0Qd)X_q1_8p9iM`QCzg7A zdqgTyYk}%=LKZa;*Cv?!w*fjT{Iz;~?{x2%{_CzSp_})ENzB^bP;9UoEAeoPOEKNE zw8t!>rwmFSQ<@1s2UNPe(f# z8GeWfj|)*sVHzXAZZV&TyEJ%=zzUN46JX^vnXTZQ9xTFmb;%UFFw}GE2{i&Vw@@~- zn?bA_P@h*qVWcJzpD{GXS0G^0$j5=i9KyVp{Ubj+IIn9crgTGN&G1OschzuFF^c1A zNcgHOi4zoY0)%Q<9eO?gk ztN5?#_xc`;zs!aE>?$EYDbnp>-8zi2D_QwuNBkl zIGLms3&pMx^x9{vSY)V?2C0XS@>*!ZUD$}NgwEi#Rq?%&5v6aUxl(23TfnkAiHq`` z$XG7VOmK{tn%h5=7c%*3l_Z3!Az#?Yz~|$-;(b2B@#W~EN5(x%{&t-gei zt%~GVUi0Q)yAOao+#cpTLh-!dWcKEsa+FMtaBF7H<|ab9Oj6QGFV(!^`VQL8Z4Wt}!K%GbZGLGE0ABrnQM5d!;Nq#`OsL-fp_!^CjfLSK5{z?48}>Nbokb3Jq4C6nr`*KoFAZ{m0StjjcdZ z|1wzp;*B?68#2RN0os4fH2QR)uGnpo+2XT4#YYc(ue@lt?bsvD@-Vy5W33cwJ-R7I zHo~TU+Pn;4jDI;)ooKLtb>02>iGfv3F0U1d#cdlF0~z=&2rdj1)`==#2s|}7ag)1C z!vjQqI1VPp(t*uZQ*|Z%f2u)4RAM=P^Yk>n_myKR;57`WJB0x4z%sdvN!ng zJ*`!5ho{wG&_4X6sVd@f8bdrwa3QUPA=2gY)lHF-V5`lHsjZ>cNva-s6f(bbo&MB6 z3LuvEbTYLqHLb<^3i7~b+~ujoh#WE+7)_!*#_%9U=N2gZxYwyO6b5VJhg%MER z<3B5NKiPP2uES~f7DoY?uMR9T+WKBs0i}Q;OcQs+UgrJge#%c8x0+vu+53v{YuWD6 z$mhTcihX?|V=PhH3;^nTi?i?zlp$iwNq!w>RYiVot5xFaKebij`}5H~oA*4tG$u20 zDp&yXCA$&A5s6p?zVPJ=wS+x^HBrUS(j3pxl)hImQH1HpYpx{AW#7@?CIo=uBCjU%!zU&Fi@TWPB+Wd;-6l*-?%NE8X~R=T8L*yBNy{@J(!t}G9Zuyz{DIdG(t

  • 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�P7{G7XP!=inbiY) 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 x&r+ulC$(dI002ovPDHLkV1g~*^^*Vq literal 0 HcmV?d00001 diff --git a/apps/models_provider/impl/xf_model_provider/model/llm.py b/apps/models_provider/impl/xf_model_provider/model/llm.py new file mode 100644 index 000000000..db46a12a7 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/model/llm.py @@ -0,0 +1,78 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py.py + @date:2024/04/19 15:55 + @desc: +""" +from typing import List, Optional, Any, Iterator, Dict + +from langchain_community.chat_models.sparkllm import \ + ChatSparkLLM, convert_message_to_dict, _convert_delta_to_message_chunk +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.messages import BaseMessage, AIMessageChunk +from langchain_core.outputs import ChatGenerationChunk + +from models_provider.base_model_provider import MaxKBBaseModel + + +class XFChatSparkLLM(MaxKBBaseModel, ChatSparkLLM): + @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 XFChatSparkLLM( + spark_app_id=model_credential.get('spark_app_id'), + spark_api_key=model_credential.get('spark_api_key'), + spark_api_secret=model_credential.get('spark_api_secret'), + spark_api_url=model_credential.get('spark_api_url'), + spark_llm_domain=model_name, + streaming=model_kwargs.get('streaming', False), + **optional_params + ) + + usage_metadata: dict = {} + + def get_last_generation_info(self) -> Optional[Dict[str, Any]]: + return self.usage_metadata + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + return self.usage_metadata.get('prompt_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + return self.usage_metadata.get('completion_tokens', 0) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + default_chunk_class = AIMessageChunk + + self.client.arun( + [convert_message_to_dict(m) for m in messages], + self.spark_user_id, + self.model_kwargs, + True, + ) + for content in self.client.subscribe(timeout=self.request_timeout): + if "data" in content: + delta = content["data"] + chunk = _convert_delta_to_message_chunk(delta, default_chunk_class) + cg_chunk = ChatGenerationChunk(message=chunk) + elif "usage" in content: + generation_info = content["usage"] + self.usage_metadata = generation_info + continue + else: + continue + if cg_chunk is not None: + if run_manager: + run_manager.on_llm_new_token(str(cg_chunk.message.content), chunk=cg_chunk) + yield cg_chunk diff --git a/apps/models_provider/impl/xf_model_provider/model/stt.py b/apps/models_provider/impl/xf_model_provider/model/stt.py new file mode 100644 index 000000000..301a77bfa --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/model/stt.py @@ -0,0 +1,171 @@ +# -*- coding:utf-8 -*- +# +# 错误码链接:https://www.xfyun.cn/document/error-code (code返回错误码时必看) +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +import asyncio +import base64 +import datetime +import hashlib +import hmac +import json +import logging +import os +import ssl +from datetime import datetime, UTC +from typing import Dict +from urllib.parse import urlencode, urlparse + +import websockets + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + +STATUS_FIRST_FRAME = 0 # 第一帧的标识 +STATUS_CONTINUE_FRAME = 1 # 中间帧标识 +STATUS_LAST_FRAME = 2 # 最后一帧的标识 + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +ssl_context.check_hostname = False +ssl_context.verify_mode = ssl.CERT_NONE + +max_kb = logging.getLogger("max_kb") + + +class XFSparkSpeechToText(MaxKBBaseModel, BaseSpeechToText): + spark_app_id: str + spark_api_key: str + spark_api_secret: str + spark_api_url: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.spark_api_url = kwargs.get('spark_api_url') + self.spark_app_id = kwargs.get('spark_app_id') + self.spark_api_key = kwargs.get('spark_api_key') + self.spark_api_secret = kwargs.get('spark_api_secret') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return XFSparkSpeechToText( + spark_app_id=model_credential.get('spark_app_id'), + spark_api_key=model_credential.get('spark_api_key'), + spark_api_secret=model_credential.get('spark_api_secret'), + spark_api_url=model_credential.get('spark_api_url'), + **optional_params + ) + + # 生成url + def create_url(self): + url = self.spark_api_url + host = urlparse(url).hostname + # 生成RFC1123格式的时间戳 + gmt_format = '%a, %d %b %Y %H:%M:%S GMT' + date = datetime.now(UTC).strftime(gmt_format) + + # 拼接字符串 + signature_origin = "host: " + host + "\n" + signature_origin += "date: " + date + "\n" + signature_origin += "GET " + "/v2/iat " + "HTTP/1.1" + # 进行hmac-sha256进行加密 + signature_sha = hmac.new(self.spark_api_secret.encode('utf-8'), signature_origin.encode('utf-8'), + digestmod=hashlib.sha256).digest() + signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8') + + authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % ( + self.spark_api_key, "hmac-sha256", "host date request-line", signature_sha) + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + # 将请求的鉴权参数组合为字典 + v = { + "authorization": authorization, + "date": date, + "host": host + } + # 拼接鉴权参数,生成url + url = url + '?' + urlencode(v) + # print("date: ",date) + # print("v: ",v) + # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致 + # print('websocket url :', url) + return url + + def check_auth(self): + cwd = os.path.dirname(os.path.abspath(__file__)) + with open(f'{cwd}/iat_mp3_16k.mp3', 'rb') as f: + self.speech_to_text(f) + + def speech_to_text(self, file): + async def handle(): + async with websockets.connect(self.create_url(), max_size=1000000000, ssl=ssl_context) as ws: + # 发送 full client request + await self.send(ws, file) + return await self.handle_message(ws) + + return asyncio.run(handle()) + + @staticmethod + async def handle_message(ws): + res = await ws.recv() + message = json.loads(res) + code = message["code"] + sid = message["sid"] + if code != 0: + errMsg = message["message"] + raise Exception(f"sid: {sid} call error: {errMsg} code is: {code}") + else: + data = message["data"]["result"]["ws"] + result = "" + for i in data: + for w in i["cw"]: + result += w["w"] + # print("sid:%s call success!,data is:%s" % (sid, json.dumps(data, ensure_ascii=False))) + return result + + # 收到websocket连接建立的处理 + async def send(self, ws, file): + frameSize = 8000 # 每一帧的音频大小 + status = STATUS_FIRST_FRAME # 音频的状态信息,标识音频是第一帧,还是中间帧、最后一帧 + + while True: + buf = file.read(frameSize) + # 文件结束 + if not buf: + status = STATUS_LAST_FRAME + # 第一帧处理 + # 发送第一帧音频,带business 参数 + # appid 必须带上,只需第一帧发送 + if status == STATUS_FIRST_FRAME: + d = { + "common": {"app_id": self.spark_app_id}, + "business": { + "domain": "iat", + "language": "zh_cn", + "accent": "mandarin", + "vinfo": 1, + "vad_eos": 10000 + }, + "data": { + "status": 0, "format": "audio/L16;rate=16000", + "audio": str(base64.b64encode(buf), 'utf-8'), + "encoding": "lame"} + } + d = json.dumps(d) + await ws.send(d) + status = STATUS_CONTINUE_FRAME + # 中间帧处理 + elif status == STATUS_CONTINUE_FRAME: + d = {"data": {"status": 1, "format": "audio/L16;rate=16000", + "audio": str(base64.b64encode(buf), 'utf-8'), + "encoding": "lame"}} + await ws.send(json.dumps(d)) + # 最后一帧处理 + elif status == STATUS_LAST_FRAME: + d = {"data": {"status": 2, "format": "audio/L16;rate=16000", + "audio": str(base64.b64encode(buf), 'utf-8'), + "encoding": "lame"}} + await ws.send(json.dumps(d)) + break diff --git a/apps/models_provider/impl/xf_model_provider/model/tts.py b/apps/models_provider/impl/xf_model_provider/model/tts.py new file mode 100644 index 000000000..dcd51a907 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/model/tts.py @@ -0,0 +1,150 @@ +# -*- coding:utf-8 -*- +# +# author: iflytek +# +# 错误码链接:https://www.xfyun.cn/document/error-code (code返回错误码时必看) +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +import asyncio +import base64 +import datetime +import hashlib +import hmac +import json +import logging +import ssl +from datetime import datetime, UTC +from typing import Dict +from urllib.parse import urlencode, urlparse + +import websockets +from django.utils.translation import gettext as _ + +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech + +max_kb = logging.getLogger("max_kb") + +STATUS_FIRST_FRAME = 0 # 第一帧的标识 +STATUS_CONTINUE_FRAME = 1 # 中间帧标识 +STATUS_LAST_FRAME = 2 # 最后一帧的标识 + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +ssl_context.check_hostname = False +ssl_context.verify_mode = ssl.CERT_NONE + + +class XFSparkTextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + spark_app_id: str + spark_api_key: str + spark_api_secret: str + spark_api_url: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.spark_api_url = kwargs.get('spark_api_url') + self.spark_app_id = kwargs.get('spark_app_id') + self.spark_api_key = kwargs.get('spark_api_key') + self.spark_api_secret = kwargs.get('spark_api_secret') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'vcn': 'xiaoyan', 'speed': 50}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return XFSparkTextToSpeech( + spark_app_id=model_credential.get('spark_app_id'), + spark_api_key=model_credential.get('spark_api_key'), + spark_api_secret=model_credential.get('spark_api_secret'), + spark_api_url=model_credential.get('spark_api_url'), + **optional_params + ) + + # 生成url + def create_url(self): + url = self.spark_api_url + host = urlparse(url).hostname + # 生成RFC1123格式的时间戳 + gmt_format = '%a, %d %b %Y %H:%M:%S GMT' + date = datetime.now(UTC).strftime(gmt_format) + + # 拼接字符串 + signature_origin = "host: " + host + "\n" + signature_origin += "date: " + date + "\n" + signature_origin += "GET " + "/v2/tts " + "HTTP/1.1" + # 进行hmac-sha256进行加密 + signature_sha = hmac.new(self.spark_api_secret.encode('utf-8'), signature_origin.encode('utf-8'), + digestmod=hashlib.sha256).digest() + signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8') + + authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % ( + self.spark_api_key, "hmac-sha256", "host date request-line", signature_sha) + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + # 将请求的鉴权参数组合为字典 + v = { + "authorization": authorization, + "date": date, + "host": host + } + # 拼接鉴权参数,生成url + url = url + '?' + urlencode(v) + # print("date: ",date) + # print("v: ",v) + # 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致 + # print('websocket url :', url) + return url + + def check_auth(self): + self.text_to_speech(_('Hello')) + + def text_to_speech(self, text): + + # 使用小语种须使用以下方式,此处的unicode指的是 utf16小端的编码方式,即"UTF-16LE"” + # self.Data = {"status": 2, "text": str(base64.b64encode(self.Text.encode('utf-16')), "UTF8")} + text = _remove_empty_lines(text) + + async def handle(): + async with websockets.connect(self.create_url(), max_size=1000000000, ssl=ssl_context) as ws: + # 发送 full client request + await self.send(ws, text) + return await self.handle_message(ws) + + return asyncio.run(handle()) + + def is_cache_model(self): + return False + + @staticmethod + async def handle_message(ws): + audio_bytes: bytes = b'' + while True: + res = await ws.recv() + message = json.loads(res) + # print(message) + code = message["code"] + sid = message["sid"] + + if code != 0: + errMsg = message["message"] + raise Exception(f"sid: {sid} call error: {errMsg} code is: {code}") + else: + audio = message["data"]["audio"] + audio = base64.b64decode(audio) + audio_bytes += audio + # 退出 + if message["data"]["status"] == 2: + break + return audio_bytes + + async def send(self, ws, text): + business = {"aue": "lame", "sfl": 1, "auf": "audio/L16;rate=16000", "tte": "utf8"} + d = { + "common": {"app_id": self.spark_app_id}, + "business": business | self.params, + "data": {"status": 2, "text": str(base64.b64encode(text.encode('utf-8')), "UTF8")}, + } + d = json.dumps(d) + await ws.send(d) diff --git a/apps/models_provider/impl/xf_model_provider/xf_model_provider.py b/apps/models_provider/impl/xf_model_provider/xf_model_provider.py new file mode 100644 index 000000000..c93351e59 --- /dev/null +++ b/apps/models_provider/impl/xf_model_provider/xf_model_provider.py @@ -0,0 +1,68 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: xf_model_provider.py + @date:2024/04/19 14:47 + @desc: +""" +import os +import ssl + +from common.utils.common import get_file_content +from models_provider.base_model_provider import ModelProvideInfo, ModelTypeConst, ModelInfo, IModelProvider, \ + ModelInfoManage +from models_provider.impl.xf_model_provider.credential.embedding import XFEmbeddingCredential +from models_provider.impl.xf_model_provider.credential.image import XunFeiImageModelCredential +from models_provider.impl.xf_model_provider.credential.llm import XunFeiLLMModelCredential +from models_provider.impl.xf_model_provider.credential.stt import XunFeiSTTModelCredential +from models_provider.impl.xf_model_provider.credential.tts import XunFeiTTSModelCredential +from models_provider.impl.xf_model_provider.model.embedding import XFEmbedding +from models_provider.impl.xf_model_provider.model.image import XFSparkImage +from models_provider.impl.xf_model_provider.model.llm import XFChatSparkLLM +from models_provider.impl.xf_model_provider.model.stt import XFSparkSpeechToText +from models_provider.impl.xf_model_provider.model.tts import XFSparkTextToSpeech +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +ssl._create_default_https_context = ssl.create_default_context() + +qwen_model_credential = XunFeiLLMModelCredential() +stt_model_credential = XunFeiSTTModelCredential() +image_model_credential = XunFeiImageModelCredential() +tts_model_credential = XunFeiTTSModelCredential() +embedding_model_credential = XFEmbeddingCredential() +model_info_list = [ + ModelInfo('generalv3.5', '', ModelTypeConst.LLM, qwen_model_credential, XFChatSparkLLM), + ModelInfo('generalv3', '', ModelTypeConst.LLM, qwen_model_credential, XFChatSparkLLM), + ModelInfo('generalv2', '', ModelTypeConst.LLM, qwen_model_credential, XFChatSparkLLM), + ModelInfo('iat', _('Chinese and English recognition'), ModelTypeConst.STT, stt_model_credential, XFSparkSpeechToText), + ModelInfo('tts', '', ModelTypeConst.TTS, tts_model_credential, XFSparkTextToSpeech), + ModelInfo('embedding', '', ModelTypeConst.EMBEDDING, embedding_model_credential, XFEmbedding) +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info( + ModelInfo('generalv3.5', '', ModelTypeConst.LLM, qwen_model_credential, XFChatSparkLLM)) + .append_default_model_info( + ModelInfo('iat', _('Chinese and English recognition'), ModelTypeConst.STT, stt_model_credential, XFSparkSpeechToText), + ) + .append_default_model_info( + ModelInfo('tts', '', ModelTypeConst.TTS, tts_model_credential, XFSparkTextToSpeech)) + .append_default_model_info( + ModelInfo('embedding', '', ModelTypeConst.EMBEDDING, embedding_model_credential, XFEmbedding)) + .build() +) + + +class XunFeiModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_xf_provider', name=_('iFlytek Spark'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'xf_model_provider', 'icon', + 'xf_icon_svg'))) diff --git a/apps/models_provider/impl/xinference_model_provider/__init__.py b/apps/models_provider/impl/xinference_model_provider/__init__.py new file mode 100644 index 000000000..9bad5790a --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/apps/models_provider/impl/xinference_model_provider/credential/embedding.py b/apps/models_provider/impl/xinference_model_provider/credential/embedding.py new file mode 100644 index 000000000..1ca79e40a --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/embedding.py @@ -0,0 +1,44 @@ +# coding=utf-8 +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode +from models_provider.impl.local_model_provider.model.embedding import LocalEmbedding + + +class XinferenceEmbeddingModelCredential(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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base'), model_credential.get('api_key'), + 'embedding') + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, _('API domain name is invalid')) + exist = provider.get_model_info_by_name(model_list, model_name) + model: LocalEmbedding = provider.get_model(model_type, model_name, model_credential) + if len(exist) == 0: + model.start_down_model_thread() + raise AppApiException(ValidCode.model_not_fount, + _('The model does not exist, please download the model first')) + model.embed_query(_('Hello')) + return True + + def encryption_dict(self, model_info: Dict[str, object]): + return model_info + + def build_model(self, model_info: Dict[str, object]): + for key in ['model']: + if key not in model_info: + raise AppApiException(500, _('{key} is required').format(key=key)) + return self + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) diff --git a/apps/models_provider/impl/xinference_model_provider/credential/image.py b/apps/models_provider/impl/xinference_model_provider/credential/image.py new file mode 100644 index 000000000..ccb378c82 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/image.py @@ -0,0 +1,70 @@ +# coding=utf-8 +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XinferenceImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class XinferenceImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return XinferenceImageModelParams() diff --git a/apps/models_provider/impl/xinference_model_provider/credential/llm.py b/apps/models_provider/impl/xinference_model_provider/credential/llm.py new file mode 100644 index 000000000..aee80fee7 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/llm.py @@ -0,0 +1,67 @@ +# coding=utf-8 + +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XinferenceLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.7, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=800, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class XinferenceLLMModelCredential(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)) + try: + model_list = provider.get_base_model_list(model_credential.get('api_base'), model_credential.get('api_key'), + model_type) + except Exception as e: + raise AppApiException(ValidCode.valid_error.value, gettext('API domain name is invalid')) + exist = provider.get_model_info_by_name(model_list, model_name) + if len(exist) == 0: + raise AppApiException(ValidCode.valid_error.value, + gettext('The model does not exist, please download the model first')) + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + return True + + def encryption_dict(self, model_info: Dict[str, object]): + 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', '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') + return self + + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return XinferenceLLMModelParams() diff --git a/apps/models_provider/impl/xinference_model_provider/credential/reranker.py b/apps/models_provider/impl/xinference_model_provider/credential/reranker.py new file mode 100644 index 000000000..af1026cb1 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/reranker.py @@ -0,0 +1,51 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: reranker.py + @date:2024/9/10 9:46 + @desc: +""" +from typing import Dict + +from django.utils.translation import gettext as _ +from langchain_core.documents import Document + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XInferenceRerankerModelCredential(BaseForm, BaseModelCredential): + def is_valid(self, model_type: str, model_name, model_credential: Dict[str, object], model_params, provider, + raise_exception=True): + if not model_type == 'RERANKER': + raise AppApiException(ValidCode.valid_error.value, + _('{model_type} Model type is not supported').format(model_type=model_type)) + for key in ['server_url']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.compress_documents([Document(page_content=_('Hello'))], _('Hello')) + except Exception as e: + 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))) + else: + return False + return True + + def encryption_dict(self, model_info: Dict[str, object]): + return model_info + + server_url = forms.TextInputField('API URL', required=True) + + api_key = forms.PasswordInputField('API Key', required=False) diff --git a/apps/models_provider/impl/xinference_model_provider/credential/stt.py b/apps/models_provider/impl/xinference_model_provider/credential/stt.py new file mode 100644 index 000000000..3cdbb53f2 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/stt.py @@ -0,0 +1,47 @@ +# coding=utf-8 +from typing import Dict + +from django.utils.translation import gettext as _ + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XInferenceSTTModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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, + _('{model_type} Model type is not supported').format(model_type=model_type)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, _('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential) + model.check_auth() + except Exception as e: + 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))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + pass diff --git a/apps/models_provider/impl/xinference_model_provider/credential/tti.py b/apps/models_provider/impl/xinference_model_provider/credential/tti.py new file mode 100644 index 000000000..20e899493 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/tti.py @@ -0,0 +1,87 @@ +# coding=utf-8 +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XinferenceTTIModelParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), + _('The image generation endpoint allows you to create raw images based on text prompts. The dimensions of the image can be 1024x1024, 1024x1792, or 1792x1024 pixels.')), + required=True, + default_value='1024x1024', + option_list=[ + {'value': '1024x1024', 'label': '1024x1024'}, + {'value': '1024x1792', 'label': '1024x1792'}, + {'value': '1792x1024', 'label': '1792x1024'}, + ], + text_field='label', + value_field='value' + ) + + quality = forms.SingleSelect( + TooltipLabel(_('Picture quality'), + _('By default, images are generated in standard quality, you can set quality: "hd" to enhance detail. Square, standard quality images are generated fastest.')), + required=True, + default_value='standard', + option_list=[ + {'value': 'standard', 'label': 'standard'}, + {'value': 'hd', 'label': 'hd'}, + ], + text_field='label', + value_field='value' + ) + + n = forms.SliderField( + TooltipLabel(_('Number of pictures'), + _('You can request 1 image at a time (requesting more images by making parallel requests), or up to 10 images at a time using the n parameter.')), + required=True, default_value=1, + _min=1, + _max=10, + _step=1, + precision=0) + + +class XinferenceTextToImageModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return XinferenceTTIModelParams() diff --git a/apps/models_provider/impl/xinference_model_provider/credential/tts.py b/apps/models_provider/impl/xinference_model_provider/credential/tts.py new file mode 100644 index 000000000..b1dc2f885 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/credential/tts.py @@ -0,0 +1,66 @@ +# coding=utf-8 +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class XInferenceTTSModelGeneralParams(BaseForm): + # ['中文女', '中文男', '日语男', '粤语女', '英文女', '英文男', '韩语女'] + voice = forms.SingleSelect( + TooltipLabel(_('timbre'), ''), + required=True, default_value='中文女', + text_field='value', + value_field='value', + option_list=[ + {'text': _('Chinese female'), 'value': '中文女'}, + {'text': _('Chinese male'), 'value': '中文男'}, + {'text': _('Japanese male'), 'value': '日语男'}, + {'text': _('Cantonese female'), 'value': '粤语女'}, + {'text': _('English female'), 'value': '英文女'}, + {'text': _('English male'), 'value': '英文男'}, + {'text': _('Korean female'), 'value': '韩语女'}, + ]) + + +class XInferenceTTSModelCredential(BaseForm, BaseModelCredential): + api_base = forms.TextInputField('API URL', required=True) + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_base', 'api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.check_auth() + except Exception as e: + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return XInferenceTTSModelGeneralParams() diff --git a/apps/models_provider/impl/xinference_model_provider/icon/xinference_icon_svg b/apps/models_provider/impl/xinference_model_provider/icon/xinference_icon_svg new file mode 100644 index 000000000..fc553ee3c --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/icon/xinference_icon_svg @@ -0,0 +1,5 @@ + + + + diff --git a/apps/models_provider/impl/xinference_model_provider/model/embedding.py b/apps/models_provider/impl/xinference_model_provider/model/embedding.py new file mode 100644 index 000000000..923971958 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/embedding.py @@ -0,0 +1,91 @@ +# coding=utf-8 +import threading +from typing import Dict, Optional, List, Any + +from langchain_core.embeddings import Embeddings + +from models_provider.base_model_provider import MaxKBBaseModel + + +class XinferenceEmbedding(MaxKBBaseModel, Embeddings): + client: Any + server_url: Optional[str] + """URL of the xinference server""" + model_uid: Optional[str] + """UID of the launched model""" + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return XinferenceEmbedding( + model_uid=model_name, + server_url=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + ) + + def down_model(self): + self.client.launch_model(model_name=self.model_uid, model_type="embedding") + + def start_down_model_thread(self): + thread = threading.Thread(target=self.down_model) + thread.daemon = True + thread.start() + + def __init__( + self, server_url: Optional[str] = None, model_uid: Optional[str] = None, + api_key: Optional[str] = None + ): + try: + from xinference.client import RESTfulClient + except ImportError: + try: + from xinference_client import RESTfulClient + except ImportError as e: + raise ImportError( + "Could not import RESTfulClient from xinference. Please install it" + " with `pip install xinference` or `pip install xinference_client`." + ) from e + + if server_url is None: + raise ValueError("Please provide server URL") + + if model_uid is None: + raise ValueError("Please provide the model UID") + + self.server_url = server_url + + self.model_uid = model_uid + + self.api_key = api_key + + self.client = RESTfulClient(server_url, api_key) + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Embed a list of documents using Xinference. + Args: + texts: The list of texts to embed. + Returns: + List of embeddings, one for each text. + """ + + model = self.client.get_model(self.model_uid) + + embeddings = [ + model.create_embedding(text)["data"][0]["embedding"] for text in texts + ] + return [list(map(float, e)) for e in embeddings] + + def embed_query(self, text: str) -> List[float]: + """Embed a query of documents using Xinference. + Args: + text: The text to embed. + Returns: + Embeddings for the text. + """ + + model = self.client.get_model(self.model_uid) + + embedding_res = model.create_embedding(text) + + embedding = embedding_res["data"][0]["embedding"] + + return list(map(float, embedding)) diff --git a/apps/models_provider/impl/xinference_model_provider/model/image.py b/apps/models_provider/impl/xinference_model_provider/model/image.py new file mode 100644 index 000000000..3b634ebfa --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/image.py @@ -0,0 +1,35 @@ +from typing import Dict, List + +from langchain_core.messages import BaseMessage, get_buffer_string + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class XinferenceImage(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return XinferenceImage( + model_name=model_name, + openai_api_base=model_credential.get('api_base'), + openai_api_key=model_credential.get('api_key'), + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + return self.usage_metadata.get('input_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) + return self.get_last_generation_info().get('output_tokens', 0) diff --git a/apps/models_provider/impl/xinference_model_provider/model/llm.py b/apps/models_provider/impl/xinference_model_provider/model/llm.py new file mode 100644 index 000000000..9c9550920 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/llm.py @@ -0,0 +1,50 @@ +# coding=utf-8 + +from typing import Dict, List +from urllib.parse import urlparse, ParseResult + +from langchain_core.messages import BaseMessage, get_buffer_string + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +class XinferenceChatModel(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def is_cache_model(): + return False + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + api_base = model_credential.get('api_base', '') + base_url = get_base_url(api_base) + base_url = base_url if base_url.endswith('/v1') else (base_url + '/v1') + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return XinferenceChatModel( + model=model_name, + openai_api_base=base_url, + openai_api_key=model_credential.get('api_key'), + **optional_params + ) + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return sum([len(tokenizer.encode(get_buffer_string([m]))) for m in messages]) + return self.usage_metadata.get('input_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + if self.usage_metadata is None or self.usage_metadata == {}: + tokenizer = TokenizerManage.get_tokenizer() + return len(tokenizer.encode(text)) + return self.get_last_generation_info().get('output_tokens', 0) diff --git a/apps/models_provider/impl/xinference_model_provider/model/reranker.py b/apps/models_provider/impl/xinference_model_provider/model/reranker.py new file mode 100644 index 000000000..405e83224 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/reranker.py @@ -0,0 +1,54 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: reranker.py + @date:2024/9/10 9:45 + @desc: +""" +from typing import Sequence, Optional, Any, Dict + +from langchain_core.callbacks import Callbacks +from langchain_core.documents import BaseDocumentCompressor, Document +from xinference_client.client.restful.restful_client import RESTfulRerankModelHandle + +from models_provider.base_model_provider import MaxKBBaseModel + + +class XInferenceReranker(MaxKBBaseModel, BaseDocumentCompressor): + server_url: Optional[str] + """URL of the xinference server""" + model_uid: Optional[str] + """UID of the launched model""" + api_key: Optional[str] + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + return XInferenceReranker(server_url=model_credential.get('server_url'), model_uid=model_name, + api_key=model_credential.get('api_key'), top_n=model_kwargs.get('top_n', 3)) + + top_n: Optional[int] = 3 + + def compress_documents(self, documents: Sequence[Document], query: str, callbacks: Optional[Callbacks] = None) -> \ + Sequence[Document]: + if documents is None or len(documents) == 0: + return [] + client: Any + if documents is None or len(documents) == 0: + return [] + try: + from xinference.client import RESTfulClient + except ImportError: + try: + from xinference_client import RESTfulClient + except ImportError as e: + raise ImportError( + "Could not import RESTfulClient from xinference. Please install it" + " with `pip install xinference` or `pip install xinference_client`." + ) from e + + client = RESTfulClient(self.server_url, self.api_key) + model: RESTfulRerankModelHandle = client.get_model(self.model_uid) + res = model.rerank([document.page_content for document in documents], query, self.top_n, return_documents=True) + return [Document(page_content=d.get('document', {}).get('text'), + metadata={'relevance_score': d.get('relevance_score')}) for d in res.get('results', [])] diff --git a/apps/models_provider/impl/xinference_model_provider/model/stt.py b/apps/models_provider/impl/xinference_model_provider/model/stt.py new file mode 100644 index 000000000..840dc5fdb --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/stt.py @@ -0,0 +1,57 @@ +import io +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_stt import BaseSpeechToText + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class XInferenceSpeechToText(MaxKBBaseModel, BaseSpeechToText): + api_base: str + api_key: str + model: str + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {} + if 'max_tokens' in model_kwargs and model_kwargs['max_tokens'] is not None: + 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'] + return XInferenceSpeechToText( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + response_list = client.models.with_raw_response.list() + # print(response_list) + + def speech_to_text(self, audio_file): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + audio_data = audio_file.read() + buffer = io.BytesIO(audio_data) + buffer.name = "file.mp3" # this is the important line + res = client.audio.transcriptions.create(model=self.model, language="zh", file=buffer) + return res.text diff --git a/apps/models_provider/impl/xinference_model_provider/model/tti.py b/apps/models_provider/impl/xinference_model_provider/model/tti.py new file mode 100644 index 000000000..566ddf89a --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/tti.py @@ -0,0 +1,63 @@ +import base64 +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from common.utils.common import bytes_to_uploaded_file +#from dataset.serializers.file_serializers import FileSerializer +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class XinferenceTextToImage(MaxKBBaseModel, BaseTextToImage): + api_base: str + api_key: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'size': '1024x1024', 'quality': 'standard', 'n': 1}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return XinferenceTextToImage( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def is_cache_model(self): + return False + + def check_auth(self): + self.generate_image('生成一个小猫图片') + + def generate_image(self, prompt: str, negative_prompt: str = None): + chat = OpenAI(api_key=self.api_key, base_url=self.api_base) + res = chat.images.generate(model=self.model, prompt=prompt, response_format='b64_json', **self.params) + file_urls = [] + # 临时文件 + for img in res.data: + file = bytes_to_uploaded_file(base64.b64decode(img.b64_json), 'file_name.jpg') + meta = { + 'debug': True, + } + #file_url = FileSerializer(data={'file': file, 'meta': meta}).upload() + #file_urls.append(f'http://localhost:8080{file_url}') + + return file_urls diff --git a/apps/models_provider/impl/xinference_model_provider/model/tts.py b/apps/models_provider/impl/xinference_model_provider/model/tts.py new file mode 100644 index 000000000..243203c42 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/model/tts.py @@ -0,0 +1,61 @@ +from typing import Dict + +from openai import OpenAI + +from common.config.tokenizer_manage_config import TokenizerManage +from common.utils.common import _remove_empty_lines +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tts import BaseTextToSpeech +from django.utils.translation import gettext as _ + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class XInferenceTextToSpeech(MaxKBBaseModel, BaseTextToSpeech): + api_base: str + api_key: str + model: str + params: dict + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_key = kwargs.get('api_key') + self.api_base = kwargs.get('api_base') + self.model = kwargs.get('model') + self.params = kwargs.get('params') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'voice': '中文女'}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return XInferenceTextToSpeech( + model=model_name, + api_base=model_credential.get('api_base'), + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def check_auth(self): + self.text_to_speech(_('Hello')) + + def text_to_speech(self, text): + client = OpenAI( + base_url=self.api_base, + api_key=self.api_key + ) + # ['中文女', '中文男', '日语男', '粤语女', '英文女', '英文男', '韩语女'] + text = _remove_empty_lines(text) + with client.audio.speech.with_streaming_response.create( + model=self.model, + input=text, + **self.params + ) as response: + return response.read() + + def is_cache_model(self): + return False diff --git a/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py b/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py new file mode 100644 index 000000000..749dcbeb9 --- /dev/null +++ b/apps/models_provider/impl/xinference_model_provider/xinference_model_provider.py @@ -0,0 +1,585 @@ +# coding=utf-8 +import os +from urllib.parse import urlparse, ParseResult + +import requests + +from common.utils.common import get_file_content +from models_provider.base_model_provider import IModelProvider, ModelProvideInfo, ModelInfo, ModelTypeConst, \ + ModelInfoManage +from models_provider.impl.xinference_model_provider.credential.embedding import \ + XinferenceEmbeddingModelCredential +from models_provider.impl.xinference_model_provider.credential.image import XinferenceImageModelCredential +from models_provider.impl.xinference_model_provider.credential.llm import XinferenceLLMModelCredential +from models_provider.impl.xinference_model_provider.credential.reranker import XInferenceRerankerModelCredential +from models_provider.impl.xinference_model_provider.credential.stt import XInferenceSTTModelCredential +from models_provider.impl.xinference_model_provider.credential.tti import XinferenceTextToImageModelCredential +from models_provider.impl.xinference_model_provider.credential.tts import XInferenceTTSModelCredential +from models_provider.impl.xinference_model_provider.model.embedding import XinferenceEmbedding +from models_provider.impl.xinference_model_provider.model.image import XinferenceImage +from models_provider.impl.xinference_model_provider.model.llm import XinferenceChatModel +from models_provider.impl.xinference_model_provider.model.reranker import XInferenceReranker +from models_provider.impl.xinference_model_provider.model.stt import XInferenceSpeechToText +from models_provider.impl.xinference_model_provider.model.tti import XinferenceTextToImage +from models_provider.impl.xinference_model_provider.model.tts import XInferenceTextToSpeech +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +xinference_llm_model_credential = XinferenceLLMModelCredential() +xinference_stt_model_credential = XInferenceSTTModelCredential() +xinference_tts_model_credential = XInferenceTTSModelCredential() +xinference_image_model_credential = XinferenceImageModelCredential() +xinference_tti_model_credential = XinferenceTextToImageModelCredential() + +model_info_list = [ + ModelInfo( + 'code-llama', + _('Code Llama is a language model specifically designed for code generation.'), + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'code-llama-instruct', + _(''' +Code Llama Instruct is a fine-tuned version of Code Llama's instructions, designed to perform specific tasks. + '''), + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'code-llama-python', + _('Code Llama Python is a language model specifically designed for Python code generation.'), + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'codeqwen1.5', + _('CodeQwen 1.5 is a language model for code generation with high performance.'), + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'codeqwen1.5-chat', + _('CodeQwen 1.5 Chat is a chat model version of CodeQwen 1.5.'), + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'deepseek', + _('Deepseek is a large-scale language model with 13 billion parameters.'), + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'deepseek-chat', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'deepseek-coder', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'deepseek-coder-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'deepseek-vl-chat', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'gpt-3.5-turbo', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'gpt-4', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'gpt-4-vision-preview', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'gpt4all', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'llama2', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'llama2-chat', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'llama2-chat-32k', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen-chat', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen-chat-32k', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen-code', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen-code-chat', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen-vl', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen-vl-chat', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2-72b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2-57b-a14b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2-7b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-72b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-32b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-14b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-7b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-1.5b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-0.5b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'qwen2.5-3b-instruct', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), + ModelInfo( + 'minicpm-llama3-v-2_5', + '', + ModelTypeConst.LLM, + xinference_llm_model_credential, + XinferenceChatModel + ), +] + +voice_model_info = [ + ModelInfo( + 'CosyVoice-300M-SFT', + '', + ModelTypeConst.TTS, + xinference_tts_model_credential, + XInferenceTextToSpeech + ), + ModelInfo( + 'Belle-whisper-large-v3-zh', + '', + ModelTypeConst.STT, + xinference_stt_model_credential, + XInferenceSpeechToText + ), +] + +image_model_info = [ + ModelInfo( + 'qwen-vl-chat', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'deepseek-vl-chat', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'yi-vl-chat', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'omnilmm', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'internvl-chat', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'cogvlm2', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'MiniCPM-Llama3-V-2_5', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'GLM-4V', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'MiniCPM-V-2.6', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'internvl2', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'qwen2-vl-instruct', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'llama-3.2-vision', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'llama-3.2-vision-instruct', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), + ModelInfo( + 'glm-edge-v', + '', + ModelTypeConst.IMAGE, + xinference_image_model_credential, + XinferenceImage + ), +] + +tti_model_info = [ + ModelInfo( + 'sd-turbo', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), + ModelInfo( + 'sdxl-turbo', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), + ModelInfo( + 'stable-diffusion-v1.5', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), + ModelInfo( + 'stable-diffusion-xl-base-1.0', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), + ModelInfo( + 'sd3-medium', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), + ModelInfo( + 'FLUX.1-schnell', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), + ModelInfo( + 'FLUX.1-dev', + '', + ModelTypeConst.TTI, + xinference_tti_model_credential, + XinferenceTextToImage + ), +] + +xinference_embedding_model_credential = XinferenceEmbeddingModelCredential() + +# 生成embedding_model_info列表 +embedding_model_info = [ + ModelInfo('bce-embedding-base_v1', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-base-en', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-base-en-v1.5', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-base-zh', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-base-zh-v1.5', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-large-en', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-large-en-v1.5', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-large-zh', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-large-zh-noinstruct', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-large-zh-v1.5', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-m3', '', ModelTypeConst.EMBEDDING, xinference_embedding_model_credential, + XinferenceEmbedding), + ModelInfo('bge-small-en-v1.5', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-small-zh', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('bge-small-zh-v1.5', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('e5-large-v2', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('gte-base', '', ModelTypeConst.EMBEDDING, xinference_embedding_model_credential, + XinferenceEmbedding), + ModelInfo('gte-large', '', ModelTypeConst.EMBEDDING, xinference_embedding_model_credential, + XinferenceEmbedding), + ModelInfo('jina-embeddings-v2-base-en', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('jina-embeddings-v2-base-zh', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('jina-embeddings-v2-small-en', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('m3e-base', '', ModelTypeConst.EMBEDDING, xinference_embedding_model_credential, + XinferenceEmbedding), + ModelInfo('m3e-large', '', ModelTypeConst.EMBEDDING, xinference_embedding_model_credential, + XinferenceEmbedding), + ModelInfo('m3e-small', '', ModelTypeConst.EMBEDDING, xinference_embedding_model_credential, + XinferenceEmbedding), + ModelInfo('multilingual-e5-large', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('text2vec-base-chinese', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('text2vec-base-chinese-paraphrase', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('text2vec-base-chinese-sentence', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('text2vec-base-multilingual', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), + ModelInfo('text2vec-large-chinese', '', ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding), +] +rerank_list = [ModelInfo('bce-reranker-base_v1', + '', + ModelTypeConst.RERANKER, XInferenceRerankerModelCredential(), XInferenceReranker)] +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_model_info_list(voice_model_info) + .append_default_model_info(voice_model_info[0]) + .append_default_model_info(voice_model_info[1]) + .append_default_model_info(ModelInfo('phi3', + '', + ModelTypeConst.LLM, xinference_llm_model_credential, + XinferenceChatModel)) + .append_model_info_list(embedding_model_info) + .append_default_model_info(ModelInfo('', + '', + ModelTypeConst.EMBEDDING, + xinference_embedding_model_credential, XinferenceEmbedding)) + .append_model_info_list(rerank_list) + .append_model_info_list(image_model_info) + .append_default_model_info(image_model_info[0]) + .append_model_info_list(tti_model_info) + .append_default_model_info(tti_model_info[0]) + .append_default_model_info(rerank_list[0]) + .build() +) + + +def get_base_url(url: str): + parse = urlparse(url) + result_url = ParseResult(scheme=parse.scheme, netloc=parse.netloc, path=parse.path, params='', + query='', + fragment='').geturl() + return result_url[:-1] if result_url.endswith("/") else result_url + + +class XinferenceModelProvider(IModelProvider): + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_xinference_provider', name='Xorbits Inference', icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'xinference_model_provider', 'icon', + 'xinference_icon_svg'))) + + @staticmethod + def get_base_model_list(api_base, api_key, model_type): + base_url = get_base_url(api_base) + base_url = base_url if base_url.endswith('/v1') else (base_url + '/v1') + headers = {} + if api_key: + headers['Authorization'] = f"Bearer {api_key}" + r = requests.request(method="GET", url=f"{base_url}/models", headers=headers, timeout=5) + r.raise_for_status() + model_list = r.json().get('data') + return [model for model in model_list if model.get('model_type') == model_type] + + @staticmethod + def get_model_info_by_name(model_list, model_name): + if model_list is None: + return [] + return [model for model in model_list if model.get('model_name') == model_name or model.get('id') == model_name] diff --git a/apps/models_provider/impl/zhipu_model_provider/__init__.py b/apps/models_provider/impl/zhipu_model_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/models_provider/impl/zhipu_model_provider/credential/image.py b/apps/models_provider/impl/zhipu_model_provider/credential/image.py new file mode 100644 index 000000000..acc1e4480 --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/credential/image.py @@ -0,0 +1,71 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class ZhiPuImageModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.95, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class ZhiPuImageModelCredential(BaseForm, BaseModelCredential): + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.stream([HumanMessage(content=[{"type": "text", "text": gettext('Hello')}])]) + for chunk in res: + print(chunk) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return ZhiPuImageModelParams() diff --git a/apps/models_provider/impl/zhipu_model_provider/credential/llm.py b/apps/models_provider/impl/zhipu_model_provider/credential/llm.py new file mode 100644 index 000000000..9be8a4beb --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/credential/llm.py @@ -0,0 +1,76 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: llm.py + @date:2024/7/12 10:46 + @desc: +""" +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext +from langchain_core.messages import HumanMessage + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class ZhiPuLLMModelParams(BaseForm): + temperature = forms.SliderField(TooltipLabel(_('Temperature'), + _('Higher values make the output more random, while lower values make it more focused and deterministic')), + required=True, default_value=0.95, + _min=0.1, + _max=1.0, + _step=0.01, + precision=2) + + max_tokens = forms.SliderField( + TooltipLabel(_('Output the maximum Tokens'), + _('Specify the maximum number of tokens that the model can generate')), + required=True, default_value=1024, + _min=1, + _max=100000, + _step=1, + precision=0) + + +class ZhiPuLLMModelCredential(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)) + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + model.invoke([HumanMessage(content=gettext('Hello'))]) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + api_key = forms.PasswordInputField('API Key', required=True) + + def get_model_params_setting_form(self, model_name): + return ZhiPuLLMModelParams() diff --git a/apps/models_provider/impl/zhipu_model_provider/credential/tti.py b/apps/models_provider/impl/zhipu_model_provider/credential/tti.py new file mode 100644 index 000000000..82cf3a483 --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/credential/tti.py @@ -0,0 +1,69 @@ +# coding=utf-8 +import traceback +from typing import Dict + +from django.utils.translation import gettext_lazy as _, gettext + +from common import forms +from common.exception.app_exception import AppApiException +from common.forms import BaseForm, TooltipLabel +from models_provider.base_model_provider import BaseModelCredential, ValidCode + + +class ZhiPuTTIModelParams(BaseForm): + size = forms.SingleSelect( + TooltipLabel(_('Image size'), + _('Image size, only cogview-3-plus supports this parameter. Optional range: [1024x1024,768x1344,864x1152,1344x768,1152x864,1440x720,720x1440], the default is 1024x1024.')), + required=True, + default_value='1024x1024', + option_list=[ + {'value': '1024x1024', 'label': '1024x1024'}, + {'value': '768x1344', 'label': '768x1344'}, + {'value': '864x1152', 'label': '864x1152'}, + {'value': '1344x768', 'label': '1344x768'}, + {'value': '1152x864', 'label': '1152x864'}, + {'value': '1440x720', 'label': '1440x720'}, + {'value': '720x1440', 'label': '720x1440'}, + ], + text_field='label', + value_field='value') + + +class ZhiPuTextToImageModelCredential(BaseForm, BaseModelCredential): + api_key = forms.PasswordInputField('API Key', required=True) + + 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)) + + for key in ['api_key']: + if key not in model_credential: + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, gettext('{key} is required').format(key=key)) + else: + return False + try: + model = provider.get_model(model_type, model_name, model_credential, **model_params) + res = model.check_auth() + print(res) + except Exception as e: + traceback.print_exc() + if isinstance(e, AppApiException): + raise e + if raise_exception: + raise AppApiException(ValidCode.valid_error.value, + gettext( + 'Verification failed, please check whether the parameters are correct: {error}').format( + error=str(e))) + else: + return False + return True + + def encryption_dict(self, model: Dict[str, object]): + return {**model, 'api_key': super().encryption(model.get('api_key', ''))} + + def get_model_params_setting_form(self, model_name): + return ZhiPuTTIModelParams() diff --git a/apps/models_provider/impl/zhipu_model_provider/icon/zhipuai_icon_svg b/apps/models_provider/impl/zhipu_model_provider/icon/zhipuai_icon_svg new file mode 100644 index 000000000..f39fedcbb --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/icon/zhipuai_icon_svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/models_provider/impl/zhipu_model_provider/model/image.py b/apps/models_provider/impl/zhipu_model_provider/model/image.py new file mode 100644 index 000000000..7a84aade2 --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/model/image.py @@ -0,0 +1,20 @@ +from typing import Dict + +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_chat_open_ai import BaseChatOpenAI + + +class ZhiPuImage(MaxKBBaseModel, BaseChatOpenAI): + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = MaxKBBaseModel.filter_optional_params(model_kwargs) + return ZhiPuImage( + model_name=model_name, + openai_api_key=model_credential.get('api_key'), + openai_api_base='https://open.bigmodel.cn/api/paas/v4', + # stream_options={"include_usage": True}, + streaming=True, + stream_usage=True, + **optional_params, + ) diff --git a/apps/models_provider/impl/zhipu_model_provider/model/llm.py b/apps/models_provider/impl/zhipu_model_provider/model/llm.py new file mode 100644 index 000000000..dbbb9b671 --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/model/llm.py @@ -0,0 +1,107 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: llm.py + @date:2024/4/28 11:42 + @desc: +""" + +import json +from collections.abc import Iterator +from typing import Any, Dict, List, Optional + +from langchain_community.chat_models import ChatZhipuAI +from langchain_community.chat_models.zhipuai import _truncate_params, _get_jwt_token, connect_sse, \ + _convert_delta_to_message_chunk +from langchain_core.callbacks import ( + CallbackManagerForLLMRun, +) +from langchain_core.messages import ( + AIMessageChunk, + BaseMessage +) +from langchain_core.outputs import ChatGenerationChunk + +from models_provider.base_model_provider import MaxKBBaseModel + + +class ZhipuChatModel(MaxKBBaseModel, ChatZhipuAI): + optional_params: dict + + @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) + zhipuai_chat = ZhipuChatModel( + api_key=model_credential.get('api_key'), + model=model_name, + streaming=model_kwargs.get('streaming', False), + optional_params=optional_params, + **optional_params, + ) + return zhipuai_chat + + usage_metadata: dict = {} + + def get_last_generation_info(self) -> Optional[Dict[str, Any]]: + return self.usage_metadata + + def get_num_tokens_from_messages(self, messages: List[BaseMessage]) -> int: + return self.usage_metadata.get('prompt_tokens', 0) + + def get_num_tokens(self, text: str) -> int: + return self.usage_metadata.get('completion_tokens', 0) + + def _stream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> Iterator[ChatGenerationChunk]: + """Stream the chat response in chunks.""" + if self.zhipuai_api_key is None: + raise ValueError("Did not find zhipuai_api_key.") + if self.zhipuai_api_base is None: + raise ValueError("Did not find zhipu_api_base.") + message_dicts, params = self._create_message_dicts(messages, stop) + payload = {**params, **kwargs, **self.optional_params, "messages": message_dicts, "stream": True} + _truncate_params(payload) + headers = { + "Authorization": _get_jwt_token(self.zhipuai_api_key), + "Accept": "application/json", + } + + default_chunk_class = AIMessageChunk + import httpx + + with httpx.Client(headers=headers, timeout=60) as client: + with connect_sse( + client, "POST", self.zhipuai_api_base, json=payload + ) as event_source: + for sse in event_source.iter_sse(): + chunk = json.loads(sse.data) + if len(chunk["choices"]) == 0: + continue + choice = chunk["choices"][0] + generation_info = {} + if "usage" in chunk: + generation_info = chunk["usage"] + self.usage_metadata = generation_info + chunk = _convert_delta_to_message_chunk( + choice["delta"], default_chunk_class + ) + finish_reason = choice.get("finish_reason", None) + + chunk = ChatGenerationChunk( + message=chunk, generation_info=generation_info + ) + yield chunk + if run_manager: + run_manager.on_llm_new_token(chunk.text, chunk=chunk) + if finish_reason is not None: + break diff --git a/apps/models_provider/impl/zhipu_model_provider/model/tti.py b/apps/models_provider/impl/zhipu_model_provider/model/tti.py new file mode 100644 index 000000000..e6d2641f4 --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/model/tti.py @@ -0,0 +1,69 @@ +from typing import Dict + +from django.utils.translation import gettext +from langchain_community.chat_models import ChatZhipuAI +from langchain_core.messages import HumanMessage +from zhipuai import ZhipuAI + +from common.config.tokenizer_manage_config import TokenizerManage +from models_provider.base_model_provider import MaxKBBaseModel +from models_provider.impl.base_tti import BaseTextToImage + + +def custom_get_token_ids(text: str): + tokenizer = TokenizerManage.get_tokenizer() + return tokenizer.encode(text) + + +class ZhiPuTextToImage(MaxKBBaseModel, BaseTextToImage): + api_key: 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') + + @staticmethod + def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs): + optional_params = {'params': {'size': '1024x1024'}} + for key, value in model_kwargs.items(): + if key not in ['model_id', 'use_local', 'streaming']: + optional_params['params'][key] = value + return ZhiPuTextToImage( + model=model_name, + api_key=model_credential.get('api_key'), + **optional_params, + ) + + def is_cache_model(self): + return False + + def check_auth(self): + chat = ChatZhipuAI( + zhipuai_api_key=self.api_key, + model_name=self.model, + ) + chat.invoke([HumanMessage([{"type": "text", "text": gettext('Hello')}])]) + + # self.generate_image('生成一个小猫图片') + + def generate_image(self, prompt: str, negative_prompt: str = None): + # chat = ChatZhipuAI( + # zhipuai_api_key=self.api_key, + # model_name=self.model, + # ) + chat = ZhipuAI(api_key=self.api_key) + response = chat.images.generations( + model=self.model, # 填写需要调用的模型编码 + prompt=prompt, # 填写需要生成图片的文本 + **self.params # 填写额外参数 + ) + file_urls = [] + for content in response.data: + url = content.url + file_urls.append(url) + + return file_urls diff --git a/apps/models_provider/impl/zhipu_model_provider/zhipu_model_provider.py b/apps/models_provider/impl/zhipu_model_provider/zhipu_model_provider.py new file mode 100644 index 000000000..e71e01b17 --- /dev/null +++ b/apps/models_provider/impl/zhipu_model_provider/zhipu_model_provider.py @@ -0,0 +1,77 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: zhipu_model_provider.py + @date:2024/04/19 13:5 + @desc: +""" +import os + +from common.utils.common import get_file_content +from models_provider.base_model_provider import ModelProvideInfo, ModelTypeConst, ModelInfo, IModelProvider, \ + ModelInfoManage +from models_provider.impl.zhipu_model_provider.credential.image import ZhiPuImageModelCredential +from models_provider.impl.zhipu_model_provider.credential.llm import ZhiPuLLMModelCredential +from models_provider.impl.zhipu_model_provider.credential.tti import ZhiPuTextToImageModelCredential +from models_provider.impl.zhipu_model_provider.model.image import ZhiPuImage +from models_provider.impl.zhipu_model_provider.model.llm import ZhipuChatModel +from models_provider.impl.zhipu_model_provider.model.tti import ZhiPuTextToImage +from maxkb.conf import PROJECT_DIR +from django.utils.translation import gettext as _ + +qwen_model_credential = ZhiPuLLMModelCredential() +zhipu_image_model_credential = ZhiPuImageModelCredential() +zhipu_tti_model_credential = ZhiPuTextToImageModelCredential() + +model_info_list = [ + ModelInfo('glm-4', '', ModelTypeConst.LLM, qwen_model_credential, ZhipuChatModel), + ModelInfo('glm-4v', '', ModelTypeConst.LLM, qwen_model_credential, ZhipuChatModel), + ModelInfo('glm-3-turbo', '', ModelTypeConst.LLM, qwen_model_credential, ZhipuChatModel) +] + +model_info_image_list = [ + ModelInfo('glm-4v-plus', _('Have strong multi-modal understanding capabilities. Able to understand up to five images simultaneously and supports video content understanding'), + ModelTypeConst.IMAGE, zhipu_image_model_credential, + ZhiPuImage), + ModelInfo('glm-4v', _('Focus on single picture understanding. Suitable for scenarios requiring efficient image analysis'), + ModelTypeConst.IMAGE, zhipu_image_model_credential, + ZhiPuImage), + ModelInfo('glm-4v-flash', _('Focus on single picture understanding. Suitable for scenarios requiring efficient image analysis (free)'), + ModelTypeConst.IMAGE, zhipu_image_model_credential, + ZhiPuImage), +] + +model_info_tti_list = [ + ModelInfo('cogview-3', _('Quickly and accurately generate images based on user text descriptions. Resolution supports 1024x1024'), + ModelTypeConst.TTI, zhipu_tti_model_credential, + ZhiPuTextToImage), + ModelInfo('cogview-3-plus', _('Generate high-quality images based on user text descriptions, supporting multiple image sizes'), + ModelTypeConst.TTI, zhipu_tti_model_credential, + ZhiPuTextToImage), + ModelInfo('cogview-3-flash', _('Generate high-quality images based on user text descriptions, supporting multiple image sizes (free)'), + ModelTypeConst.TTI, zhipu_tti_model_credential, + ZhiPuTextToImage), +] + +model_info_manage = ( + ModelInfoManage.builder() + .append_model_info_list(model_info_list) + .append_default_model_info(ModelInfo('glm-4', '', ModelTypeConst.LLM, qwen_model_credential, ZhipuChatModel)) + .append_model_info_list(model_info_image_list) + .append_default_model_info(model_info_image_list[0]) + .append_model_info_list(model_info_tti_list) + .append_default_model_info(model_info_tti_list[0]) + .build() +) + + +class ZhiPuModelProvider(IModelProvider): + + def get_model_info_manage(self): + return model_info_manage + + def get_model_provide_info(self): + return ModelProvideInfo(provider='model_zhipu_provider', name=_('zhipu AI'), icon=get_file_content( + os.path.join(PROJECT_DIR, "apps", 'models_provider', 'impl', 'zhipu_model_provider', 'icon', + 'zhipuai_icon_svg'))) diff --git a/apps/models_provider/migrations/0001_initial.py b/apps/models_provider/migrations/0001_initial.py new file mode 100644 index 000000000..cda4794b8 --- /dev/null +++ b/apps/models_provider/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2 on 2025-04-17 03:32 + +import django.db.models.deletion +import uuid_utils.compat +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Model', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), + ('name', models.CharField(max_length=128, verbose_name='名称')), + ('status', models.CharField(choices=[('SUCCESS', '成功'), ('ERROR', '失败'), ('DOWNLOAD', '下载中'), ('PAUSE_DOWNLOAD', '暂停下载')], default='SUCCESS', max_length=20, verbose_name='设置类型')), + ('model_type', models.CharField(max_length=128, verbose_name='模型类型')), + ('model_name', models.CharField(max_length=128, verbose_name='模型名称')), + ('provider', models.CharField(max_length=128, verbose_name='供应商')), + ('credential', models.CharField(max_length=102400, verbose_name='模型认证信息')), + ('meta', models.JSONField(default=dict, verbose_name='模型元数据,用于存储下载,或者错误信息')), + ('model_params_form', models.JSONField(default=list, verbose_name='模型参数配置')), + ('workspace_id', models.CharField(blank=True, max_length=128, null=True, verbose_name='工作空间id')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user', verbose_name='成员用户id')), + ], + options={ + 'db_table': 'model', + 'unique_together': {('name', 'user_id')}, + }, + ), + ] diff --git a/apps/models_provider/migrations/__init__.py b/apps/models_provider/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/models_provider/models/__init__.py b/apps/models_provider/models/__init__.py new file mode 100644 index 000000000..8d63a0921 --- /dev/null +++ b/apps/models_provider/models/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: __init__.py + @date:2023/9/25 15:04 + @desc: +""" + +from .model_management import * diff --git a/apps/models_provider/models/model_management.py b/apps/models_provider/models/model_management.py new file mode 100644 index 000000000..e1701143f --- /dev/null +++ b/apps/models_provider/models/model_management.py @@ -0,0 +1,50 @@ +# coding=utf-8 +import uuid_utils.compat as uuid + +from django.db import models + +from common.mixins.app_model_mixin import AppModelMixin +from users.models import User + + +class Status(models.TextChoices): + """系统设置类型""" + SUCCESS = "SUCCESS", '成功' + + ERROR = "ERROR", "失败" + + DOWNLOAD = "DOWNLOAD", '下载中' + + PAUSE_DOWNLOAD = "PAUSE_DOWNLOAD", '暂停下载' + + +class Model(AppModelMixin): + """ + 模型数据 + """ + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id") + + name = models.CharField(max_length=128, verbose_name="名称") + + status = models.CharField(max_length=20, verbose_name='设置类型', choices=Status.choices, + default=Status.SUCCESS) + + model_type = models.CharField(max_length=128, verbose_name="模型类型") + + model_name = models.CharField(max_length=128, verbose_name="模型名称") + + user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="成员用户id") + + provider = models.CharField(max_length=128, verbose_name='供应商') + + credential = models.CharField(max_length=102400, verbose_name="模型认证信息") + + meta = models.JSONField(verbose_name="模型元数据,用于存储下载,或者错误信息", default=dict) + + model_params_form = models.JSONField(verbose_name="模型参数配置", default=list) + + workspace_id = models.CharField(max_length=128, verbose_name="工作空间id", null=True, blank=True) + + class Meta: + db_table = "model" + unique_together = ['name', 'user_id'] diff --git a/apps/models_provider/serializers/__init__.py b/apps/models_provider/serializers/__init__.py new file mode 100644 index 000000000..9bad5790a --- /dev/null +++ b/apps/models_provider/serializers/__init__.py @@ -0,0 +1 @@ +# coding=utf-8 diff --git a/apps/models_provider/serializers/model.py b/apps/models_provider/serializers/model.py new file mode 100644 index 000000000..2a6b10778 --- /dev/null +++ b/apps/models_provider/serializers/model.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +import json +import threading +import time +from typing import Dict + +import uuid_utils.compat as uuid +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.exception.app_exception import AppApiException +from common.utils.rsa_util import rsa_long_encrypt, rsa_long_decrypt +from models_provider.base_model_provider import ValidCode, DownModelChunkStatus +from models_provider.constants.model_provider_constants import ModelProvideConstants +from models_provider.models import Model, Status + + +class ModelModelSerializer(serializers.ModelSerializer): + class Meta: + model = Model + fields = [ + 'id', 'name', 'status', 'model_type', 'model_name', + 'user', 'provider', 'credential', 'meta', + 'model_params_form', 'workspace_id' + ] + + +class ModelCreateRequest(serializers.Serializer): + name = serializers.CharField(required=True, max_length=64, label=_("model name")) + provider = serializers.CharField(required=True, label=_("provider")) + model_type = serializers.CharField(required=True, label=_("model type")) + model_name = serializers.CharField(required=True, label=_("model name")) + model_params_form = serializers.ListField(required=False, default=list, label=_("parameter configuration")) + credential = serializers.DictField(required=True, label=_("certification information")) + + +class ModelPullManage: + @staticmethod + def pull(model: Model, credential: Dict): + try: + response = ModelProvideConstants[model.provider].value.down_model( + model.model_type, model.model_name, credential + ) + down_model_chunk = {} + last_update_time = time.time() + + for chunk in response: + down_model_chunk[chunk.digest] = chunk.to_dict() + if time.time() - last_update_time > 5: + current_model = QuerySet(Model).filter(id=model.id).first() + if current_model and current_model.status == Status.PAUSE_DOWNLOAD: + return + QuerySet(Model).filter(id=model.id).update( + meta={"down_model_chunk": list(down_model_chunk.values())} + ) + last_update_time = time.time() + + status = Status.ERROR + message = "" + for chunk in down_model_chunk.values(): + if chunk.get('status') == DownModelChunkStatus.success.value: + status = Status.SUCCESS + elif chunk.get('status') == DownModelChunkStatus.error.value: + message = chunk.get("digest") + + QuerySet(Model).filter(id=model.id).update( + meta={"down_model_chunk": [], "message": message}, + status=status + ) + except Exception as e: + QuerySet(Model).filter(id=model.id).update( + meta={"down_model_chunk": [], "message": str(e)}, + status=Status.ERROR + ) + + +class ModelSerializer(serializers.Serializer): + @staticmethod + def model_to_dict(model: Model): + credential = json.loads(rsa_long_decrypt(model.credential)) + return { + 'id': str(model.id), + 'provider': model.provider, + 'name': model.name, + 'model_type': model.model_type, + 'model_name': model.model_name, + 'status': model.status, + 'meta': model.meta, + 'credential': ModelProvideConstants[model.provider].value.get_model_credential( + model.model_type, model.model_name + ).encryption_dict(credential), + 'workspace_id': model.workspace_id + } + + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=True, label=_("模型id")) + user_id = serializers.UUIDField(required=True, label=_("user id")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + model = QuerySet(Model).filter( + id=self.data.get("id"), user_id=self.data.get("user_id") + ).first() + if model is None: + raise AppApiException(500, _('模型不存在')) + + def one(self, with_valid=False): + if with_valid: + self.is_valid(raise_exception=True) + model = QuerySet(Model).get( + id=self.data.get('id'), user_id=self.data.get('user_id') + ) + return ModelSerializer.model_to_dict(model) + + class Create(serializers.Serializer): + user_id = serializers.UUIDField(required=True, label=_('user id')) + name = serializers.CharField(required=True, max_length=64, label=_("model name")) + provider = serializers.CharField(required=True, label=_("provider")) + model_type = serializers.CharField(required=True, label=_("model type")) + model_name = serializers.CharField(required=True, label=_("model name")) + model_params_form = serializers.ListField(required=False, default=list, label=_("parameter configuration")) + credential = serializers.DictField(required=True, label=_("certification information")) + workspace_id = serializers.CharField(required=False, label=_("workspace id"), max_length=128) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + if QuerySet(Model).filter( + user_id=self.data.get('user_id'), + name=self.data.get('name'), + workspace_id=self.data.get('workspace_id') + ).exists(): + raise AppApiException( + 500, + _('Model name【{model_name}】already exists').format(model_name=self.data.get("name")) + ) + default_params = {item['field']: item['default_value'] for item in self.data.get('model_params_form')} + ModelProvideConstants[self.data.get('provider')].value.is_valid_credential( + self.data.get('model_type'), + self.data.get('model_name'), + self.data.get('credential'), + default_params, + raise_exception=True + ) + + def insert(self, workspace_id, with_valid=True): + status = Status.SUCCESS + if with_valid: + try: + self.is_valid(raise_exception=True) + except AppApiException as e: + if e.code == ValidCode.model_not_fount: + status = Status.DOWNLOAD + else: + raise e + + credential = self.data.get('credential') + model_data = { + 'id': uuid.uuid1(), + 'status': status, + 'user_id': self.data.get('user_id'), + 'name': self.data.get('name'), + 'credential': rsa_long_encrypt(json.dumps(credential)), + 'provider': self.data.get('provider'), + 'model_type': self.data.get('model_type'), + 'model_name': self.data.get('model_name'), + 'model_params_form': self.data.get('model_params_form'), + 'workspace_id': workspace_id + } + model = Model(**model_data) + try: + model.save() + except Exception as save_error: + # 可添加日志记录 + raise AppApiException(500, _('模型保存失败')) from save_error + + if status == Status.DOWNLOAD: + thread = threading.Thread(target=ModelPullManage.pull, args=(model, credential)) + thread.start() + + return ModelModelSerializer(model).data diff --git a/apps/models_provider/tests.py b/apps/models_provider/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/models_provider/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/models_provider/tools.py b/apps/models_provider/tools.py new file mode 100644 index 000000000..08f5780c6 --- /dev/null +++ b/apps/models_provider/tools.py @@ -0,0 +1,125 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎 + @file: tools.py + @date:2024/7/22 11:18 + @desc: +""" +from django.db import connection +from django.db.models import QuerySet + +from common.config.embedding_config import ModelManage +from models_provider.models import Model +from django.utils.translation import gettext_lazy as _ + +import json +from typing import Dict + +from common.utils.rsa_util import rsa_long_decrypt +from models_provider.constants.model_provider_constants import ModelProvideConstants + + +def get_model_(provider, model_type, model_name, credential, model_id, use_local=False, **kwargs): + """ + 获取模型实例 + @param provider: 供应商 + @param model_type: 模型类型 + @param model_name: 模型名称 + @param credential: 认证信息 + @param model_id: 模型id + @param use_local: 是否调用本地模型 只适用于本地供应商 + @return: 模型实例 + """ + model = get_provider(provider).get_model(model_type, model_name, + json.loads( + rsa_long_decrypt(credential)), + model_id=model_id, + use_local=use_local, + streaming=True, **kwargs) + return model + + +def get_model(model, **kwargs): + """ + 获取模型实例 + @param model: model 数据库Model实例对象 + @return: 模型实例 + """ + return get_model_(model.provider, model.model_type, model.model_name, model.credential, str(model.id), **kwargs) + + +def get_provider(provider): + """ + 获取供应商实例 + @param provider: 供应商字符串 + @return: 供应商实例 + """ + return ModelProvideConstants[provider].value + + +def get_model_list(provider, model_type): + """ + 获取模型列表 + @param provider: 供应商字符串 + @param model_type: 模型类型 + @return: 模型列表 + """ + return get_provider(provider).get_model_list(model_type) + + +def get_model_credential(provider, model_type, model_name): + """ + 获取模型认证实例 + @param provider: 供应商字符串 + @param model_type: 模型类型 + @param model_name: 模型名称 + @return: 认证实例对象 + """ + return get_provider(provider).get_model_credential(model_type, model_name) + + +def get_model_type_list(provider): + """ + 获取模型类型列表 + @param provider: 供应商字符串 + @return: 模型类型列表 + """ + return get_provider(provider).get_model_type_list() + + +def is_valid_credential(provider, model_type, model_name, model_credential: Dict[str, object], model_params, + raise_exception=False): + """ + 校验模型认证参数 + @param provider: 供应商字符串 + @param model_type: 模型类型 + @param model_name: 模型名称 + @param model_credential: 模型认证数据 + @param raise_exception: 是否抛出错误 + @return: True|False + """ + return get_provider(provider).is_valid_credential(model_type, model_name, model_credential, model_params, + raise_exception) + + +def get_model_by_id(_id, user_id): + model = QuerySet(Model).filter(id=_id).first() + # 手动关闭数据库连接 + connection.close() + if model is None: + raise Exception(_('Model does not exist')) + if model.permission_type == 'PRIVATE' and str(model.user_id) != str(user_id): + raise Exception(_('No permission to use this model') + f"{model.name}") + return model + + +def get_model_instance_by_model_user_id(model_id, user_id, **kwargs): + """ + 获取模型实例,根据模型相关数据 + @param model_id: 模型id + @param user_id: 用户id + @return: 模型实例 + """ + model = get_model_by_id(model_id, user_id) + return ModelManage.get_model(model_id, lambda _id: get_model(model, **kwargs)) diff --git a/apps/models_provider/urls.py b/apps/models_provider/urls.py new file mode 100644 index 000000000..7560b87cd --- /dev/null +++ b/apps/models_provider/urls.py @@ -0,0 +1,21 @@ +from django.urls import path + +from . import views + +app_name = "models_provider" +urlpatterns = [ + # path('provider//', views.Provide.Exec.as_view(), name='provide_exec'), + path('provider', views.Provide.as_view(), name='provide'), + path('provider/model_type_list', views.Provide.ModelTypeList.as_view(), name="provider/model_type_list"), + path('provider/model_list', views.Provide.ModelList.as_view(), name="provider/model_name_list"), + # path('provider/model_params_form', views.Provide.ModelParamsForm.as_view(), + # name="provider/model_params_form"), + # path('provider/model_form', views.Provide.ModelForm.as_view(), + # name="provider/model_form"), + path('workspace//model', views.Model.as_view(), name='model'), + # path('workspace//model//model_params_form', views.Model.ModelParamsForm.as_view(), + # name='model/model_params_form'), + # path('workspace//model/', views.Model.Operate.as_view(), name='model/operate'), + # path('workspace//model//pause_download', views.Model.PauseDownload.as_view(), name='model/operate'), + # path('workspace//model//meta', views.Model.ModelMeta.as_view(), name='model/operate/meta'), +] diff --git a/apps/models_provider/views/__init__.py b/apps/models_provider/views/__init__.py new file mode 100644 index 000000000..6543afd24 --- /dev/null +++ b/apps/models_provider/views/__init__.py @@ -0,0 +1,4 @@ +# coding=utf-8 + +from .model import * +from .provide import * \ No newline at end of file diff --git a/apps/models_provider/views/model.py b/apps/models_provider/views/model.py new file mode 100644 index 000000000..539f4ed9f --- /dev/null +++ b/apps/models_provider/views/model.py @@ -0,0 +1,35 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 19:25 + @desc: +""" +from drf_spectacular.utils import extend_schema +from rest_framework.views import APIView +from django.utils.translation import gettext_lazy as _ +from rest_framework.request import Request + +from common.auth import TokenAuth +from common.auth.authentication import has_permissions +from common.constants.permission_constants import PermissionConstants +from common.result import result +from models_provider.api.model import ModelCreateAPI +from models_provider.serializers.model import ModelSerializer + + +class Model(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['POST'], + description=_("Create model"), + operation_id=_("Create model"), + tags=[_("Model")], + request=ModelCreateAPI.get_request(), + responses=ModelCreateAPI.get_response()) + @has_permissions(PermissionConstants.MODEL_CREATE) + def post(self, request: Request, workspace_id: str): + return result.success( + ModelSerializer.Create(data={**request.data, 'user_id': request.user.id}).insert(workspace_id, + with_valid=True)) diff --git a/apps/models_provider/views/provide.py b/apps/models_provider/views/provide.py new file mode 100644 index 000000000..6b142bacb --- /dev/null +++ b/apps/models_provider/views/provide.py @@ -0,0 +1,68 @@ +# coding=utf-8 + +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +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 +from models_provider.api.provide import ProvideApi +from models_provider.constants.model_provider_constants import ModelProvideConstants + + +class Provide(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['GET'], + description=_('Get a list of model suppliers'), + operation_id=_('Get a list of model suppliers'), + responses=ProvideApi.get_response(), + tags=[_('Model')]) + @has_permissions(PermissionConstants.MODEL_READ) + def get(self, request: Request): + model_type = request.query_params.get('model_type') + if model_type: + providers = [] + for key in ModelProvideConstants.__members__: + if len([item for item in ModelProvideConstants[key].value.get_model_type_list() if + item['value'] == model_type]) > 0: + providers.append(ModelProvideConstants[key].value.get_model_provide_info().to_dict()) + return result.success(providers) + return result.success( + [ModelProvideConstants[key].value.get_model_provide_info().to_dict() for key in + ModelProvideConstants.__members__]) + + class ModelTypeList(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['GET'], + description=_('Get a list of model types'), + operation_id=_('Get a list of model types'), + parameters=ProvideApi.ModelTypeList.get_query_params_api(), + responses=ProvideApi.ModelTypeList.get_response(), + tags=[_('Model')]) + @has_permissions(PermissionConstants.MODEL_READ) + def get(self, request: Request): + provider = request.query_params.get('provider') + return result.success(ModelProvideConstants[provider].value.get_model_type_list()) + + class ModelList(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['GET'], + description=_('Example of obtaining model list'), + operation_id=_('Example of obtaining model list'), + parameters=ProvideApi.ModelList.get_query_params_api(), + responses=ProvideApi.ModelList.get_response(), + tags=[_('Model')]) + @has_permissions(PermissionConstants.MODEL_READ) + def get(self, request: Request): + provider = request.query_params.get('provider') + model_type = request.query_params.get('model_type') + + return result.success( + ModelProvideConstants[provider].value.get_model_list( + model_type)) diff --git a/apps/system_manage/migrations/0002_systemsetting.py b/apps/system_manage/migrations/0002_systemsetting.py new file mode 100644 index 000000000..cba9d9854 --- /dev/null +++ b/apps/system_manage/migrations/0002_systemsetting.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2 on 2025-04-17 03:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system_manage', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SystemSetting', + fields=[ + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')), + ('type', models.IntegerField(choices=[(0, '邮箱'), (1, '私钥秘钥')], default=0, primary_key=True, serialize=False, verbose_name='设置类型')), + ('meta', models.JSONField(default=dict, verbose_name='配置数据')), + ], + options={ + 'db_table': 'system_setting', + }, + ), + ] diff --git a/apps/system_manage/models/__init__.py b/apps/system_manage/models/__init__.py index 415c04f3d..eca60db8c 100644 --- a/apps/system_manage/models/__init__.py +++ b/apps/system_manage/models/__init__.py @@ -7,3 +7,4 @@ @desc: """ from .workspace_user_permission import * +from .system_setting import * diff --git a/apps/system_manage/models/system_setting.py b/apps/system_manage/models/system_setting.py new file mode 100644 index 000000000..8dea8955b --- /dev/null +++ b/apps/system_manage/models/system_setting.py @@ -0,0 +1,32 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎 + @file: system_management.py + @date:2024/3/19 13:47 + @desc: 邮箱管理 +""" + +from django.db import models + +from common.mixins.app_model_mixin import AppModelMixin + + +class SettingType(models.IntegerChoices): + """系统设置类型""" + EMAIL = 0, '邮箱' + + RSA = 1, "私钥秘钥" + + +class SystemSetting(AppModelMixin): + """ + 系统设置 + """ + type = models.IntegerField(primary_key=True, verbose_name='设置类型', choices=SettingType.choices, + default=SettingType.EMAIL) + + meta = models.JSONField(verbose_name="配置数据", default=dict) + + class Meta: + db_table = "system_setting" diff --git a/pyproject.toml b/pyproject.toml index 033d404d4..013a8cfe6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,29 @@ psycopg = { extras = ["binary"], version = "3.2.6" } python-dotenv = "1.1.0" uuid-utils = "0.10.0" diskcache2 = "0.1.2" - +langchain-openai = "^0.3.0" +langchain-anthropic = "^0.3.0" +langchain-community = "^0.3.0" +langchain-deepseek = "^0.1.0" +langchain-google-genai = "^2.0.9" +langchain-mcp-adapters = "^0.0.5" +langchain-huggingface = "^0.1.2" +langchain-ollama = "^0.3.0" +langgraph = "^0.3.0" +mcp = "^1.4.1" +qianfan = "^0.3.6.1" +zhipuai = "^2.0.1" +boto3 = "^1.34.160" +tencentcloud-sdk-python = "^3.0.1209" +xinference-client = "^1.3.0" +anthropic = "^0.49.0" +dashscope = "^1.17.0" +pylint = "3.1.0" +pydub = "^0.25.1" +cffi = "^1.17.1" +pysilk = "^0.0.1" +sentence-transformers = "^4.0.2" +websockets = "^13.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"