From d8425f3f4c8c477ffaae394a0eb38d06c4a4ce06 Mon Sep 17 00:00:00 2001 From: Yuanyuan Zhang Date: Mon, 22 May 2023 10:08:26 +0800 Subject: [PATCH 01/32] Fix the email validation problem for a.b@c.club. (#94) Co-authored-by: yyzhang --- api/libs/helper.py | 2 +- web/app/install/installForm.tsx | 2 +- web/app/signin/normalForm.tsx | 2 +- web/config/index.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index bbf01cbad7..0db4363a2d 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -21,7 +21,7 @@ class TimestampField(fields.Raw): def email(email): # Define a regex pattern for email addresses - pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" + pattern = r"^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$" # Check if the email matches the pattern if re.match(pattern, email) is not None: return email diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 749719e9e9..a53373e794 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation' import Toast from '../components/base/toast' import { setup } from '@/service/common' -const validEmailReg = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/ +const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ const InstallForm = () => { diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index e0626572ac..b7b0f0cca6 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -13,7 +13,7 @@ import Button from '@/app/components/base/button' import { login, oauth } from '@/service/common' import { apiPrefix } from '@/config' -const validEmailReg = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/ +const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/ type IState = { formValid: boolean diff --git a/web/config/index.ts b/web/config/index.ts index ec7784459f..2a8ac0a1eb 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -77,7 +77,7 @@ export const DEFAULT_VALUE_MAX_LEN = 48 export const zhRegex = /^[\u4e00-\u9fa5]$/gm export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/gm -export const emailRegex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/gm const MAX_ZN_VAR_NAME_LENGHT = 8 const MAX_EN_VAR_VALUE_LENGHT = 16 export const getMaxVarNameLength = (value: string) => { From 4db01403aeb82634c782a405e974fd0e0b71d0f7 Mon Sep 17 00:00:00 2001 From: crazywoola <100913391+crazywoola@users.noreply.github.com> Date: Mon, 22 May 2023 10:12:17 +0800 Subject: [PATCH 02/32] feat: add missing i18n (#130) --- web/app/components/base/emoji-picker/index.tsx | 13 +++++++------ web/i18n/lang/app.en.ts | 4 ++++ web/i18n/lang/app.zh.ts | 4 ++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 47dbeaaa5d..5c888fb94d 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,18 +1,18 @@ 'use client' +import React from 'react' +import { useState, FC, ChangeEvent } from 'react' import data from '@emoji-mart/data' import { init, SearchIndex } from 'emoji-mart' -// import AppIcon from '@/app/components/base/app-icon' import cn from 'classnames' import Divider from '@/app/components/base/divider' - import Button from '@/app/components/base/button' import s from './style.module.css' -import { useState, FC, ChangeEvent } from 'react' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' -import React from 'react' + import Modal from '@/app/components/base/modal' +import { useTranslation } from 'react-i18next' declare global { namespace JSX { @@ -69,6 +69,7 @@ const EmojiPicker: FC = ({ onClose }) => { + const { t } = useTranslation() const { categories } = data as any const [selectedEmoji, setSelectedEmoji] = useState('') const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) @@ -187,7 +188,7 @@ const EmojiPicker: FC = ({ : <> diff --git a/web/i18n/lang/app.en.ts b/web/i18n/lang/app.en.ts index 73c2a5b1b6..aea93eb168 100644 --- a/web/i18n/lang/app.en.ts +++ b/web/i18n/lang/app.en.ts @@ -35,6 +35,10 @@ const translation = { appCreated: 'App created', appCreateFailed: 'Failed to create app', }, + emoji: { + ok: 'OK', + cancel: 'Cancel', + } } export default translation diff --git a/web/i18n/lang/app.zh.ts b/web/i18n/lang/app.zh.ts index 03fde52c35..467d85feeb 100644 --- a/web/i18n/lang/app.zh.ts +++ b/web/i18n/lang/app.zh.ts @@ -34,6 +34,10 @@ const translation = { appCreated: '应用已创建', appCreateFailed: '应用创建失败', }, + emoji: { + ok: '确认', + cancel: '取消', + } } export default translation From 17a8118154053c26afbd3845c94d56cb4f62aba2 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 May 2023 10:39:51 +0800 Subject: [PATCH 03/32] fix: email reg (#135) --- api/libs/helper.py | 2 +- web/app/install/installForm.tsx | 2 +- web/app/signin/normalForm.tsx | 2 +- web/config/index.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index 0db4363a2d..767f368d33 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -21,7 +21,7 @@ class TimestampField(fields.Raw): def email(email): # Define a regex pattern for email addresses - pattern = r"^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$" + pattern = r"^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$" # Check if the email matches the pattern if re.match(pattern, email) is not None: return email diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index a53373e794..605a2dced0 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation' import Toast from '../components/base/toast' import { setup } from '@/service/common' -const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/ +const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ const InstallForm = () => { diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index b7b0f0cca6..93e6b0d561 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -13,7 +13,7 @@ import Button from '@/app/components/base/button' import { login, oauth } from '@/service/common' import { apiPrefix } from '@/config' -const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/ +const validEmailReg = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/ type IState = { formValid: boolean diff --git a/web/config/index.ts b/web/config/index.ts index 2a8ac0a1eb..3b3453061a 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -75,9 +75,9 @@ export const LOCALE_COOKIE_NAME = 'locale' export const DEFAULT_VALUE_MAX_LEN = 48 -export const zhRegex = /^[\u4e00-\u9fa5]$/gm -export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/gm -export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$/gm +export const zhRegex = /^[\u4e00-\u9fa5]$/m +export const emojiRegex = /^[\uD800-\uDBFF][\uDC00-\uDFFF]$/m +export const emailRegex = /^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$/m const MAX_ZN_VAR_NAME_LENGHT = 8 const MAX_EN_VAR_VALUE_LENGHT = 16 export const getMaxVarNameLength = (value: string) => { From 7a2291f450c6c9d231fa8493577ccb1e2c3ad525 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 May 2023 11:25:40 +0800 Subject: [PATCH 04/32] fix: more than 6th options would be hide (#136) --- web/app/components/share/chat/config-scence/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/share/chat/config-scence/index.tsx b/web/app/components/share/chat/config-scence/index.tsx index e22933ce5d..152d5e58a4 100644 --- a/web/app/components/share/chat/config-scence/index.tsx +++ b/web/app/components/share/chat/config-scence/index.tsx @@ -5,7 +5,7 @@ import Welcome from '../welcome' const ConfigSence: FC = (props) => { return ( -
+
) From eddd03895932a50d6aee9038ae96686ec00970b4 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 May 2023 13:01:46 +0800 Subject: [PATCH 05/32] Update issue templates (#140) --- .github/ISSUE_TEMPLATE/bug-.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-.md diff --git a/.github/ISSUE_TEMPLATE/bug-.md b/.github/ISSUE_TEMPLATE/bug-.md new file mode 100644 index 0000000000..c6a601840a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-.md @@ -0,0 +1,32 @@ +--- +name: 'Bug:' +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + +Dify version: Cloud | Self Host + +## Steps To Reproduce + + +1. +2. + + +## The current behavior + + +## The expected behavior From c43c3098a036e942adba647b23a11ca8fa70055b Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 May 2023 13:13:04 +0800 Subject: [PATCH 06/32] Update issue templates (#142) --- .../{bug-.md => 🐛-bug-report.md} | 4 ++-- .github/ISSUE_TEMPLATE/🚀-feature-request.md | 20 +++++++++++++++++++ .../ISSUE_TEMPLATE/🤔-questions-and-help.md | 10 ++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) rename .github/ISSUE_TEMPLATE/{bug-.md => 🐛-bug-report.md} (93%) create mode 100644 .github/ISSUE_TEMPLATE/🚀-feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/🤔-questions-and-help.md diff --git a/.github/ISSUE_TEMPLATE/bug-.md b/.github/ISSUE_TEMPLATE/🐛-bug-report.md similarity index 93% rename from .github/ISSUE_TEMPLATE/bug-.md rename to .github/ISSUE_TEMPLATE/🐛-bug-report.md index c6a601840a..3553cbda8a 100644 --- a/.github/ISSUE_TEMPLATE/bug-.md +++ b/.github/ISSUE_TEMPLATE/🐛-bug-report.md @@ -1,8 +1,8 @@ --- -name: 'Bug:' +name: "\U0001F41B Bug report" about: Create a report to help us improve title: '' -labels: '' +labels: bug assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/🚀-feature-request.md b/.github/ISSUE_TEMPLATE/🚀-feature-request.md new file mode 100644 index 0000000000..34f39d2fa2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/🚀-feature-request.md @@ -0,0 +1,20 @@ +--- +name: "\U0001F680 Feature request" +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/🤔-questions-and-help.md b/.github/ISSUE_TEMPLATE/🤔-questions-and-help.md new file mode 100644 index 0000000000..df93653cac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/🤔-questions-and-help.md @@ -0,0 +1,10 @@ +--- +name: "\U0001F914 Questions and Help" +about: Ask a usage or consultation question +title: '' +labels: '' +assignees: '' + +--- + + From 54a6571462cb4cc8a04d77ad086d325e03d7c457 Mon Sep 17 00:00:00 2001 From: John Wang Date: Mon, 22 May 2023 14:32:22 +0800 Subject: [PATCH 07/32] fix: extra input for opening statement was not suitable for prompt (#143) --- api/core/completion.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/core/completion.py b/api/core/completion.py index d05d8417da..8e79e03a93 100644 --- a/api/core/completion.py +++ b/api/core/completion.py @@ -177,13 +177,21 @@ Avoid mentioning that you obtained the information from the context. And answer according to the language of the user's question. """ if pre_prompt: - human_inputs.update(inputs) + extra_inputs = {k: inputs[k] for k in + OutLinePromptTemplate.from_template(template=pre_prompt).input_variables + if k in inputs} + if extra_inputs: + human_inputs.update(extra_inputs) human_message_instruction += pre_prompt + "\n" human_message_prompt = human_message_instruction + "Q:{query}\nA:" else: if pre_prompt: - human_inputs.update(inputs) + extra_inputs = {k: inputs[k] for k in + OutLinePromptTemplate.from_template(template=pre_prompt).input_variables + if k in inputs} + if extra_inputs: + human_inputs.update(extra_inputs) human_message_prompt = pre_prompt + "\n" + human_message_prompt # construct main prompt From 2d0d3365ed55a434e83f6b3d6b17b0bf464fa480 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 May 2023 16:05:08 +0800 Subject: [PATCH 08/32] fix: buffer not return event show errors (#149) --- web/service/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/service/base.ts b/web/service/base.ts index e783c247f9..c6e841e5ac 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -78,7 +78,7 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted if (message.startsWith('data: ')) { // check if it starts with data: // console.log(message); bufferObj = JSON.parse(message.substring(6)) // remove data: and parse as json - if (bufferObj.status === 400) { + if (bufferObj.status === 400 || !bufferObj.event) { onData('', false, { conversationId: undefined, messageId: '', From a7c40a07d89f82df93c88fe0957882353f912141 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 22 May 2023 17:22:28 +0800 Subject: [PATCH 09/32] fix: seg no blank break ui (#150) --- .../datasets/documents/detail/completed/style.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/datasets/documents/detail/completed/style.module.css b/web/app/components/datasets/documents/detail/completed/style.module.css index 323315b957..778b550ad4 100644 --- a/web/app/components/datasets/documents/detail/completed/style.module.css +++ b/web/app/components/datasets/documents/detail/completed/style.module.css @@ -44,7 +44,7 @@ @apply h-8 py-0 bg-gray-50 hover:bg-gray-100 rounded-lg shadow-none !important; } .segModalContent { - @apply h-96 text-gray-800 text-base overflow-y-scroll; + @apply h-96 text-gray-800 text-base break-all overflow-y-scroll; white-space: pre-line; } .footer { From 9a5ae9f51f5d3476ace282de71dfafc99a9c0dc0 Mon Sep 17 00:00:00 2001 From: John Wang Date: Mon, 22 May 2023 17:39:28 +0800 Subject: [PATCH 10/32] Feat: optimize error desc (#152) --- api/controllers/console/app/__init__.py | 2 +- api/controllers/console/app/error.py | 14 ++--- .../console/datasets/datasets_document.py | 13 ++++- api/controllers/console/datasets/error.py | 16 +++--- .../console/datasets/hit_testing.py | 9 ++++ api/controllers/console/error.py | 5 +- api/controllers/console/workspace/error.py | 4 +- .../console/workspace/providers.py | 4 +- api/controllers/service_api/app/error.py | 16 +++--- api/controllers/service_api/dataset/error.py | 2 +- api/controllers/web/error.py | 20 ++++---- api/core/index/index_builder.py | 12 +++++ api/core/index/vector_index.py | 4 +- api/core/llm/provider/azure_provider.py | 51 ++++++++++++++----- web/i18n/lang/common.en.ts | 2 +- 15 files changed, 119 insertions(+), 55 deletions(-) diff --git a/api/controllers/console/app/__init__.py b/api/controllers/console/app/__init__.py index 1f22ab30c6..f0c7956e0f 100644 --- a/api/controllers/console/app/__init__.py +++ b/api/controllers/console/app/__init__.py @@ -17,6 +17,6 @@ def _get_app(app_id, mode=None): raise NotFound("App not found") if mode and app.mode != mode: - raise AppUnavailableError() + raise NotFound("The {} app not found".format(mode)) return app diff --git a/api/controllers/console/app/error.py b/api/controllers/console/app/error.py index c19f054be4..8923f90bf8 100644 --- a/api/controllers/console/app/error.py +++ b/api/controllers/console/app/error.py @@ -9,31 +9,33 @@ class AppNotFoundError(BaseHTTPException): class ProviderNotInitializeError(BaseHTTPException): error_code = 'provider_not_initialize' - description = "Provider Token not initialize." + description = "No valid model provider credentials found. " \ + "Please go to Settings -> Model Provider to complete your provider credentials." code = 400 class ProviderQuotaExceededError(BaseHTTPException): error_code = 'provider_quota_exceeded' - description = "Provider quota exceeded." + description = "Your quota for Dify Hosted OpenAI has been exhausted. " \ + "Please go to Settings -> Model Provider to complete your own provider credentials." code = 400 class ProviderModelCurrentlyNotSupportError(BaseHTTPException): error_code = 'model_currently_not_support' - description = "GPT-4 currently not support." + description = "Dify Hosted OpenAI trial currently not support the GPT-4 model." code = 400 class ConversationCompletedError(BaseHTTPException): error_code = 'conversation_completed' - description = "Conversation was completed." + description = "The conversation has ended. Please start a new conversation." code = 400 class AppUnavailableError(BaseHTTPException): error_code = 'app_unavailable' - description = "App unavailable." + description = "App unavailable, please check your app configurations." code = 400 @@ -45,5 +47,5 @@ class CompletionRequestError(BaseHTTPException): class AppMoreLikeThisDisabledError(BaseHTTPException): error_code = 'app_more_like_this_disabled' - description = "More like this disabled." + description = "The 'More like this' feature is disabled. Please refresh your page." code = 403 diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 79e52d565b..3b9efeaab4 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -10,13 +10,14 @@ from werkzeug.exceptions import NotFound, Forbidden import services from controllers.console import api -from controllers.console.app.error import ProviderNotInitializeError +from controllers.console.app.error import ProviderNotInitializeError, ProviderQuotaExceededError, \ + ProviderModelCurrentlyNotSupportError from controllers.console.datasets.error import DocumentAlreadyFinishedError, InvalidActionError, DocumentIndexingError, \ InvalidMetadataError, ArchivedDocumentImmutableError from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required from core.indexing_runner import IndexingRunner -from core.llm.error import ProviderTokenNotInitError +from core.llm.error import ProviderTokenNotInitError, QuotaExceededError, ModelCurrentlyNotSupportError from extensions.ext_redis import redis_client from libs.helper import TimestampField from extensions.ext_database import db @@ -222,6 +223,10 @@ class DatasetDocumentListApi(Resource): document = DocumentService.save_document_with_dataset_id(dataset, args, current_user) except ProviderTokenNotInitError: raise ProviderNotInitializeError() + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() return document @@ -259,6 +264,10 @@ class DatasetInitApi(Resource): ) except ProviderTokenNotInitError: raise ProviderNotInitializeError() + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() response = { 'dataset': dataset, diff --git a/api/controllers/console/datasets/error.py b/api/controllers/console/datasets/error.py index 014822d565..29142b80e6 100644 --- a/api/controllers/console/datasets/error.py +++ b/api/controllers/console/datasets/error.py @@ -3,7 +3,7 @@ from libs.exception import BaseHTTPException class NoFileUploadedError(BaseHTTPException): error_code = 'no_file_uploaded' - description = "No file uploaded." + description = "Please upload your file." code = 400 @@ -27,25 +27,25 @@ class UnsupportedFileTypeError(BaseHTTPException): class HighQualityDatasetOnlyError(BaseHTTPException): error_code = 'high_quality_dataset_only' - description = "High quality dataset only." + description = "Current operation only supports 'high-quality' datasets." code = 400 class DatasetNotInitializedError(BaseHTTPException): error_code = 'dataset_not_initialized' - description = "Dataset not initialized." + description = "The dataset is still being initialized or indexing. Please wait a moment." code = 400 class ArchivedDocumentImmutableError(BaseHTTPException): error_code = 'archived_document_immutable' - description = "Cannot process an archived document." + description = "The archived document is not editable." code = 403 class DatasetNameDuplicateError(BaseHTTPException): error_code = 'dataset_name_duplicate' - description = "Dataset name already exists." + description = "The dataset name already exists. Please modify your dataset name." code = 409 @@ -57,17 +57,17 @@ class InvalidActionError(BaseHTTPException): class DocumentAlreadyFinishedError(BaseHTTPException): error_code = 'document_already_finished' - description = "Document already finished." + description = "The document has been processed. Please refresh the page or go to the document details." code = 400 class DocumentIndexingError(BaseHTTPException): error_code = 'document_indexing' - description = "Document indexing." + description = "The document is being processed and cannot be edited." code = 400 class InvalidMetadataError(BaseHTTPException): error_code = 'invalid_metadata' - description = "Invalid metadata." + description = "The metadata content is incorrect. Please check and verify." code = 400 diff --git a/api/controllers/console/datasets/hit_testing.py b/api/controllers/console/datasets/hit_testing.py index 16bb571df3..771d49045f 100644 --- a/api/controllers/console/datasets/hit_testing.py +++ b/api/controllers/console/datasets/hit_testing.py @@ -6,9 +6,12 @@ from werkzeug.exceptions import InternalServerError, NotFound, Forbidden import services from controllers.console import api +from controllers.console.app.error import ProviderNotInitializeError, ProviderQuotaExceededError, \ + ProviderModelCurrentlyNotSupportError from controllers.console.datasets.error import HighQualityDatasetOnlyError, DatasetNotInitializedError from controllers.console.setup import setup_required from controllers.console.wraps import account_initialization_required +from core.llm.error import ProviderTokenNotInitError, QuotaExceededError, ModelCurrentlyNotSupportError from libs.helper import TimestampField from services.dataset_service import DatasetService from services.hit_testing_service import HitTestingService @@ -92,6 +95,12 @@ class HitTestingApi(Resource): return {"query": response['query'], 'records': marshal(response['records'], hit_testing_record_fields)} except services.errors.index.IndexNotInitializedError: raise DatasetNotInitializedError() + except ProviderTokenNotInitError: + raise ProviderNotInitializeError() + except QuotaExceededError: + raise ProviderQuotaExceededError() + except ModelCurrentlyNotSupportError: + raise ProviderModelCurrentlyNotSupportError() except Exception as e: logging.exception("Hit testing failed.") raise InternalServerError(str(e)) diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index 3040423d71..e563364f27 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -3,13 +3,14 @@ from libs.exception import BaseHTTPException class AlreadySetupError(BaseHTTPException): error_code = 'already_setup' - description = "Application already setup." + description = "Dify has been successfully installed. Please refresh the page or return to the dashboard homepage." code = 403 class NotSetupError(BaseHTTPException): error_code = 'not_setup' - description = "Application not setup." + description = "Dify has not been initialized and installed yet. " \ + "Please proceed with the initialization and installation process first." code = 401 diff --git a/api/controllers/console/workspace/error.py b/api/controllers/console/workspace/error.py index c5e3a3fb6a..cb744232ec 100644 --- a/api/controllers/console/workspace/error.py +++ b/api/controllers/console/workspace/error.py @@ -21,11 +21,11 @@ class InvalidInvitationCodeError(BaseHTTPException): class AccountAlreadyInitedError(BaseHTTPException): error_code = 'account_already_inited' - description = "Account already inited." + description = "The account has been initialized. Please refresh the page." code = 400 class AccountNotInitializedError(BaseHTTPException): error_code = 'account_not_initialized' - description = "Account not initialized." + description = "The account has not been initialized yet. Please proceed with the initialization process first." code = 400 diff --git a/api/controllers/console/workspace/providers.py b/api/controllers/console/workspace/providers.py index dc9e9c45f1..87dad0d93a 100644 --- a/api/controllers/console/workspace/providers.py +++ b/api/controllers/console/workspace/providers.py @@ -90,8 +90,8 @@ class ProviderTokenApi(Resource): configs=args['token'] ) token_is_valid = True - except ValidateFailedError: - token_is_valid = False + except ValidateFailedError as ex: + raise ValueError(str(ex)) base64_encrypted_token = ProviderService.get_encrypted_token( tenant=current_user.current_tenant, diff --git a/api/controllers/service_api/app/error.py b/api/controllers/service_api/app/error.py index c59f570efd..b7f6e0f6fa 100644 --- a/api/controllers/service_api/app/error.py +++ b/api/controllers/service_api/app/error.py @@ -4,43 +4,45 @@ from libs.exception import BaseHTTPException class AppUnavailableError(BaseHTTPException): error_code = 'app_unavailable' - description = "App unavailable." + description = "App unavailable, please check your app configurations." code = 400 class NotCompletionAppError(BaseHTTPException): error_code = 'not_completion_app' - description = "Not Completion App" + description = "Please check if your Completion app mode matches the right API route." code = 400 class NotChatAppError(BaseHTTPException): error_code = 'not_chat_app' - description = "Not Chat App" + description = "Please check if your Chat app mode matches the right API route." code = 400 class ConversationCompletedError(BaseHTTPException): error_code = 'conversation_completed' - description = "Conversation Completed." + description = "The conversation has ended. Please start a new conversation." code = 400 class ProviderNotInitializeError(BaseHTTPException): error_code = 'provider_not_initialize' - description = "Provider Token not initialize." + description = "No valid model provider credentials found. " \ + "Please go to Settings -> Model Provider to complete your provider credentials." code = 400 class ProviderQuotaExceededError(BaseHTTPException): error_code = 'provider_quota_exceeded' - description = "Provider quota exceeded." + description = "Your quota for Dify Hosted OpenAI has been exhausted. " \ + "Please go to Settings -> Model Provider to complete your own provider credentials." code = 400 class ProviderModelCurrentlyNotSupportError(BaseHTTPException): error_code = 'model_currently_not_support' - description = "GPT-4 currently not support." + description = "Dify Hosted OpenAI trial currently not support the GPT-4 model." code = 400 diff --git a/api/controllers/service_api/dataset/error.py b/api/controllers/service_api/dataset/error.py index d231e0b40a..2131fe0bac 100644 --- a/api/controllers/service_api/dataset/error.py +++ b/api/controllers/service_api/dataset/error.py @@ -16,5 +16,5 @@ class DocumentIndexingError(BaseHTTPException): class DatasetNotInitedError(BaseHTTPException): error_code = 'dataset_not_inited' - description = "Dataset not inited." + description = "The dataset is still being initialized or indexing. Please wait a moment." code = 403 diff --git a/api/controllers/web/error.py b/api/controllers/web/error.py index ea72422a1b..fdfe36f6d1 100644 --- a/api/controllers/web/error.py +++ b/api/controllers/web/error.py @@ -4,43 +4,45 @@ from libs.exception import BaseHTTPException class AppUnavailableError(BaseHTTPException): error_code = 'app_unavailable' - description = "App unavailable." + description = "App unavailable, please check your app configurations." code = 400 class NotCompletionAppError(BaseHTTPException): error_code = 'not_completion_app' - description = "Not Completion App" + description = "Please check if your Completion app mode matches the right API route." code = 400 class NotChatAppError(BaseHTTPException): error_code = 'not_chat_app' - description = "Not Chat App" + description = "Please check if your Chat app mode matches the right API route." code = 400 class ConversationCompletedError(BaseHTTPException): error_code = 'conversation_completed' - description = "Conversation Completed." + description = "The conversation has ended. Please start a new conversation." code = 400 class ProviderNotInitializeError(BaseHTTPException): error_code = 'provider_not_initialize' - description = "Provider Token not initialize." + description = "No valid model provider credentials found. " \ + "Please go to Settings -> Model Provider to complete your provider credentials." code = 400 class ProviderQuotaExceededError(BaseHTTPException): error_code = 'provider_quota_exceeded' - description = "Provider quota exceeded." + description = "Your quota for Dify Hosted OpenAI has been exhausted. " \ + "Please go to Settings -> Model Provider to complete your own provider credentials." code = 400 class ProviderModelCurrentlyNotSupportError(BaseHTTPException): error_code = 'model_currently_not_support' - description = "GPT-4 currently not support." + description = "Dify Hosted OpenAI trial currently not support the GPT-4 model." code = 400 @@ -52,11 +54,11 @@ class CompletionRequestError(BaseHTTPException): class AppMoreLikeThisDisabledError(BaseHTTPException): error_code = 'app_more_like_this_disabled' - description = "More like this disabled." + description = "The 'More like this' feature is disabled. Please refresh your page." code = 403 class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException): error_code = 'app_suggested_questions_after_answer_disabled' - description = "Function Suggested questions after answer disabled." + description = "The 'Suggested Questions After Answer' feature is disabled. Please refresh your page." code = 403 diff --git a/api/core/index/index_builder.py b/api/core/index/index_builder.py index 7f0486546e..05f08075d4 100644 --- a/api/core/index/index_builder.py +++ b/api/core/index/index_builder.py @@ -46,3 +46,15 @@ class IndexBuilder: prompt_helper=prompt_helper, embed_model=OpenAIEmbedding(**model_credentials), ) + + @classmethod + def get_fake_llm_service_context(cls, tenant_id: str) -> ServiceContext: + llm = LLMBuilder.to_llm( + tenant_id=tenant_id, + model_name='fake' + ) + + return ServiceContext.from_defaults( + llm_predictor=LLMPredictor(llm=llm), + embed_model=OpenAIEmbedding() + ) diff --git a/api/core/index/vector_index.py b/api/core/index/vector_index.py index f9d8542a8c..fa1c93cc06 100644 --- a/api/core/index/vector_index.py +++ b/api/core/index/vector_index.py @@ -83,7 +83,7 @@ class VectorIndex: if not self._dataset.index_struct_dict: return - service_context = IndexBuilder.get_default_service_context(tenant_id=self._dataset.tenant_id) + service_context = IndexBuilder.get_fake_llm_service_context(tenant_id=self._dataset.tenant_id) index = vector_store.get_index( service_context=service_context, @@ -101,7 +101,7 @@ class VectorIndex: if not self._dataset.index_struct_dict: return - service_context = IndexBuilder.get_default_service_context(tenant_id=self._dataset.tenant_id) + service_context = IndexBuilder.get_fake_llm_service_context(tenant_id=self._dataset.tenant_id) index = vector_store.get_index( service_context=service_context, diff --git a/api/core/llm/provider/azure_provider.py b/api/core/llm/provider/azure_provider.py index d68ed3ccc4..bd44a0cc4b 100644 --- a/api/core/llm/provider/azure_provider.py +++ b/api/core/llm/provider/azure_provider.py @@ -1,22 +1,24 @@ import json +import logging from typing import Optional, Union import requests from core.llm.provider.base import BaseProvider +from core.llm.provider.errors import ValidateFailedError from models.provider import ProviderName class AzureProvider(BaseProvider): - def get_models(self, model_id: Optional[str] = None) -> list[dict]: - credentials = self.get_credentials(model_id) + def get_models(self, model_id: Optional[str] = None, credentials: Optional[dict] = None) -> list[dict]: + credentials = self.get_credentials(model_id) if not credentials else credentials url = "{}/openai/deployments?api-version={}".format( - credentials.get('openai_api_base'), - credentials.get('openai_api_version') + str(credentials.get('openai_api_base')), + str(credentials.get('openai_api_version')) ) headers = { - "api-key": credentials.get('openai_api_key'), + "api-key": str(credentials.get('openai_api_key')), "content-type": "application/json; charset=utf-8" } @@ -29,8 +31,10 @@ class AzureProvider(BaseProvider): 'name': '{} ({})'.format(deployment['id'], deployment['model']) } for deployment in result['data'] if deployment['status'] == 'succeeded'] else: - # TODO: optimize in future - raise Exception('Failed to get deployments from Azure OpenAI. Status code: {}'.format(response.status_code)) + if response.status_code == 401: + raise AzureAuthenticationError() + else: + raise AzureRequestFailedError('Failed to request Azure OpenAI. Status code: {}'.format(response.status_code)) def get_credentials(self, model_id: Optional[str] = None) -> dict: """ @@ -38,7 +42,7 @@ class AzureProvider(BaseProvider): """ config = self.get_provider_api_key(model_id=model_id) config['openai_api_type'] = 'azure' - config['deployment_name'] = model_id.replace('.', '') + config['deployment_name'] = model_id.replace('.', '') if model_id else None return config def get_provider_name(self): @@ -54,7 +58,7 @@ class AzureProvider(BaseProvider): config = { 'openai_api_type': 'azure', 'openai_api_version': '2023-03-15-preview', - 'openai_api_base': 'https://.openai.azure.com/', + 'openai_api_base': '', 'openai_api_key': '' } @@ -63,7 +67,7 @@ class AzureProvider(BaseProvider): config = { 'openai_api_type': 'azure', 'openai_api_version': '2023-03-15-preview', - 'openai_api_base': 'https://.openai.azure.com/', + 'openai_api_base': '', 'openai_api_key': '' } @@ -80,8 +84,23 @@ class AzureProvider(BaseProvider): """ Validates the given config. """ - # TODO: implement - pass + try: + if not isinstance(config, dict): + raise ValueError('Config must be a object.') + + if 'openai_api_version' not in config: + config['openai_api_version'] = '2023-03-15-preview' + + self.get_models(credentials=config) + except AzureAuthenticationError: + raise ValidateFailedError('Azure OpenAI Credentials validation failed, please check your API Key.') + except requests.ConnectionError: + raise ValidateFailedError('Azure OpenAI Credentials validation failed, please check your API Base Endpoint.') + except AzureRequestFailedError as ex: + raise ValidateFailedError('Azure OpenAI Credentials validation failed, error: {}.'.format(str(ex))) + except Exception as ex: + logging.exception('Azure OpenAI Credentials validation failed') + raise ex def get_encrypted_token(self, config: Union[dict | str]): """ @@ -101,3 +120,11 @@ class AzureProvider(BaseProvider): config = json.loads(token) config['openai_api_key'] = self.decrypt_token(config['openai_api_key']) return config + + +class AzureAuthenticationError(Exception): + pass + + +class AzureRequestFailedError(Exception): + pass diff --git a/web/i18n/lang/common.en.ts b/web/i18n/lang/common.en.ts index 96304d2677..fa73fbd8a8 100644 --- a/web/i18n/lang/common.en.ts +++ b/web/i18n/lang/common.en.ts @@ -149,7 +149,7 @@ const translation = { invalidApiKey: 'Invalid API key', azure: { apiBase: 'API Base', - apiBasePlaceholder: 'The API Base URL of your Azure OpenAI Resource.', + apiBasePlaceholder: 'The API Base URL of your Azure OpenAI Endpoint.', apiKey: 'API Key', apiKeyPlaceholder: 'Enter your API key here', helpTip: 'Learn Azure OpenAI Service', From 4ba38465acf3c7a5b25fb682351f76dac935515e Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 23 May 2023 10:43:38 +0800 Subject: [PATCH 11/32] fix: dark-theme-btn-selected (#156) --- web/app/styles/globals.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/app/styles/globals.css b/web/app/styles/globals.css index f4710b0275..57145790ec 100644 --- a/web/app/styles/globals.css +++ b/web/app/styles/globals.css @@ -131,4 +131,10 @@ button:focus-within { -webkit-text-fill-color: transparent; background-clip: text; text-fill-color: transparent; +} + +/* overwrite paging active dark model style */ +[class*=style_paginatio] li .text-primary-600 { + color: rgb(28 100 242); + background-color: rgb(235 245 255); } \ No newline at end of file From 7722a7c5cd978d5f3d87d54f276dd66e79fa4126 Mon Sep 17 00:00:00 2001 From: Yuhao Date: Tue, 23 May 2023 10:48:03 +0800 Subject: [PATCH 12/32] fix: bootstrap env (#127) Co-authored-by: yuhao1118 --- api/.env.example | 2 +- web/.env.example | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 web/.env.example diff --git a/api/.env.example b/api/.env.example index 5f307dc106..4e2d76f810 100644 --- a/api/.env.example +++ b/api/.env.example @@ -14,7 +14,7 @@ CONSOLE_URL=http://127.0.0.1:5001 API_URL=http://127.0.0.1:5001 # Web APP base URL -APP_URL=http://127.0.0.1:5001 +APP_URL=http://127.0.0.1:3000 # celery configuration CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1 diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000000..acd853834f --- /dev/null +++ b/web/.env.example @@ -0,0 +1,12 @@ +# For production release, change this to PRODUCTION +NEXT_PUBLIC_DEPLOY_ENV=DEVELOPMENT +# The deployment edition, SELF_HOSTED or CLOUD +NEXT_PUBLIC_EDITION=SELF_HOSTED +# The base URL of console application, refers to the Console base URL of WEB service if console domain is +# different from api or web app domain. +# example: http://cloud.dify.ai/console/api +NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api +# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from +# console or api domain. +# example: http://udify.app/api +NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api \ No newline at end of file From 90150a6ca9bd7e45b5614eab95e2fc2ee909977e Mon Sep 17 00:00:00 2001 From: John Wang Date: Tue, 23 May 2023 12:26:28 +0800 Subject: [PATCH 13/32] Feat/optimize chat prompt (#158) --- api/core/completion.py | 69 ++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/api/core/completion.py b/api/core/completion.py index 8e79e03a93..afa40b45cd 100644 --- a/api/core/completion.py +++ b/api/core/completion.py @@ -39,7 +39,8 @@ class Completion: memory = cls.get_memory_from_conversation( tenant_id=app.tenant_id, app_model_config=app_model_config, - conversation=conversation + conversation=conversation, + return_messages=False ) inputs = conversation.inputs @@ -119,7 +120,8 @@ class Completion: return response @classmethod - def get_main_llm_prompt(cls, mode: str, llm: BaseLanguageModel, pre_prompt: str, query: str, inputs: dict, chain_output: Optional[str], + def get_main_llm_prompt(cls, mode: str, llm: BaseLanguageModel, pre_prompt: str, query: str, inputs: dict, + chain_output: Optional[str], memory: Optional[ReadOnlyConversationTokenDBBufferSharedMemory]) -> \ Union[str | List[BaseMessage]]: pre_prompt = PromptBuilder.process_template(pre_prompt) if pre_prompt else pre_prompt @@ -161,11 +163,19 @@ And answer according to the language of the user's question. "query": query } - human_message_prompt = "{query}" + human_message_prompt = "" + + if pre_prompt: + pre_prompt_inputs = {k: inputs[k] for k in + OutLinePromptTemplate.from_template(template=pre_prompt).input_variables + if k in inputs} + + if pre_prompt_inputs: + human_inputs.update(pre_prompt_inputs) if chain_output: human_inputs['context'] = chain_output - human_message_instruction = """Use the following CONTEXT as your learned knowledge. + human_message_prompt += """Use the following CONTEXT as your learned knowledge. [CONTEXT] {context} [END CONTEXT] @@ -176,23 +186,27 @@ When answer to user: Avoid mentioning that you obtained the information from the context. And answer according to the language of the user's question. """ - if pre_prompt: - extra_inputs = {k: inputs[k] for k in - OutLinePromptTemplate.from_template(template=pre_prompt).input_variables - if k in inputs} - if extra_inputs: - human_inputs.update(extra_inputs) - human_message_instruction += pre_prompt + "\n" - human_message_prompt = human_message_instruction + "Q:{query}\nA:" - else: - if pre_prompt: - extra_inputs = {k: inputs[k] for k in - OutLinePromptTemplate.from_template(template=pre_prompt).input_variables - if k in inputs} - if extra_inputs: - human_inputs.update(extra_inputs) - human_message_prompt = pre_prompt + "\n" + human_message_prompt + if pre_prompt: + human_message_prompt += pre_prompt + + query_prompt = "\nHuman: {query}\nAI: " + + if memory: + # append chat histories + tmp_human_message = PromptBuilder.to_human_message( + prompt_content=human_message_prompt + query_prompt, + inputs=human_inputs + ) + + curr_message_tokens = memory.llm.get_messages_tokens([tmp_human_message]) + rest_tokens = llm_constant.max_context_token_length[memory.llm.model_name] \ + - memory.llm.max_tokens - curr_message_tokens + rest_tokens = max(rest_tokens, 0) + history_messages = cls.get_history_messages_from_memory(memory, rest_tokens) + human_message_prompt += "\n\n" + history_messages + + human_message_prompt += query_prompt # construct main prompt human_message = PromptBuilder.to_human_message( @@ -200,23 +214,14 @@ And answer according to the language of the user's question. inputs=human_inputs ) - if memory: - # append chat histories - tmp_messages = messages.copy() + [human_message] - curr_message_tokens = memory.llm.get_messages_tokens(tmp_messages) - rest_tokens = llm_constant.max_context_token_length[ - memory.llm.model_name] - memory.llm.max_tokens - curr_message_tokens - rest_tokens = max(rest_tokens, 0) - history_messages = cls.get_history_messages_from_memory(memory, rest_tokens) - messages += history_messages - messages.append(human_message) return messages @classmethod def get_llm_callback_manager(cls, llm: Union[StreamableOpenAI, StreamableChatOpenAI], - streaming: bool, conversation_message_task: ConversationMessageTask) -> CallbackManager: + streaming: bool, + conversation_message_task: ConversationMessageTask) -> CallbackManager: llm_callback_handler = LLMCallbackHandler(llm, conversation_message_task) if streaming: callback_handlers = [llm_callback_handler, DifyStreamingStdOutCallbackHandler()] @@ -228,7 +233,7 @@ And answer according to the language of the user's question. @classmethod def get_history_messages_from_memory(cls, memory: ReadOnlyConversationTokenDBBufferSharedMemory, max_token_limit: int) -> \ - List[BaseMessage]: + str: """Get memory messages.""" memory.max_token_limit = max_token_limit memory_key = memory.memory_variables[0] From 219011b62ae8fcc31f59dd99862be4690ce1af7c Mon Sep 17 00:00:00 2001 From: John Wang Date: Tue, 23 May 2023 12:57:26 +0800 Subject: [PATCH 14/32] fix: disable template string in query (#160) --- api/core/completion.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/core/completion.py b/api/core/completion.py index afa40b45cd..6b4250c4c5 100644 --- a/api/core/completion.py +++ b/api/core/completion.py @@ -124,6 +124,13 @@ class Completion: chain_output: Optional[str], memory: Optional[ReadOnlyConversationTokenDBBufferSharedMemory]) -> \ Union[str | List[BaseMessage]]: + # disable template string in query + query_params = OutLinePromptTemplate.from_template(template=query).input_variables + if query_params: + for query_param in query_params: + if query_param not in inputs: + inputs[query_param] = '{' + query_param + '}' + pre_prompt = PromptBuilder.process_template(pre_prompt) if pre_prompt else pre_prompt if mode == 'completion': prompt_template = OutLinePromptTemplate.from_template( From f3219ff107bb7761ca3f22877b25164c5d7ee974 Mon Sep 17 00:00:00 2001 From: John Wang Date: Tue, 23 May 2023 13:16:33 +0800 Subject: [PATCH 15/32] fix: template string in template error (#162) --- api/core/completion.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/api/core/completion.py b/api/core/completion.py index 6b4250c4c5..47658f3bf1 100644 --- a/api/core/completion.py +++ b/api/core/completion.py @@ -151,6 +151,11 @@ And answer according to the language of the user's question. if chain_output: inputs['context'] = chain_output + context_params = OutLinePromptTemplate.from_template(template=chain_output).input_variables + if context_params: + for context_param in context_params: + if context_param not in inputs: + inputs[context_param] = '{' + context_param + '}' prompt_inputs = {k: inputs[k] for k in prompt_template.input_variables if k in inputs} prompt_content = prompt_template.format( @@ -210,8 +215,16 @@ And answer according to the language of the user's question. rest_tokens = llm_constant.max_context_token_length[memory.llm.model_name] \ - memory.llm.max_tokens - curr_message_tokens rest_tokens = max(rest_tokens, 0) - history_messages = cls.get_history_messages_from_memory(memory, rest_tokens) - human_message_prompt += "\n\n" + history_messages + histories = cls.get_history_messages_from_memory(memory, rest_tokens) + + # disable template string in query + histories_params = OutLinePromptTemplate.from_template(template=histories).input_variables + if histories_params: + for histories_param in histories_params: + if histories_param not in human_inputs: + human_inputs[histories_param] = '{' + histories_param + '}' + + human_message_prompt += "\n\n" + histories human_message_prompt += query_prompt From 1c5f63de7e38685bb6820a06975d70262c914003 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 23 May 2023 14:15:33 +0800 Subject: [PATCH 16/32] fix: azure-openai key validate (#164) --- .../provider-page/azure-provider/index.tsx | 69 ++++- .../account-setting/provider-page/index.tsx | 2 +- .../provider-page/openai-provider/index.tsx | 272 +++++------------- .../openai-provider/provider.tsx | 52 ---- .../provider-page/provider-input/Validate.tsx | 59 ++++ .../provider-page/provider-input/index.tsx | 103 +------ .../provider-input/useValidateToken.ts | 29 +- .../provider-page/provider-item/index.tsx | 25 +- web/models/common.ts | 29 +- 9 files changed, 254 insertions(+), 386 deletions(-) delete mode 100644 web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx create mode 100644 web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx diff --git a/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx b/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx index 5681fd2204..1cbe7d0674 100644 --- a/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx @@ -1,10 +1,17 @@ import type { Provider, ProviderAzureToken } from '@/models/common' +import { ProviderName } from '@/models/common' import { useTranslation } from 'react-i18next' import Link from 'next/link' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' -import ProviderInput, { ProviderValidateTokenInput} from '../provider-input' -import { useState } from 'react' -import { ValidatedStatus } from '../provider-input/useValidateToken' +import { useState, useEffect } from 'react' +import ProviderInput from '../provider-input' +import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken' +import { + ValidatedErrorIcon, + ValidatedSuccessIcon, + ValidatingTip, + ValidatedErrorOnAzureOpenaiTip +} from '../provider-input/Validate' interface IAzureProviderProps { provider: Provider @@ -17,19 +24,51 @@ const AzureProvider = ({ onValidatedStatus }: IAzureProviderProps) => { const { t } = useTranslation() - const [token, setToken] = useState(provider.token as ProviderAzureToken || {}) - const handleFocus = () => { - if (token === provider.token) { - token.openai_api_key = '' + const [token, setToken] = useState(provider.provider_name === ProviderName.AZURE_OPENAI ? {...provider.token}: {}) + const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name) + const handleFocus = (type: keyof ProviderAzureToken) => { + if (token[type] === (provider?.token as ProviderAzureToken)[type]) { + token[type] = '' setToken({...token}) onTokenChange({...token}) + setValidatedStatus(undefined) } } - const handleChange = (type: keyof ProviderAzureToken, v: string) => { + const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => { token[type] = v setToken({...token}) onTokenChange({...token}) + validate({...token}, { + beforeValidating: () => { + if (!token.openai_api_base || !token.openai_api_key) { + setValidatedStatus(undefined) + return false + } + return true + } + }) } + const getValidatedIcon = () => { + if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) { + return + } + if (validatedStatus === ValidatedStatus.Success) { + return + } + } + const getValidatedTip = () => { + if (validating) { + return + } + if (validatedStatus === ValidatedStatus.Error) { + return + } + } + useEffect(() => { + if (typeof onValidatedStatus === 'function') { + onValidatedStatus(validatedStatus) + } + }, [validatedStatus]) return (
@@ -38,17 +77,19 @@ const AzureProvider = ({ name={t('common.provider.azure.apiBase')} placeholder={t('common.provider.azure.apiBasePlaceholder')} value={token.openai_api_base} - onChange={(v) => handleChange('openai_api_base', v)} + onChange={(v) => handleChange('openai_api_base', v, validate)} + onFocus={() => handleFocus('openai_api_base')} + validatedIcon={getValidatedIcon()} /> - handleChange('openai_api_key', v)} - onFocus={handleFocus} - onValidatedStatus={onValidatedStatus} - providerName={provider.provider_name} + onChange={(v) => handleChange('openai_api_key', v, validate)} + onFocus={() => handleFocus('openai_api_key')} + validatedIcon={getValidatedIcon()} + validatedTip={getValidatedTip()} /> {t('common.provider.azure.helpTip')} diff --git a/web/app/components/header/account-setting/provider-page/index.tsx b/web/app/components/header/account-setting/provider-page/index.tsx index c1bce2c54c..67112b8142 100644 --- a/web/app/components/header/account-setting/provider-page/index.tsx +++ b/web/app/components/header/account-setting/provider-page/index.tsx @@ -67,7 +67,7 @@ const ProviderPage = () => { const providerHosted = data?.filter(provider => provider.provider_name === 'openai' && provider.provider_type === 'system')?.[0] return ( -
+
{ providerHosted && !IS_CE_EDITION && ( <> diff --git a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx index adff6bdf30..f49b229812 100644 --- a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx @@ -1,222 +1,94 @@ -import { ChangeEvent, useEffect, useRef, useState } from 'react' -import { useContext } from 'use-context-selector' +import type { Provider } from '@/models/common' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { debounce } from 'lodash-es' +import ProviderInput from '../provider-input' import Link from 'next/link' -import useSWR from 'swr' -import { ArrowTopRightOnSquareIcon, PencilIcon } from '@heroicons/react/24/outline' -import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid' -import Button from '@/app/components/base/button' -import s from './index.module.css' -import classNames from 'classnames' -import { fetchTenantInfo, validateProviderKey, updateProviderAIKey } from '@/service/common' -import { ToastContext } from '@/app/components/base/toast' -import Indicator from '../../../indicator' -import I18n from '@/context/i18n' +import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' +import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken' +import { + ValidatedErrorIcon, + ValidatedSuccessIcon, + ValidatingTip, + ValidatedExceedOnOpenaiTip, + ValidatedErrorOnOpenaiTip +} from '../provider-input/Validate' -type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill' - -type TInputWithStatusProps = { - value: string - onChange: (v: string) => void - onValidating: (validating: boolean) => void - verifiedStatus: IStatusType - onVerified: (verified: IStatusType) => void -} -const InputWithStatus = ({ - value, - onChange, - onValidating, - verifiedStatus, - onVerified -}: TInputWithStatusProps) => { - const { t } = useTranslation() - const validateKey = useRef(debounce(async (token: string) => { - if (!token) return - onValidating(true) - try { - const res = await validateProviderKey({ url: '/workspaces/current/providers/openai/token-validate', body: { token } }) - onVerified(res.result === 'success' ? 'verified' : 'error') - } catch (e: any) { - if (e.status === 400) { - e.json().then(({ code }: any) => { - if (code === 'provider_request_failed') { - onVerified('error-api-key-exceed-bill') - } - }) - } else { - onVerified('error') - } - } finally { - onValidating(false) - } - }, 500)) - - const handleChange = (e: ChangeEvent) => { - const inputValue = e.target.value - onChange(inputValue) - if (!inputValue) { - onVerified('normal') - } - validateKey.current(inputValue) - } - return ( -
- - { - verifiedStatus === 'error' && - } - { - verifiedStatus === 'verified' && - } -
- ) +interface IOpenaiProviderProps { + provider: Provider + onValidatedStatus: (status?: ValidatedStatus) => void + onTokenChange: (token: string) => void } -const OpenaiProvider = () => { +const OpenaiProvider = ({ + provider, + onValidatedStatus, + onTokenChange +}: IOpenaiProviderProps) => { const { t } = useTranslation() - const { locale } = useContext(I18n) - const { data: userInfo, mutate } = useSWR({ url: '/info' }, fetchTenantInfo) - const [inputValue, setInputValue] = useState('') - const [validating, setValidating] = useState(false) - const [editStatus, setEditStatus] = useState('normal') - const [loading, setLoading] = useState(false) - const [editing, setEditing] = useState(false) - const [invalidStatus, setInvalidStatus] = useState(false) - const { notify } = useContext(ToastContext) - const provider = userInfo?.providers?.find(({ provider }) => provider === 'openai') - - const handleReset = () => { - setInputValue('') - setValidating(false) - setEditStatus('normal') - setLoading(false) - setEditing(false) - } - const handleSave = async () => { - if (editStatus === 'verified') { - try { - setLoading(true) - await updateProviderAIKey({ url: '/workspaces/current/providers/openai/token', body: { token: inputValue ?? '' } }) - notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - } catch (e) { - notify({ type: 'error', message: t('common.provider.saveFailed') }) - } finally { - setLoading(false) - handleReset() - mutate() - } + const [token, setToken] = useState(provider.token as string || '') + const [ validating, validatedStatus, setValidatedStatus, validate ] = useValidateToken(provider.provider_name) + const handleFocus = () => { + if (token === provider.token) { + setToken('') + onTokenChange('') + setValidatedStatus(undefined) } } + const handleChange = (v: string) => { + setToken(v) + onTokenChange(v) + validate(v, { + beforeValidating: () => { + if (!v) { + setValidatedStatus(undefined) + return false + } + return true + } + }) + } useEffect(() => { - if (provider && !provider.token_is_valid && provider.token_is_set) { - setInvalidStatus(true) + if (typeof onValidatedStatus === 'function') { + onValidatedStatus(validatedStatus) } - }, [userInfo]) + }, [validatedStatus]) - const showInvalidStatus = invalidStatus && !editing - const renderErrorMessage = () => { + const getValidatedIcon = () => { + if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) { + return + } + if (validatedStatus === ValidatedStatus.Success) { + return + } + } + const getValidatedTip = () => { if (validating) { - return ( -
- {t('common.provider.validating')} -
- ) + return } - if (editStatus === 'error-api-key-exceed-bill') { - return ( -
- {t('common.provider.apiKeyExceedBill')}  - - {locale === 'en' ? 'this link' : '这篇文档'} - -
- ) + if (validatedStatus === ValidatedStatus.Exceed) { + return } - if (showInvalidStatus || editStatus === 'error') { - return ( -
- {t('common.provider.invalidKey')} -
- ) + if (validatedStatus === ValidatedStatus.Error) { + return } - return null } return (
-
-
- {t('common.provider.apiKey')} -
- { - provider && !editing && ( -
setEditing(true)} - > - - {t('common.operation.edit')} -
- ) - } - { - (inputValue || editing) && ( - <> - - - - ) - } -
- { - (!provider || (provider && editing)) && ( - setInputValue(v)} - verifiedStatus={editStatus} - onVerified={v => setEditStatus(v)} - onValidating={v => setValidating(v)} - /> - ) - } - { - (provider && !editing) && ( -
- sk-0C...skuA - -
- ) - } - {renderErrorMessage()} - - {t('appOverview.welcome.getKeyTip')} -
+ + + {t('appOverview.welcome.getKeyTip')} +
) } diff --git a/web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx b/web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx deleted file mode 100644 index 45747cb3a8..0000000000 --- a/web/app/components/header/account-setting/provider-page/openai-provider/provider.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Provider } from '@/models/common' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { ProviderValidateTokenInput } from '../provider-input' -import Link from 'next/link' -import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' -import { ValidatedStatus } from '../provider-input/useValidateToken' - -interface IOpenaiProviderProps { - provider: Provider - onValidatedStatus: (status?: ValidatedStatus) => void - onTokenChange: (token: string) => void -} - -const OpenaiProvider = ({ - provider, - onValidatedStatus, - onTokenChange -}: IOpenaiProviderProps) => { - const { t } = useTranslation() - const [token, setToken] = useState(provider.token as string || '') - const handleFocus = () => { - if (token === provider.token) { - setToken('') - onTokenChange('') - } - } - const handleChange = (v: string) => { - setToken(v) - onTokenChange(v) - } - - return ( -
- - - {t('appOverview.welcome.getKeyTip')} -
- ) -} - -export default OpenaiProvider \ No newline at end of file diff --git a/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx b/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx new file mode 100644 index 0000000000..740a149a93 --- /dev/null +++ b/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' +import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import I18n from '@/context/i18n' + +export const ValidatedErrorIcon = () => { + return +} + +export const ValidatedSuccessIcon = () => { + return +} + +export const ValidatingTip = () => { + const { t } = useTranslation() + return ( +
+ {t('common.provider.validating')} +
+ ) +} + +export const ValidatedExceedOnOpenaiTip = () => { + const { t } = useTranslation() + const { locale } = useContext(I18n) + + return ( +
+ {t('common.provider.apiKeyExceedBill')}  + + {locale === 'en' ? 'this link' : '这篇文档'} + +
+ ) +} + +export const ValidatedErrorOnOpenaiTip = () => { + const { t } = useTranslation() + + return ( +
+ {t('common.provider.invalidKey')} +
+ ) +} + +export const ValidatedErrorOnAzureOpenaiTip = () => { + const { t } = useTranslation() + + return ( +
+ {t('common.provider.invalidApiKey')} +
+ ) +} \ No newline at end of file diff --git a/web/app/components/header/account-setting/provider-page/provider-input/index.tsx b/web/app/components/header/account-setting/provider-page/provider-input/index.tsx index 5a489d17f2..84ab9901c1 100644 --- a/web/app/components/header/account-setting/provider-page/provider-input/index.tsx +++ b/web/app/components/header/account-setting/provider-page/provider-input/index.tsx @@ -1,10 +1,5 @@ -import { ChangeEvent, useEffect } from 'react' -import Link from 'next/link' -import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid' -import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import useValidateToken, { ValidatedStatus } from './useValidateToken' +import { ChangeEvent } from 'react' +import { ReactElement } from 'react-markdown/lib/react-markdown' interface IProviderInputProps { value?: string @@ -13,6 +8,8 @@ interface IProviderInputProps { className?: string onChange: (v: string) => void onFocus?: () => void + validatedIcon?: ReactElement + validatedTip?: ReactElement } const ProviderInput = ({ @@ -22,6 +19,8 @@ const ProviderInput = ({ className, onChange, onFocus, + validatedIcon, + validatedTip }: IProviderInputProps) => { const handleChange = (e: ChangeEvent) => { @@ -47,95 +46,9 @@ const ProviderInput = ({ onChange={handleChange} onFocus={onFocus} /> + {validatedIcon}
-
- ) -} - -type TproviderInputProps = IProviderInputProps - & { - onValidatedStatus?: (status?: ValidatedStatus) => void - providerName: string - } -export const ProviderValidateTokenInput = ({ - value, - name, - placeholder, - className, - onChange, - onFocus, - onValidatedStatus, - providerName -}: TproviderInputProps) => { - const { t } = useTranslation() - const { locale } = useContext(I18n) - const [ validating, validatedStatus, validate ] = useValidateToken(providerName) - - useEffect(() => { - if (typeof onValidatedStatus === 'function') { - onValidatedStatus(validatedStatus) - } - }, [validatedStatus]) - - const handleChange = (e: ChangeEvent) => { - const inputValue = e.target.value - onChange(inputValue) - - validate(inputValue) - } - - return ( -
-
{name}
-
- - { - validatedStatus === ValidatedStatus.Error && - } - { - validatedStatus === ValidatedStatus.Success && - } -
- { - validating && ( -
- {t('common.provider.validating')} -
- ) - } - { - validatedStatus === ValidatedStatus.Exceed && !validating && ( -
- {t('common.provider.apiKeyExceedBill')}  - - {locale === 'en' ? 'this link' : '这篇文档'} - -
- ) - } - { - validatedStatus === ValidatedStatus.Error && !validating && ( -
- {t('common.provider.invalidKey')} -
- ) - } + {validatedTip}
) } diff --git a/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts b/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts index 5064910671..69b7529449 100644 --- a/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts +++ b/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, SetStateAction, Dispatch } from 'react' import debounce from 'lodash-es/debounce' import { DebouncedFunc } from 'lodash-es' import { validateProviderKey } from '@/service/common' @@ -8,14 +8,24 @@ export enum ValidatedStatus { Error = 'error', Exceed = 'exceed' } +export type SetValidatedStatus = Dispatch> +export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void> +type ValidateTokenReturn = [ + boolean, + ValidatedStatus | undefined, + SetValidatedStatus, + ValidateFn +] +export type ValidateFnConfig = { + beforeValidating: (token: any) => boolean +} -const useValidateToken = (providerName: string): [boolean, ValidatedStatus | undefined, DebouncedFunc<(token: string) => Promise>] => { +const useValidateToken = (providerName: string): ValidateTokenReturn => { const [validating, setValidating] = useState(false) const [validatedStatus, setValidatedStatus] = useState() - const validate = useCallback(debounce(async (token: string) => { - if (!token) { - setValidatedStatus(undefined) - return + const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => { + if (!config.beforeValidating(token)) { + return false } setValidating(true) try { @@ -24,8 +34,10 @@ const useValidateToken = (providerName: string): [boolean, ValidatedStatus | und } catch (e: any) { if (e.status === 400) { e.json().then(({ code }: any) => { - if (code === 'provider_request_failed') { + if (code === 'provider_request_failed' && providerName === 'openai') { setValidatedStatus(ValidatedStatus.Exceed) + } else { + setValidatedStatus(ValidatedStatus.Error) } }) } else { @@ -39,7 +51,8 @@ const useValidateToken = (providerName: string): [boolean, ValidatedStatus | und return [ validating, validatedStatus, - validate, + setValidatedStatus, + validate ] } diff --git a/web/app/components/header/account-setting/provider-page/provider-item/index.tsx b/web/app/components/header/account-setting/provider-page/provider-item/index.tsx index 6a3cf85846..14f8c3f5c3 100644 --- a/web/app/components/header/account-setting/provider-page/provider-item/index.tsx +++ b/web/app/components/header/account-setting/provider-page/provider-item/index.tsx @@ -5,7 +5,8 @@ import { useContext } from 'use-context-selector' import Indicator from '../../../indicator' import { useTranslation } from 'react-i18next' import type { Provider, ProviderAzureToken } from '@/models/common' -import OpenaiProvider from '../openai-provider/provider' +import { ProviderName } from '@/models/common' +import OpenaiProvider from '../openai-provider' import AzureProvider from '../azure-provider' import { ValidatedStatus } from '../provider-input/useValidateToken' import { updateProviderAIKey } from '@/service/common' @@ -38,13 +39,23 @@ const ProviderItem = ({ ) const id = `${provider.provider_name}-${provider.provider_type}` const isOpen = id === activeId - const providerKey = provider.provider_name === 'azure_openai' ? (provider.token as ProviderAzureToken)?.openai_api_key : provider.token const comingSoon = false const isValid = provider.is_valid + const providerTokenHasSetted = () => { + if (provider.provider_name === ProviderName.AZURE_OPENAI) { + return provider.token && provider.token.openai_api_base && provider.token.openai_api_key ? { + openai_api_base: provider.token.openai_api_base, + openai_api_key: provider.token.openai_api_key + }: undefined + } + if (provider.provider_name === ProviderName.OPENAI) { + return provider.token + } + } const handleUpdateToken = async () => { if (loading) return - if (validatedStatus === ValidatedStatus.Success || !token) { + if (validatedStatus === ValidatedStatus.Success) { try { setLoading(true) await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } }) @@ -65,7 +76,7 @@ const ProviderItem = ({
{name}
{ - providerKey && !comingSoon && !isOpen && ( + providerTokenHasSetted() && !comingSoon && !isOpen && (
{!isValid &&
{t('common.provider.invalidApiKey')}
} @@ -78,7 +89,7 @@ const ProviderItem = ({ px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer text-xs font-medium text-gray-700 flex items-center ' onClick={() => onActive(id)}> - {providerKey ? t('common.provider.editKey') : t('common.provider.addKey')} + {providerTokenHasSetted() ? t('common.provider.editKey') : t('common.provider.addKey')}
) } @@ -114,7 +125,7 @@ const ProviderItem = ({ }
{ - provider.provider_name === 'openai' && isOpen && ( + provider.provider_name === ProviderName.OPENAI && isOpen && ( setValidatedStatus(v)} @@ -123,7 +134,7 @@ const ProviderItem = ({ ) } { - provider.provider_name === 'azure_openai' && isOpen && ( + provider.provider_name === ProviderName.AZURE_OPENAI && isOpen && ( setValidatedStatus(v)} diff --git a/web/models/common.ts b/web/models/common.ts index adce856fd1..df19701a8d 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -54,18 +54,29 @@ export type Member = Pick Date: Tue, 23 May 2023 14:16:26 +0800 Subject: [PATCH 17/32] Feat/open azure validate (#163) --- .../console/workspace/providers.py | 2 +- api/core/llm/provider/azure_provider.py | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/api/controllers/console/workspace/providers.py b/api/controllers/console/workspace/providers.py index 87dad0d93a..f2baec29c1 100644 --- a/api/controllers/console/workspace/providers.py +++ b/api/controllers/console/workspace/providers.py @@ -157,7 +157,7 @@ class ProviderTokenValidateApi(Resource): args = parser.parse_args() # todo: remove this when the provider is supported - if provider in [ProviderName.ANTHROPIC.value, ProviderName.AZURE_OPENAI.value, ProviderName.COHERE.value, + if provider in [ProviderName.ANTHROPIC.value, ProviderName.COHERE.value, ProviderName.HUGGINGFACEHUB.value]: return {'result': 'success', 'warning': 'MOCK: This provider is not supported yet.'} diff --git a/api/core/llm/provider/azure_provider.py b/api/core/llm/provider/azure_provider.py index bd44a0cc4b..649c64cf73 100644 --- a/api/core/llm/provider/azure_provider.py +++ b/api/core/llm/provider/azure_provider.py @@ -78,7 +78,7 @@ class AzureProvider(BaseProvider): def get_token_type(self): # TODO: change to dict when implemented - return lambda value: value + return dict def config_validate(self, config: Union[dict | str]): """ @@ -91,16 +91,34 @@ class AzureProvider(BaseProvider): if 'openai_api_version' not in config: config['openai_api_version'] = '2023-03-15-preview' - self.get_models(credentials=config) + models = self.get_models(credentials=config) + + if not models: + raise ValidateFailedError("Please add deployments for 'text-davinci-003', " + "'gpt-3.5-turbo', 'text-embedding-ada-002'.") + + fixed_model_ids = [ + 'text-davinci-003', + 'gpt-35-turbo', + 'text-embedding-ada-002' + ] + + current_model_ids = [model['id'] for model in models] + + missing_model_ids = [fixed_model_id for fixed_model_id in fixed_model_ids if + fixed_model_id not in current_model_ids] + + if missing_model_ids: + raise ValidateFailedError("Please add deployments for '{}'.".format(", ".join(missing_model_ids))) except AzureAuthenticationError: - raise ValidateFailedError('Azure OpenAI Credentials validation failed, please check your API Key.') - except requests.ConnectionError: - raise ValidateFailedError('Azure OpenAI Credentials validation failed, please check your API Base Endpoint.') + raise ValidateFailedError('Validation failed, please check your API Key.') + except (requests.ConnectionError, requests.RequestException): + raise ValidateFailedError('Validation failed, please check your API Base Endpoint.') except AzureRequestFailedError as ex: - raise ValidateFailedError('Azure OpenAI Credentials validation failed, error: {}.'.format(str(ex))) + raise ValidateFailedError('Validation failed, error: {}.'.format(str(ex))) except Exception as ex: logging.exception('Azure OpenAI Credentials validation failed') - raise ex + raise ValidateFailedError('Validation failed, error: {}.'.format(str(ex))) def get_encrypted_token(self, config: Union[dict | str]): """ From b93903920137f70552ed6fd22885c1ca747604af Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 23 May 2023 15:23:07 +0800 Subject: [PATCH 18/32] feat: add product hunt (#167) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 499707d2e7..d691c47b89 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ [Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) +Vote for us on ProductHunt ↓ +Product Hunt Badge + **Dify** is an easy-to-use LLMOps platform designed to empower more people to create sustainable, AI-native applications. With visual orchestration for various application types, Dify offers out-of-the-box, ready-to-use applications that can also serve as Backend-as-a-Service APIs. Unify your development process with one API for plugins and datasets integration, and streamline your operations using a single interface for prompt engineering, visual analytics, and continuous improvement. Applications created with Dify include: From 933bd06460f07a32efa8e252f11e79f969879d85 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 23 May 2023 15:34:55 +0800 Subject: [PATCH 19/32] feat: add ph (#169) --- README.md | 4 ++-- README_CN.md | 3 +++ README_JA.md | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d691c47b89..3478d5066d 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ [Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) -Vote for us on ProductHunt ↓ -Product Hunt Badge +Vote for us on Product Hunt ↓ +Product Hunt Badge **Dify** is an easy-to-use LLMOps platform designed to empower more people to create sustainable, AI-native applications. With visual orchestration for various application types, Dify offers out-of-the-box, ready-to-use applications that can also serve as Backend-as-a-Service APIs. Unify your development process with one API for plugins and datasets integration, and streamline your operations using a single interface for prompt engineering, visual analytics, and continuous improvement. diff --git a/README_CN.md b/README_CN.md index 17cd027aee..d2c7c0015a 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,6 +8,9 @@ [官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) +在 Product Hunt 上投我们一票吧 ↓ +Product Hunt Badge + **Dify** 是一个易用的 LLMOps 平台,旨在让更多人可以创建可持续运营的原生 AI 应用。Dify 提供多种类型应用的可视化编排,应用可开箱即用,也能以“后端即服务”的 API 提供服务。 通过 Dify 创建的应用包含了: diff --git a/README_JA.md b/README_JA.md index 6b62747c9c..017c988c74 100644 --- a/README_JA.md +++ b/README_JA.md @@ -7,6 +7,10 @@ [Web サイト](https://dify.ai) • [ドキュメント](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) +Product Huntで私たちに投票してください ↓ +Product Hunt Badge + + **Dify** は、より多くの人々が持続可能な AI ネイティブアプリケーションを作成できるように設計された、使いやすい LLMOps プラットフォームです。様々なアプリケーションタイプに対応したビジュアルオーケストレーションにより Dify は Backend-as-a-Service API としても機能する、すぐに使えるアプリケーションを提供します。プラグインやデータセットを統合するための1つの API で開発プロセスを統一し、プロンプトエンジニアリング、ビジュアル分析、継続的な改善のための1つのインターフェイスを使って業務を合理化します。 Difyで作成したアプリケーションは以下の通りです: From 0e8afa3aa285f62e0b3d816cd7a85cb7bd0c2b02 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 23 May 2023 16:05:05 +0800 Subject: [PATCH 20/32] Feat/add ph (#171) --- README.md | 2 +- README_CN.md | 2 +- README_JA.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3478d5066d..9610cda8ae 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) Vote for us on Product Hunt ↓ -Product Hunt Badge +Product Hunt Badge **Dify** is an easy-to-use LLMOps platform designed to empower more people to create sustainable, AI-native applications. With visual orchestration for various application types, Dify offers out-of-the-box, ready-to-use applications that can also serve as Backend-as-a-Service APIs. Unify your development process with one API for plugins and datasets integration, and streamline your operations using a single interface for prompt engineering, visual analytics, and continuous improvement. diff --git a/README_CN.md b/README_CN.md index d2c7c0015a..e37399343e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -9,7 +9,7 @@ [官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) 在 Product Hunt 上投我们一票吧 ↓ -Product Hunt Badge +Product Hunt Badge **Dify** 是一个易用的 LLMOps 平台,旨在让更多人可以创建可持续运营的原生 AI 应用。Dify 提供多种类型应用的可视化编排,应用可开箱即用,也能以“后端即服务”的 API 提供服务。 diff --git a/README_JA.md b/README_JA.md index 017c988c74..fed796d4f1 100644 --- a/README_JA.md +++ b/README_JA.md @@ -8,7 +8,7 @@ [Web サイト](https://dify.ai) • [ドキュメント](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7) Product Huntで私たちに投票してください ↓ -Product Hunt Badge +Product Hunt Badge **Dify** は、より多くの人々が持続可能な AI ネイティブアプリケーションを作成できるように設計された、使いやすい LLMOps プラットフォームです。様々なアプリケーションタイプに対応したビジュアルオーケストレーションにより Dify は Backend-as-a-Service API としても機能する、すぐに使えるアプリケーションを提供します。プラグインやデータセットを統合するための1つの API で開発プロセスを統一し、プロンプトエンジニアリング、ビジュアル分析、継続的な改善のための1つのインターフェイスを使って業務を合理化します。 From 056898bf2144018579494d4c7e9634b0751089fb Mon Sep 17 00:00:00 2001 From: John Wang Date: Tue, 23 May 2023 16:16:22 +0800 Subject: [PATCH 21/32] fix: quota update error on azure openai (#172) --- api/core/conversation_message_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/core/conversation_message_task.py b/api/core/conversation_message_task.py index 0c2e9983ce..b23d6664bd 100644 --- a/api/core/conversation_message_task.py +++ b/api/core/conversation_message_task.py @@ -56,6 +56,9 @@ class ConversationMessageTask: ) def init(self): + provider_name = LLMBuilder.get_default_provider(self.app.tenant_id) + self.model_dict['provider'] = provider_name + override_model_configs = None if self.is_override: override_model_configs = { From fe688b505ab60db7e61e80eea3a272de8d11ae91 Mon Sep 17 00:00:00 2001 From: John Wang Date: Tue, 23 May 2023 17:34:48 +0800 Subject: [PATCH 22/32] feat: support disable version check (#173) --- api/controllers/console/version.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/controllers/console/version.py b/api/controllers/console/version.py index 0e6e75c361..285cdcba5c 100644 --- a/api/controllers/console/version.py +++ b/api/controllers/console/version.py @@ -19,6 +19,14 @@ class VersionApi(Resource): args = parser.parse_args() check_update_url = current_app.config['CHECK_UPDATE_URL'] + if not check_update_url: + return { + 'version': '0.0.0', + 'release_date': '', + 'release_notes': '', + 'can_auto_update': False + } + try: response = requests.get(check_update_url, { 'current_version': args.get('current_version') From 4350bb9a00799e20e5cd963687080bfd09405ab8 Mon Sep 17 00:00:00 2001 From: John Wang Date: Tue, 23 May 2023 19:54:04 +0800 Subject: [PATCH 23/32] Fix/human in answer (#174) --- api/core/completion.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/core/completion.py b/api/core/completion.py index 47658f3bf1..5e559ac7c7 100644 --- a/api/core/completion.py +++ b/api/core/completion.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Union +from typing import Optional, List, Union, Tuple from langchain.callbacks import CallbackManager from langchain.chat_models.base import BaseChatModel @@ -97,7 +97,7 @@ class Completion: ) # get llm prompt - prompt = cls.get_main_llm_prompt( + prompt, stop_words = cls.get_main_llm_prompt( mode=mode, llm=final_llm, pre_prompt=app_model_config.pre_prompt, @@ -115,7 +115,7 @@ class Completion: mode=mode ) - response = final_llm.generate([prompt]) + response = final_llm.generate([prompt], stop_words) return response @@ -123,7 +123,7 @@ class Completion: def get_main_llm_prompt(cls, mode: str, llm: BaseLanguageModel, pre_prompt: str, query: str, inputs: dict, chain_output: Optional[str], memory: Optional[ReadOnlyConversationTokenDBBufferSharedMemory]) -> \ - Union[str | List[BaseMessage]]: + Tuple[Union[str | List[BaseMessage]], Optional[List[str]]]: # disable template string in query query_params = OutLinePromptTemplate.from_template(template=query).input_variables if query_params: @@ -165,9 +165,9 @@ And answer according to the language of the user's question. if isinstance(llm, BaseChatModel): # use chat llm as completion model - return [HumanMessage(content=prompt_content)] + return [HumanMessage(content=prompt_content)], None else: - return prompt_content + return prompt_content, None else: messages: List[BaseMessage] = [] @@ -236,7 +236,7 @@ And answer according to the language of the user's question. messages.append(human_message) - return messages + return messages, ['\nHuman:'] @classmethod def get_llm_callback_manager(cls, llm: Union[StreamableOpenAI, StreamableChatOpenAI], @@ -323,7 +323,7 @@ And answer according to the language of the user's question. ) # get llm prompt - original_prompt = cls.get_main_llm_prompt( + original_prompt, _ = cls.get_main_llm_prompt( mode="completion", llm=llm, pre_prompt=pre_prompt, From e2bf18053cbd3f3a6fa9b665c1d7df231bf79888 Mon Sep 17 00:00:00 2001 From: Jyong <76649700+JohnJyong@users.noreply.github.com> Date: Tue, 23 May 2023 22:54:59 +0800 Subject: [PATCH 24/32] Fix/dateset update rule (#177) --- api/services/dataset_service.py | 8 ++- api/tasks/deal_dataset_vector_index_task.py | 75 +++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 api/tasks/deal_dataset_vector_index_task.py diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 39004c3437..36fe127cc5 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -18,6 +18,7 @@ from services.errors.account import NoPermissionError from services.errors.dataset import DatasetNameDuplicateError from services.errors.document import DocumentIndexingError from services.errors.file import FileNotExistsError +from tasks.deal_dataset_vector_index_task import deal_dataset_vector_index_task from tasks.document_indexing_task import document_indexing_task @@ -97,7 +98,12 @@ class DatasetService: def update_dataset(dataset_id, data, user): dataset = DatasetService.get_dataset(dataset_id) DatasetService.check_dataset_permission(dataset, user) - + if dataset.indexing_technique != data['indexing_technique']: + # if update indexing_technique + if data['indexing_technique'] == 'economy': + deal_dataset_vector_index_task.delay(dataset_id, 'remove') + elif data['indexing_technique'] == 'high_quality': + deal_dataset_vector_index_task.delay(dataset_id, 'add') filtered_data = {k: v for k, v in data.items() if v is not None or k == 'description'} filtered_data['updated_by'] = user.id diff --git a/api/tasks/deal_dataset_vector_index_task.py b/api/tasks/deal_dataset_vector_index_task.py new file mode 100644 index 0000000000..f5f9129558 --- /dev/null +++ b/api/tasks/deal_dataset_vector_index_task.py @@ -0,0 +1,75 @@ +import logging +import time + +import click +from celery import shared_task +from llama_index.data_structs.node_v2 import DocumentRelationship, Node +from core.index.vector_index import VectorIndex +from extensions.ext_database import db +from models.dataset import DocumentSegment, Document, Dataset + + +@shared_task +def deal_dataset_vector_index_task(dataset_id: str, action: str): + """ + Async deal dataset from index + :param dataset_id: dataset_id + :param action: action + Usage: deal_dataset_vector_index_task.delay(dataset_id, action) + """ + logging.info(click.style('Start deal dataset vector index: {}'.format(dataset_id), fg='green')) + start_at = time.perf_counter() + + try: + dataset = Dataset.query.filter_by( + id=dataset_id + ).first() + if not dataset: + raise Exception('Dataset not found') + documents = Document.query.filter_by(dataset_id=dataset_id).all() + if documents: + vector_index = VectorIndex(dataset=dataset) + for document in documents: + # delete from vector index + if action == "remove": + vector_index.del_doc(document.id) + elif action == "add": + segments = db.session.query(DocumentSegment).filter( + DocumentSegment.document_id == document.id, + DocumentSegment.enabled == True + ) .order_by(DocumentSegment.position.asc()).all() + + nodes = [] + previous_node = None + for segment in segments: + relationships = { + DocumentRelationship.SOURCE: document.id + } + + if previous_node: + relationships[DocumentRelationship.PREVIOUS] = previous_node.doc_id + + previous_node.relationships[DocumentRelationship.NEXT] = segment.index_node_id + + node = Node( + doc_id=segment.index_node_id, + doc_hash=segment.index_node_hash, + text=segment.content, + extra_info=None, + node_info=None, + relationships=relationships + ) + + previous_node = node + nodes.append(node) + # save vector index + vector_index.add_nodes( + nodes=nodes, + duplicate_check=True + ) + + end_at = time.perf_counter() + logging.info( + click.style('Deal dataset vector index: {} latency: {}'.format(dataset_id, end_at - start_at), fg='green')) + except Exception: + logging.exception("Deal dataset vector index failed") From 380b4b3ddc3e7065b185f7359240310671cf9011 Mon Sep 17 00:00:00 2001 From: Nite Knite Date: Tue, 23 May 2023 23:06:16 +0800 Subject: [PATCH 25/32] fix: refresh list on delete (#178) --- web/app/(commonLayout)/apps/AppCard.tsx | 6 +++++- web/app/(commonLayout)/apps/Apps.tsx | 4 +++- web/app/(commonLayout)/datasets/DatasetCard.tsx | 7 ++++--- web/app/(commonLayout)/datasets/Datasets.tsx | 4 +++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index aec4d6ab6e..f08ce3c7a9 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -16,10 +16,12 @@ import AppsContext from '@/context/app-context' export type AppCardProps = { app: App + onDelete?: () => void } const AppCard = ({ app, + onDelete }: AppCardProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) @@ -35,6 +37,8 @@ const AppCard = ({ try { await deleteApp(app.id) notify({ type: 'success', message: t('app.appDeleted') }) + if (onDelete) + onDelete() mutateApps() } catch (e: any) { @@ -47,7 +51,7 @@ const AppCard = ({ <>
- +
{app.name}
diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index aa3ac28458..11da845165 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -42,7 +42,9 @@ const Apps = () => { return ( ) diff --git a/web/app/(commonLayout)/datasets/DatasetCard.tsx b/web/app/(commonLayout)/datasets/DatasetCard.tsx index b6786d0519..a27ac5955c 100644 --- a/web/app/(commonLayout)/datasets/DatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/DatasetCard.tsx @@ -18,16 +18,16 @@ import classNames from 'classnames' export type DatasetCardProps = { dataset: DataSet + onDelete?: () => void } const DatasetCard = ({ dataset, + onDelete }: DatasetCardProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) - const { mutate: mutateDatasets } = useSWR({ url: '/datasets', params: { page: 1 } }, fetchDatasets) - const [showConfirmDelete, setShowConfirmDelete] = useState(false) const onDeleteClick: MouseEventHandler = useCallback((e) => { e.preventDefault() @@ -37,7 +37,8 @@ const DatasetCard = ({ try { await deleteDataset(dataset.id) notify({ type: 'success', message: t('dataset.datasetDeleted') }) - mutateDatasets() + if (onDelete) + onDelete() } catch (e: any) { notify({ type: 'error', message: `${t('dataset.datasetDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` }) diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx index 31e38d7fc0..649ba64000 100644 --- a/web/app/(commonLayout)/datasets/Datasets.tsx +++ b/web/app/(commonLayout)/datasets/Datasets.tsx @@ -42,7 +42,9 @@ const Datasets = () => { return ( ) From d96bcfa4ee3dd00f2f579d432bd4728e8d67f903 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 24 May 2023 14:20:21 +0800 Subject: [PATCH 26/32] fix: dataset setting (#183) --- .../[datasetId]/settings/page.tsx | 10 ++++-- .../datasets/settings/form/index.tsx | 34 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx index 23863881c3..9a2eb8e62b 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx @@ -3,7 +3,13 @@ import { getLocaleOnServer } from '@/i18n/server' import { useTranslation } from '@/i18n/i18next-serverside-config' import Form from '@/app/components/datasets/settings/form' -const Settings = async () => { +type Props = { + params: { datasetId: string } +} + +const Settings = async ({ + params: { datasetId }, +}: Props) => { const locale = getLocaleOnServer() const { t } = await useTranslation(locale, 'dataset-settings') @@ -14,7 +20,7 @@ const Settings = async () => {
{t('desc')}
-
+
) diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index 15dfadf127..c40c900bb7 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -1,5 +1,6 @@ 'use client' -import { useState } from 'react' +import { Dispatch, SetStateAction, useEffect, useState } from 'react' +import useSWR from 'swr' import { useContext } from 'use-context-selector' import { BookOpenIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' @@ -7,8 +8,8 @@ import { ToastContext } from '@/app/components/base/toast' import PermissionsRadio from '../permissions-radio' import IndexMethodRadio from '../index-method-radio' import Button from '@/app/components/base/button' -import { useDatasetsContext } from '@/context/datasets-context' -import { updateDatasetSetting } from '@/service/datasets' +import { updateDatasetSetting, fetchDataDetail } from '@/service/datasets' +import { DataSet } from '@/models/datasets' const rowClass = ` flex justify-between py-4 @@ -20,13 +21,25 @@ const inputClass = ` w-[480px] px-3 bg-gray-100 text-sm text-gray-800 rounded-lg outline-none appearance-none ` -const Form = () => { +const useInitialValue = (depend: T, dispatch: Dispatch>) => { + useEffect(() => { + dispatch(depend) + }, [depend]) +} + +type Props = { + datasetId: string +} + +const Form = ({ + datasetId +}: Props) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) - const { currentDataset, mutateDatasets } = useDatasetsContext() + const { data: currentDataset, mutate: mutateDatasets } = useSWR(datasetId, fetchDataDetail) const [loading, setLoading] = useState(false) - const [name, setName] = useState(currentDataset?.name) - const [description, setDescription] = useState(currentDataset?.description) + const [name, setName] = useState(currentDataset?.name ?? '') + const [description, setDescription] = useState(currentDataset?.description ?? '') const [permission, setPermission] = useState(currentDataset?.permission) const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique) @@ -48,7 +61,7 @@ const Form = () => { } }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - mutateDatasets() + await mutateDatasets() } catch (e) { notify({ type: 'error', message: t('common.actionMsg.modificationFailed') }) } finally { @@ -56,6 +69,11 @@ const Form = () => { } } + useInitialValue(currentDataset?.name ?? '', setName) + useInitialValue(currentDataset?.description ?? '', setDescription) + useInitialValue(currentDataset?.permission, setPermission) + useInitialValue(currentDataset?.indexing_technique, setIndexMethod) + return (
From 8b44dba988c9c75296803972747023bc4e7e310e Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 24 May 2023 16:11:25 +0800 Subject: [PATCH 27/32] fix: api key copy fail (#186) --- web/app/components/develop/secret-key/input-copy.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/app/components/develop/secret-key/input-copy.tsx b/web/app/components/develop/secret-key/input-copy.tsx index 7cb88138fa..8ad1b78da6 100644 --- a/web/app/components/develop/secret-key/input-copy.tsx +++ b/web/app/components/develop/secret-key/input-copy.tsx @@ -1,6 +1,6 @@ 'use client' import React, { useEffect, useState } from 'react' -import useCopyToClipboard from '@/hooks/use-copy-to-clipboard' +import copy from 'copy-to-clipboard' import Tooltip from '@/app/components/base/tooltip' import { t } from 'i18next' import s from './style.module.css' @@ -18,7 +18,6 @@ const InputCopy = ({ readOnly = true, children, }: IInputCopyProps) => { - const [_, copy] = useCopyToClipboard() const [isCopied, setIsCopied] = useState(false) useEffect(() => { From d93365d429e2e069c9978c384686cc86c7083c7b Mon Sep 17 00:00:00 2001 From: John Wang Date: Wed, 24 May 2023 18:55:07 +0800 Subject: [PATCH 28/32] fix: azure embedding not support batch (#188) --- api/core/embedding/openai_embedding.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/core/embedding/openai_embedding.py b/api/core/embedding/openai_embedding.py index 0f7cb252e2..d1179180f6 100644 --- a/api/core/embedding/openai_embedding.py +++ b/api/core/embedding/openai_embedding.py @@ -173,6 +173,13 @@ class OpenAIEmbedding(BaseEmbedding): Can be overriden for batch queries. """ + if self.openai_api_type and self.openai_api_type == 'azure': + embeddings = [] + for text in texts: + embeddings.append(self._get_text_embedding(text)) + + return embeddings + if self.deployment_name is not None: engine = self.deployment_name else: @@ -187,6 +194,13 @@ class OpenAIEmbedding(BaseEmbedding): async def _aget_text_embeddings(self, texts: List[str]) -> List[List[float]]: """Asynchronously get text embeddings.""" + if self.openai_api_type and self.openai_api_type == 'azure': + embeddings = [] + for text in texts: + embeddings.append(await self._aget_text_embedding(text)) + + return embeddings + if self.deployment_name is not None: engine = self.deployment_name else: From 0bb253efe061a6e359b6758830705d7c01d3a587 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 24 May 2023 19:50:14 +0800 Subject: [PATCH 29/32] fix: providererror message when token validated fail (#190) --- .../provider-page/azure-provider/index.tsx | 16 +++++------ .../provider-page/openai-provider/index.tsx | 18 ++++++------ .../provider-page/provider-input/Validate.tsx | 8 +++--- .../provider-input/useValidateToken.ts | 28 +++++++++---------- web/i18n/lang/common.en.ts | 1 + web/i18n/lang/common.zh.ts | 1 + 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx b/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx index 1cbe7d0674..a32acaa745 100644 --- a/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/azure-provider/index.tsx @@ -5,7 +5,7 @@ import Link from 'next/link' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { useState, useEffect } from 'react' import ProviderInput from '../provider-input' -import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken' +import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken' import { ValidatedErrorIcon, ValidatedSuccessIcon, @@ -15,7 +15,7 @@ import { interface IAzureProviderProps { provider: Provider - onValidatedStatus: (status?: ValidatedStatus) => void + onValidatedStatus: (status?: ValidatedStatusState) => void onTokenChange: (token: ProviderAzureToken) => void } const AzureProvider = ({ @@ -31,7 +31,7 @@ const AzureProvider = ({ token[type] = '' setToken({...token}) onTokenChange({...token}) - setValidatedStatus(undefined) + setValidatedStatus({}) } } const handleChange = (type: keyof ProviderAzureToken, v: string, validate: any) => { @@ -41,7 +41,7 @@ const AzureProvider = ({ validate({...token}, { beforeValidating: () => { if (!token.openai_api_base || !token.openai_api_key) { - setValidatedStatus(undefined) + setValidatedStatus({}) return false } return true @@ -49,10 +49,10 @@ const AzureProvider = ({ }) } const getValidatedIcon = () => { - if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) { + if (validatedStatus.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) { return } - if (validatedStatus === ValidatedStatus.Success) { + if (validatedStatus.status === ValidatedStatus.Success) { return } } @@ -60,8 +60,8 @@ const AzureProvider = ({ if (validating) { return } - if (validatedStatus === ValidatedStatus.Error) { - return + if (validatedStatus.status === ValidatedStatus.Error) { + return } } useEffect(() => { diff --git a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx index f49b229812..873ab77a44 100644 --- a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import ProviderInput from '../provider-input' import Link from 'next/link' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' -import useValidateToken, { ValidatedStatus } from '../provider-input/useValidateToken' +import useValidateToken, { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken' import { ValidatedErrorIcon, ValidatedSuccessIcon, @@ -15,7 +15,7 @@ import { interface IOpenaiProviderProps { provider: Provider - onValidatedStatus: (status?: ValidatedStatus) => void + onValidatedStatus: (status?: ValidatedStatusState) => void onTokenChange: (token: string) => void } @@ -31,7 +31,7 @@ const OpenaiProvider = ({ if (token === provider.token) { setToken('') onTokenChange('') - setValidatedStatus(undefined) + setValidatedStatus({}) } } const handleChange = (v: string) => { @@ -40,7 +40,7 @@ const OpenaiProvider = ({ validate(v, { beforeValidating: () => { if (!v) { - setValidatedStatus(undefined) + setValidatedStatus({}) return false } return true @@ -54,10 +54,10 @@ const OpenaiProvider = ({ }, [validatedStatus]) const getValidatedIcon = () => { - if (validatedStatus === ValidatedStatus.Error || validatedStatus === ValidatedStatus.Exceed) { + if (validatedStatus?.status === ValidatedStatus.Error || validatedStatus.status === ValidatedStatus.Exceed) { return } - if (validatedStatus === ValidatedStatus.Success) { + if (validatedStatus.status === ValidatedStatus.Success) { return } } @@ -65,11 +65,11 @@ const OpenaiProvider = ({ if (validating) { return } - if (validatedStatus === ValidatedStatus.Exceed) { + if (validatedStatus?.status === ValidatedStatus.Success) { return } - if (validatedStatus === ValidatedStatus.Error) { - return + if (validatedStatus?.status === ValidatedStatus.Error) { + return } } diff --git a/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx b/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx index 740a149a93..721e266a4c 100644 --- a/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx +++ b/web/app/components/header/account-setting/provider-page/provider-input/Validate.tsx @@ -38,22 +38,22 @@ export const ValidatedExceedOnOpenaiTip = () => { ) } -export const ValidatedErrorOnOpenaiTip = () => { +export const ValidatedErrorOnOpenaiTip = ({ errorMessage }: { errorMessage: string }) => { const { t } = useTranslation() return (
- {t('common.provider.invalidKey')} + {t('common.provider.validatedError')}{errorMessage}
) } -export const ValidatedErrorOnAzureOpenaiTip = () => { +export const ValidatedErrorOnAzureOpenaiTip = ({ errorMessage }: { errorMessage: string }) => { const { t } = useTranslation() return (
- {t('common.provider.invalidApiKey')} + {t('common.provider.validatedError')}{errorMessage}
) } \ No newline at end of file diff --git a/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts b/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts index 69b7529449..a53fbf1fb1 100644 --- a/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts +++ b/web/app/components/header/account-setting/provider-page/provider-input/useValidateToken.ts @@ -8,11 +8,16 @@ export enum ValidatedStatus { Error = 'error', Exceed = 'exceed' } -export type SetValidatedStatus = Dispatch> +export type ValidatedStatusState = { + status?: ValidatedStatus, + message?: string +} +// export type ValidatedStatusState = ValidatedStatus | undefined | ValidatedError +export type SetValidatedStatus = Dispatch> export type ValidateFn = DebouncedFunc<(token: any, config: ValidateFnConfig) => void> type ValidateTokenReturn = [ boolean, - ValidatedStatus | undefined, + ValidatedStatusState, SetValidatedStatus, ValidateFn ] @@ -22,7 +27,7 @@ export type ValidateFnConfig = { const useValidateToken = (providerName: string): ValidateTokenReturn => { const [validating, setValidating] = useState(false) - const [validatedStatus, setValidatedStatus] = useState() + const [validatedStatus, setValidatedStatus] = useState({}) const validate = useCallback(debounce(async (token: string, config: ValidateFnConfig) => { if (!config.beforeValidating(token)) { return false @@ -30,19 +35,12 @@ const useValidateToken = (providerName: string): ValidateTokenReturn => { setValidating(true) try { const res = await validateProviderKey({ url: `/workspaces/current/providers/${providerName}/token-validate`, body: { token } }) - setValidatedStatus(res.result === 'success' ? ValidatedStatus.Success : ValidatedStatus.Error) + setValidatedStatus( + res.result === 'success' + ? { status: ValidatedStatus.Success } + : { status: ValidatedStatus.Error, message: res.error }) } catch (e: any) { - if (e.status === 400) { - e.json().then(({ code }: any) => { - if (code === 'provider_request_failed' && providerName === 'openai') { - setValidatedStatus(ValidatedStatus.Exceed) - } else { - setValidatedStatus(ValidatedStatus.Error) - } - }) - } else { - setValidatedStatus(ValidatedStatus.Error) - } + setValidatedStatus({ status: ValidatedStatus.Error, message: e.message }) } finally { setValidating(false) } diff --git a/web/i18n/lang/common.en.ts b/web/i18n/lang/common.en.ts index fa73fbd8a8..bc4729d410 100644 --- a/web/i18n/lang/common.en.ts +++ b/web/i18n/lang/common.en.ts @@ -140,6 +140,7 @@ const translation = { apiKey: "API Key", enterYourKey: "Enter your API key here", invalidKey: "Invalid OpenAI API key", + validatedError: "Validation failed: ", validating: "Validating key...", saveFailed: "Save api key failed", apiKeyExceedBill: "This API KEY has no quota available, please read", diff --git a/web/i18n/lang/common.zh.ts b/web/i18n/lang/common.zh.ts index 496d27ad48..4205a43606 100644 --- a/web/i18n/lang/common.zh.ts +++ b/web/i18n/lang/common.zh.ts @@ -141,6 +141,7 @@ const translation = { apiKey: "API 密钥", enterYourKey: "输入你的 API 密钥", invalidKey: '无效的 OpenAI API 密钥', + validatedError: "校验失败:", validating: "验证密钥中...", saveFailed: "API 密钥保存失败", apiKeyExceedBill: "此 API KEY 已没有可用配额,请阅读", From 7a16c880929a18c0b6401335f5726112d4c0220c Mon Sep 17 00:00:00 2001 From: Bole Chen Date: Wed, 24 May 2023 21:05:05 +0800 Subject: [PATCH 30/32] fix: php sdk error code (#179) --- sdks/php-client/dify-client.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdks/php-client/dify-client.php b/sdks/php-client/dify-client.php index 7a5d9b60cf..cc2e854775 100644 --- a/sdks/php-client/dify-client.php +++ b/sdks/php-client/dify-client.php @@ -11,7 +11,7 @@ class DifyClient { public function __construct($api_key) { $this->api_key = $api_key; - $this->base_url = "https://api.dify.ai/v1"; + $this->base_url = "https://api.dify.ai/v1/"; $this->client = new Client([ 'base_uri' => $this->base_url, 'headers' => [ @@ -37,12 +37,12 @@ class DifyClient { 'rating' => $rating, 'user' => $user, ]; - return $this->send_request('POST', "/messages/{$message_id}/feedbacks", $data); + return $this->send_request('POST', "messages/{$message_id}/feedbacks", $data); } public function get_application_parameters($user) { $params = ['user' => $user]; - return $this->send_request('GET', '/parameters', null, $params); + return $this->send_request('GET', 'parameters', null, $params); } } @@ -54,7 +54,7 @@ class CompletionClient extends DifyClient { 'response_mode' => $response_mode, 'user' => $user, ]; - return $this->send_request('POST', '/completion-messages', $data, null, $response_mode === 'streaming'); + return $this->send_request('POST', 'completion-messages', $data, null, $response_mode === 'streaming'); } } @@ -70,7 +70,7 @@ class ChatClient extends DifyClient { $data['conversation_id'] = $conversation_id; } - return $this->send_request('POST', '/chat-messages', $data, null, $response_mode === 'streaming'); + return $this->send_request('POST', 'chat-messages', $data, null, $response_mode === 'streaming'); } public function get_conversation_messages($user, $conversation_id = null, $first_id = null, $limit = null) { @@ -86,7 +86,7 @@ class ChatClient extends DifyClient { $params['limit'] = $limit; } - return $this->send_request('GET', '/messages', null, $params); + return $this->send_request('GET', 'messages', null, $params); } public function get_conversations($user, $first_id = null, $limit = null, $pinned = null) { @@ -96,7 +96,7 @@ class ChatClient extends DifyClient { 'limit' => $limit, 'pinned'=> $pinned, ]; - return $this->send_request('GET', '/conversations', null, $params); + return $this->send_request('GET', 'conversations', null, $params); } public function rename_conversation($conversation_id, $name, $user) { @@ -104,6 +104,6 @@ class ChatClient extends DifyClient { 'name' => $name, 'user' => $user, ]; - return $this->send_request('PATCH', "/conversations/{$conversation_id}", $data); + return $this->send_request('PATCH', "conversations/{$conversation_id}", $data); } } From 659c3e7a81e94c2cd016bf72213c16c737ab6239 Mon Sep 17 00:00:00 2001 From: Yuhao Date: Thu, 25 May 2023 13:01:09 +0800 Subject: [PATCH 31/32] fix: nav ui bug (#191) Co-authored-by: yuhao1118 --- web/app/components/header/nav/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/header/nav/index.tsx b/web/app/components/header/nav/index.tsx index 9c2d2dcc7b..6fc05a7627 100644 --- a/web/app/components/header/nav/index.tsx +++ b/web/app/components/header/nav/index.tsx @@ -37,7 +37,7 @@ const Nav = ({
Date: Thu, 25 May 2023 13:27:27 +0800 Subject: [PATCH 32/32] fix: provider token validate (#195) --- .../account-setting/provider-page/openai-provider/index.tsx | 3 --- .../account-setting/provider-page/provider-item/index.tsx | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx index 873ab77a44..d33042fcfd 100644 --- a/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx +++ b/web/app/components/header/account-setting/provider-page/openai-provider/index.tsx @@ -65,9 +65,6 @@ const OpenaiProvider = ({ if (validating) { return } - if (validatedStatus?.status === ValidatedStatus.Success) { - return - } if (validatedStatus?.status === ValidatedStatus.Error) { return } diff --git a/web/app/components/header/account-setting/provider-page/provider-item/index.tsx b/web/app/components/header/account-setting/provider-page/provider-item/index.tsx index 14f8c3f5c3..cd1ad038da 100644 --- a/web/app/components/header/account-setting/provider-page/provider-item/index.tsx +++ b/web/app/components/header/account-setting/provider-page/provider-item/index.tsx @@ -8,7 +8,7 @@ import type { Provider, ProviderAzureToken } from '@/models/common' import { ProviderName } from '@/models/common' import OpenaiProvider from '../openai-provider' import AzureProvider from '../azure-provider' -import { ValidatedStatus } from '../provider-input/useValidateToken' +import { ValidatedStatus, ValidatedStatusState } from '../provider-input/useValidateToken' import { updateProviderAIKey } from '@/service/common' import { ToastContext } from '@/app/components/base/toast' @@ -29,7 +29,7 @@ const ProviderItem = ({ onSave }: IProviderItemProps) => { const { t } = useTranslation() - const [validatedStatus, setValidatedStatus] = useState() + const [validatedStatus, setValidatedStatus] = useState() const [loading, setLoading] = useState(false) const { notify } = useContext(ToastContext) const [token, setToken] = useState( @@ -55,7 +55,7 @@ const ProviderItem = ({ } const handleUpdateToken = async () => { if (loading) return - if (validatedStatus === ValidatedStatus.Success) { + if (validatedStatus?.status === ValidatedStatus.Success) { try { setLoading(true) await updateProviderAIKey({ url: `/workspaces/current/providers/${provider.provider_name}/token`, body: { token } })