From 03548cdfbc0ed919c7db3d1626498727f022adc8 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 15:23:11 +0800 Subject: [PATCH 01/19] fix: handle broader request exceptions in OAuth process (#11997) Signed-off-by: -LAN- --- api/controllers/console/auth/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py index 5de8c6766d..b9188aa079 100644 --- a/api/controllers/console/auth/oauth.py +++ b/api/controllers/console/auth/oauth.py @@ -76,7 +76,7 @@ class OAuthCallback(Resource): try: token = oauth_provider.get_access_token(code) user_info = oauth_provider.get_user_info(token) - except requests.exceptions.HTTPError as e: + except requests.exceptions.RequestException as e: logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}") return {"error": "OAuth process failed"}, 400 From 4e3d732934294ab2528b09855239375cc118b0cc Mon Sep 17 00:00:00 2001 From: Shun Miyazawa <34241526+miya@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:27:49 +0900 Subject: [PATCH 02/19] feat: Warning on invite modal when mail setup is incomplete (#11809) --- .../account-setting/members-page/index.tsx | 3 ++- .../members-page/invite-modal/index.tsx | 21 ++++++++++++++++++- web/i18n/de-DE/common.ts | 1 + web/i18n/en-US/common.ts | 1 + web/i18n/es-ES/common.ts | 1 + web/i18n/fa-IR/common.ts | 1 + web/i18n/fr-FR/common.ts | 1 + web/i18n/hi-IN/common.ts | 1 + web/i18n/it-IT/common.ts | 1 + web/i18n/ja-JP/common.ts | 1 + web/i18n/ko-KR/common.ts | 1 + web/i18n/pl-PL/common.ts | 1 + web/i18n/pt-BR/common.ts | 1 + web/i18n/ro-RO/common.ts | 1 + web/i18n/ru-RU/common.ts | 1 + web/i18n/sl-SI/common.ts | 1 + web/i18n/th-TH/common.ts | 1 + web/i18n/tr-TR/common.ts | 1 + web/i18n/uk-UA/common.ts | 1 + web/i18n/vi-VN/common.ts | 1 + web/i18n/zh-Hans/common.ts | 1 + web/i18n/zh-Hant/common.ts | 1 + 22 files changed, 42 insertions(+), 2 deletions(-) diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index c2b722b4a7..808da454d1 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -34,7 +34,7 @@ const MembersPage = () => { } const { locale } = useContext(I18n) - const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext() + const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext() const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) const [inviteModalVisible, setInviteModalVisible] = useState(false) const [invitationResults, setInvitationResults] = useState([]) @@ -122,6 +122,7 @@ const MembersPage = () => { { inviteModalVisible && ( setInviteModalVisible(false)} onSend={(invitationResults) => { setInvitedModalVisible(true) diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 7d43495362..197e3ee867 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -4,6 +4,7 @@ import { useContext } from 'use-context-selector' import { XMarkIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' import { ReactMultiEmail } from 'react-multi-email' +import { RiErrorWarningFill } from '@remixicon/react' import RoleSelector from './role-selector' import s from './index.module.css' import cn from '@/utils/classnames' @@ -17,11 +18,13 @@ import I18n from '@/context/i18n' import 'react-multi-email/dist/style.css' type IInviteModalProps = { + isEmailSetup: boolean onCancel: () => void onSend: (invitationResults: InvitationResult[]) => void } const InviteModal = ({ + isEmailSetup, onCancel, onSend, }: IInviteModalProps) => { @@ -59,7 +62,23 @@ const InviteModal = ({
{t('common.members.inviteTeamMember')}
-
{t('common.members.inviteTeamMemberTip')}
+
{t('common.members.inviteTeamMemberTip')}
+ {!isEmailSetup && ( +
+
+
+
+
+ +
+
+ {t('common.members.emailNotSetup')} +
+
+
+
+ )} +
{t('common.members.email')}
diff --git a/web/i18n/de-DE/common.ts b/web/i18n/de-DE/common.ts index 1d7ca955fa..f438b4f018 100644 --- a/web/i18n/de-DE/common.ts +++ b/web/i18n/de-DE/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: 'Kann Apps erstellen & bearbeiten', inviteTeamMember: 'Teammitglied hinzufügen', inviteTeamMemberTip: 'Sie können direkt nach der Anmeldung auf Ihre Teamdaten zugreifen.', + emailNotSetup: 'E-Mail-Server ist nicht eingerichtet, daher können keine Einladungs-E-Mails versendet werden. Bitte informieren Sie die Benutzer über den Einladungslink, der nach der Einladung ausgestellt wird.', email: 'E-Mail', emailInvalid: 'Ungültiges E-Mail-Format', emailPlaceholder: 'Bitte E-Mails eingeben', diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index ce341a9148..ea0e4a88aa 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'Only can manage the knowledge base', inviteTeamMember: 'Add team member', inviteTeamMemberTip: 'They can access your team data directly after signing in.', + emailNotSetup: 'Email server is not set up, so invitation emails cannot be sent. Please notify users of the invitation link that will be issued after invitation instead.', email: 'Email', emailInvalid: 'Invalid Email Format', emailPlaceholder: 'Please input emails', diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index cc9fb47329..2540632758 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'Solo puede administrar la base de conocimiento', inviteTeamMember: 'Agregar miembro del equipo', inviteTeamMemberTip: 'Pueden acceder a tus datos del equipo directamente después de iniciar sesión.', + emailNotSetup: 'El servidor de correo no está configurado, por lo que no se pueden enviar correos de invitación. En su lugar, notifique a los usuarios el enlace de invitación que se emitirá después de la invitación.', email: 'Correo electrónico', emailInvalid: 'Formato de correo electrónico inválido', emailPlaceholder: 'Por favor ingresa correos electrónicos', diff --git a/web/i18n/fa-IR/common.ts b/web/i18n/fa-IR/common.ts index 2da6cdee8b..deab852ddb 100644 --- a/web/i18n/fa-IR/common.ts +++ b/web/i18n/fa-IR/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'فقط می‌تواند پایگاه دانش را مدیریت کند', inviteTeamMember: 'افزودن عضو تیم', inviteTeamMemberTip: 'آنها می‌توانند پس از ورود به سیستم، مستقیماً به داده‌های تیم شما دسترسی پیدا کنند.', + emailNotSetup: 'سرور ایمیل راه‌اندازی نشده است، بنابراین ایمیل‌های دعوت نمی‌توانند ارسال شوند. لطفاً کاربران را از لینک دعوت که پس از دعوت صادر خواهد شد مطلع کنید。', email: 'ایمیل', emailInvalid: 'فرمت ایمیل نامعتبر است', emailPlaceholder: 'لطفاً ایمیل‌ها را وارد کنید', diff --git a/web/i18n/fr-FR/common.ts b/web/i18n/fr-FR/common.ts index 326572916c..25142c11cc 100644 --- a/web/i18n/fr-FR/common.ts +++ b/web/i18n/fr-FR/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: 'Peut construire des applications, mais ne peut pas gérer les paramètres de l\'équipe', inviteTeamMember: 'Ajouter un membre de l\'équipe', inviteTeamMemberTip: 'Ils peuvent accéder directement à vos données d\'équipe après s\'être connectés.', + emailNotSetup: 'Le serveur de messagerie n\'est pas configuré, les e-mails d\'invitation ne peuvent donc pas être envoyés. Veuillez informer les utilisateurs du lien d\'invitation qui sera émis après l\'invitation.', email: 'Courrier électronique', emailInvalid: 'Format de courriel invalide', emailPlaceholder: 'Veuillez entrer des emails', diff --git a/web/i18n/hi-IN/common.ts b/web/i18n/hi-IN/common.ts index a2c178cb18..aabcfc86e6 100644 --- a/web/i18n/hi-IN/common.ts +++ b/web/i18n/hi-IN/common.ts @@ -204,6 +204,7 @@ const translation = { inviteTeamMember: 'टीम सदस्य जोड़ें', inviteTeamMemberTip: 'वे साइन इन करने के बाद सीधे आपकी टीम डेटा तक पहुंच सकते हैं।', + emailNotSetup: 'ईमेल सर्वर सेट नहीं है, इसलिए आमंत्रण ईमेल नहीं भेजे जा सकते। कृपया उपयोगकर्ताओं को आमंत्रण के बाद जारी किए जाने वाले आमंत्रण लिंक के बारे में सूचित करें。', email: 'ईमेल', emailInvalid: 'अवैध ईमेल प्रारूप', emailPlaceholder: 'कृपया ईमेल दर्ज करें', diff --git a/web/i18n/it-IT/common.ts b/web/i18n/it-IT/common.ts index 35a01d7114..4cee6dec50 100644 --- a/web/i18n/it-IT/common.ts +++ b/web/i18n/it-IT/common.ts @@ -208,6 +208,7 @@ const translation = { inviteTeamMember: 'Aggiungi membro del team', inviteTeamMemberTip: 'Potranno accedere ai dati del tuo team direttamente dopo aver effettuato l\'accesso.', + emailNotSetup: 'Il server email non è configurato, quindi non è possibile inviare email di invito. Si prega di notificare agli utenti il link di invito che verrà emesso dopo l\'invito.', email: 'Email', emailInvalid: 'Formato Email non valido', emailPlaceholder: 'Per favore inserisci le email', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index fa3fb223f4..9c23cb6f16 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'ナレッジベースのみを管理できる', inviteTeamMember: 'チームメンバーを招待する', inviteTeamMemberTip: '彼らはサインイン後、直接あなた様のチームデータにアクセスできます。', + emailNotSetup: 'メールサーバーがセットアップされていないので、招待メールを送信することはできません。代わりに招待後に発行される招待リンクをユーザーに通知してください。', email: 'メール', emailInvalid: '無効なメール形式', emailPlaceholder: 'メールを入力してください', diff --git a/web/i18n/ko-KR/common.ts b/web/i18n/ko-KR/common.ts index ce860e000e..a599aa9bd1 100644 --- a/web/i18n/ko-KR/common.ts +++ b/web/i18n/ko-KR/common.ts @@ -187,6 +187,7 @@ const translation = { editorTip: '앱 빌드만 가능하고 팀 설정 관리 불가능', inviteTeamMember: '팀 멤버 초대', inviteTeamMemberTip: '로그인 후에 바로 팀 데이터에 액세스할 수 있습니다.', + emailNotSetup: '이메일 서버가 설정되지 않아 초대 이메일을 보낼 수 없습니다. 대신 초대 후 발급되는 초대 링크를 사용자에게 알려주세요.', email: '이메일', emailInvalid: '유효하지 않은 이메일 형식', emailPlaceholder: '이메일 입력', diff --git a/web/i18n/pl-PL/common.ts b/web/i18n/pl-PL/common.ts index baaf5292c3..69441dbab3 100644 --- a/web/i18n/pl-PL/common.ts +++ b/web/i18n/pl-PL/common.ts @@ -198,6 +198,7 @@ const translation = { inviteTeamMember: 'Dodaj członka zespołu', inviteTeamMemberTip: 'Mogą uzyskać bezpośredni dostęp do danych Twojego zespołu po zalogowaniu.', + emailNotSetup: 'Serwer poczty nie jest skonfigurowany, więc nie można wysyłać zaproszeń e-mail. Proszę powiadomić użytkowników o linku do zaproszenia, który zostanie wydany po zaproszeniu.', email: 'Email', emailInvalid: 'Nieprawidłowy format e-maila', emailPlaceholder: 'Proszę podać adresy e-mail', diff --git a/web/i18n/pt-BR/common.ts b/web/i18n/pt-BR/common.ts index a2c74a7bee..6f66e65878 100644 --- a/web/i18n/pt-BR/common.ts +++ b/web/i18n/pt-BR/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: 'Pode editar aplicativos, mas não pode gerenciar configurações da equipe', inviteTeamMember: 'Adicionar membro da equipe', inviteTeamMemberTip: 'Eles podem acessar os dados da sua equipe diretamente após fazer login.', + emailNotSetup: 'O servidor de e-mail não está configurado, então os e-mails de convite não podem ser enviados. Por favor, notifique os usuários sobre o link de convite que será emitido após o convite.', email: 'E-mail', emailInvalid: 'Formato de e-mail inválido', emailPlaceholder: 'Por favor, insira e-mails', diff --git a/web/i18n/ro-RO/common.ts b/web/i18n/ro-RO/common.ts index 27a0ab6bf3..0badaf5a13 100644 --- a/web/i18n/ro-RO/common.ts +++ b/web/i18n/ro-RO/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: 'Poate construi aplicații, dar nu poate gestiona setările echipei', inviteTeamMember: 'Adaugă membru în echipă', inviteTeamMemberTip: 'Pot accesa direct datele echipei dvs. după autentificare.', + emailNotSetup: 'Serverul de e-mail nu este configurat, astfel încât e-mailurile de invitație nu pot fi trimise. Vă rugăm să notificați utilizatorii despre linkul de invitație care va fi emis după invitație.', email: 'Email', emailInvalid: 'Format de email invalid', emailPlaceholder: 'Vă rugăm să introduceți emailuri', diff --git a/web/i18n/ru-RU/common.ts b/web/i18n/ru-RU/common.ts index 6d9edf97c1..64a7c9375d 100644 --- a/web/i18n/ru-RU/common.ts +++ b/web/i18n/ru-RU/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'Может управлять только базой знаний', inviteTeamMember: 'Добавить участника команды', inviteTeamMemberTip: 'Они могут получить доступ к данным вашей команды сразу после входа в систему.', + emailNotSetup: 'Почтовый сервер не настроен, поэтому приглашения по электронной почте не могут быть отправлены. Пожалуйста, уведомите пользователей о ссылке для приглашения, которая будет выдана после приглашения.', email: 'Электронная почта', emailInvalid: 'Неверный формат электронной почты', emailPlaceholder: 'Пожалуйста, введите адреса электронной почты', diff --git a/web/i18n/sl-SI/common.ts b/web/i18n/sl-SI/common.ts index dc399bd3a4..0c5d1dfc4b 100644 --- a/web/i18n/sl-SI/common.ts +++ b/web/i18n/sl-SI/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'Lahko upravlja samo bazo znanja', inviteTeamMember: 'Dodaj člana ekipe', inviteTeamMemberTip: 'Do vaših podatkov bo lahko dostopal takoj po prijavi.', + emailNotSetup: 'E-poštni strežnik ni nastavljen, zato vabil po e-pošti ni mogoče poslati. Prosimo, obvestite uporabnike o povezavi za povabilo, ki bo izdana po povabilu.', email: 'E-pošta', emailInvalid: 'Neveljaven format e-pošte', emailPlaceholder: 'Vnesite e-poštne naslove', diff --git a/web/i18n/th-TH/common.ts b/web/i18n/th-TH/common.ts index 82eddae723..6aa1b30610 100644 --- a/web/i18n/th-TH/common.ts +++ b/web/i18n/th-TH/common.ts @@ -194,6 +194,7 @@ const translation = { datasetOperatorTip: 'สามารถจัดการฐานความรู้ได้เท่านั้น', inviteTeamMember: 'เพิ่มสมาชิกในทีม', inviteTeamMemberTip: 'พวกเขาสามารถเข้าถึงข้อมูลทีมของคุณได้โดยตรงหลังจากลงชื่อเข้าใช้', + emailNotSetup: 'เซิร์ฟเวอร์อีเมลไม่ได้ตั้งค่าไว้ จึงไม่สามารถส่งอีเมลเชิญได้ กรุณาแจ้งผู้ใช้เกี่ยวกับลิงก์เชิญที่จะออกหลังจากการเชิญแทน', email: 'อีเมล', emailInvalid: 'รูปแบบอีเมลไม่ถูกต้อง', emailPlaceholder: 'กรุณากรอกอีเมล', diff --git a/web/i18n/tr-TR/common.ts b/web/i18n/tr-TR/common.ts index 320517925a..9792f07e18 100644 --- a/web/i18n/tr-TR/common.ts +++ b/web/i18n/tr-TR/common.ts @@ -199,6 +199,7 @@ const translation = { datasetOperatorTip: 'Sadece bilgi tabanını yönetebilir', inviteTeamMember: 'Takım Üyesi Ekle', inviteTeamMemberTip: 'Giriş yaptıktan sonra takım verilerinize doğrudan erişebilirler.', + emailNotSetup: 'E-posta sunucusu kurulu değil, bu nedenle davet e-postaları gönderilemiyor. Lütfen kullanıcıları davetten sonra verilecek davet bağlantısı hakkında bilgilendirin.', email: 'E-posta', emailInvalid: 'Geçersiz E-posta Formatı', emailPlaceholder: 'Lütfen e-postaları girin', diff --git a/web/i18n/uk-UA/common.ts b/web/i18n/uk-UA/common.ts index 6bf6dafc4f..fbe9b67750 100644 --- a/web/i18n/uk-UA/common.ts +++ b/web/i18n/uk-UA/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: 'Може створювати програми, але не може керувати налаштуваннями команди', inviteTeamMember: 'Додати учасника команди', inviteTeamMemberTip: 'Вони зможуть отримати доступ до даних вашої команди безпосередньо після входу.', + emailNotSetup: 'Поштовий сервер не налаштований, тому запрошення електронною поштою не можуть бути надіслані. Будь ласка, повідомте користувачів про посилання для запрошення, яке буде видано після запрошення.', email: 'Електронна пошта', emailInvalid: 'Недійсний формат електронної пошти', emailPlaceholder: 'Будь ласка, введіть адресу електронної пошти', diff --git a/web/i18n/vi-VN/common.ts b/web/i18n/vi-VN/common.ts index bf5339f40e..8bafd86854 100644 --- a/web/i18n/vi-VN/common.ts +++ b/web/i18n/vi-VN/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: 'Có thể xây dựng ứng dụng, không thể quản lý cài đặt nhóm', inviteTeamMember: 'Mời thành viên nhóm', inviteTeamMemberTip: 'Sau khi đăng nhập, họ có thể truy cập trực tiếp vào dữ liệu nhóm của bạn.', + emailNotSetup: 'Máy chủ email chưa được thiết lập, vì vậy không thể gửi email mời. Vui lòng thông báo cho người dùng về liên kết mời sẽ được phát hành sau khi mời.', email: 'Email', emailInvalid: 'Định dạng Email không hợp lệ', emailPlaceholder: 'Vui lòng nhập email', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 70caaa976a..96e08a9337 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -197,6 +197,7 @@ const translation = { datasetOperatorTip: '只能管理知识库', inviteTeamMember: '添加团队成员', inviteTeamMemberTip: '对方在登录后可以访问你的团队数据。', + emailNotSetup: '由于邮件服务器未设置,无法发送邀请邮件。请将邀请后生成的邀请链接通知用户。', email: '邮箱', emailInvalid: '邮箱格式无效', emailPlaceholder: '输入邮箱', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 09c1e9d839..8340650993 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -191,6 +191,7 @@ const translation = { editorTip: '能夠建立並編輯應用程式,不能管理團隊設定', inviteTeamMember: '新增團隊成員', inviteTeamMemberTip: '對方在登入後可以訪問你的團隊資料。', + emailNotSetup: '由於郵件伺服器未設置,無法發送邀請郵件。請將邀請後生成的邀請連結通知用戶。', email: '郵箱', emailInvalid: '郵箱格式無效', emailPlaceholder: '輸入郵箱', From 8978a6a3ffba04db8aba8f5c8fd08c1c54f9a37c Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 15:30:58 +0800 Subject: [PATCH 03/19] fix: remove unused credential validation logic in VectorizerProvider (#12000) Signed-off-by: -LAN- --- .../provider/builtin/vectorizer/vectorizer.py | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/api/core/tools/provider/builtin/vectorizer/vectorizer.py b/api/core/tools/provider/builtin/vectorizer/vectorizer.py index 211ec78f4d..9d7613f8ea 100644 --- a/api/core/tools/provider/builtin/vectorizer/vectorizer.py +++ b/api/core/tools/provider/builtin/vectorizer/vectorizer.py @@ -1,32 +1,8 @@ from typing import Any -from core.file import FileTransferMethod, FileType -from core.tools.errors import ToolProviderCredentialValidationError -from core.tools.provider.builtin.vectorizer.tools.vectorizer import VectorizerTool from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController -from factories import file_factory class VectorizerProvider(BuiltinToolProviderController): def _validate_credentials(self, credentials: dict[str, Any]) -> None: - mapping = { - "transfer_method": FileTransferMethod.TOOL_FILE, - "type": FileType.IMAGE, - "id": "test_id", - "url": "https://cloud.dify.ai/logo/logo-site.png", - } - test_img = file_factory.build_from_mapping( - mapping=mapping, - tenant_id="__test_123", - ) - try: - VectorizerTool().fork_tool_runtime( - runtime={ - "credentials": credentials, - } - ).invoke( - user_id="", - tool_parameters={"mode": "test", "image": test_img}, - ) - except Exception as e: - raise ToolProviderCredentialValidationError(str(e)) + return From 9cfd1c67b6c16ec199b1be740a91a47fd57791c6 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 15:52:50 +0800 Subject: [PATCH 04/19] fix: Introduce ArrayVariable and update iteration node to handle it (#12001) Signed-off-by: -LAN- --- api/core/variables/__init__.py | 2 ++ api/core/variables/variables.py | 13 +++++++++---- .../workflow/nodes/iteration/iteration_node.py | 15 +++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/api/core/variables/__init__.py b/api/core/variables/__init__.py index 2b1a58f93a..7a1cbf9940 100644 --- a/api/core/variables/__init__.py +++ b/api/core/variables/__init__.py @@ -21,6 +21,7 @@ from .variables import ( ArrayNumberVariable, ArrayObjectVariable, ArrayStringVariable, + ArrayVariable, FileVariable, FloatVariable, IntegerVariable, @@ -43,6 +44,7 @@ __all__ = [ "ArraySegment", "ArrayStringSegment", "ArrayStringVariable", + "ArrayVariable", "FileSegment", "FileVariable", "FloatSegment", diff --git a/api/core/variables/variables.py b/api/core/variables/variables.py index c902303eef..f9268b52e6 100644 --- a/api/core/variables/variables.py +++ b/api/core/variables/variables.py @@ -10,6 +10,7 @@ from .segments import ( ArrayFileSegment, ArrayNumberSegment, ArrayObjectSegment, + ArraySegment, ArrayStringSegment, FileSegment, FloatSegment, @@ -52,19 +53,23 @@ class ObjectVariable(ObjectSegment, Variable): pass -class ArrayAnyVariable(ArrayAnySegment, Variable): +class ArrayVariable(ArraySegment, Variable): pass -class ArrayStringVariable(ArrayStringSegment, Variable): +class ArrayAnyVariable(ArrayAnySegment, ArrayVariable): pass -class ArrayNumberVariable(ArrayNumberSegment, Variable): +class ArrayStringVariable(ArrayStringSegment, ArrayVariable): pass -class ArrayObjectVariable(ArrayObjectSegment, Variable): +class ArrayNumberVariable(ArrayNumberSegment, ArrayVariable): + pass + + +class ArrayObjectVariable(ArrayObjectSegment, ArrayVariable): pass diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index d935228c16..6a89cbfad6 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast from flask import Flask, current_app from configs import dify_config -from core.variables import IntegerVariable +from core.variables import ArrayVariable, IntegerVariable, NoneVariable from core.workflow.entities.node_entities import ( NodeRunMetadataKey, NodeRunResult, @@ -75,12 +75,15 @@ class IterationNode(BaseNode[IterationNodeData]): """ Run the node. """ - iterator_list_segment = self.graph_runtime_state.variable_pool.get(self.node_data.iterator_selector) + variable = self.graph_runtime_state.variable_pool.get(self.node_data.iterator_selector) - if not iterator_list_segment: - raise IteratorVariableNotFoundError(f"Iterator variable {self.node_data.iterator_selector} not found") + if not variable: + raise IteratorVariableNotFoundError(f"iterator variable {self.node_data.iterator_selector} not found") - if len(iterator_list_segment.value) == 0: + if not isinstance(variable, ArrayVariable) and not isinstance(variable, NoneVariable): + raise InvalidIteratorValueError(f"invalid iterator value: {variable}, please provide a list.") + + if isinstance(variable, NoneVariable) or len(variable.value) == 0: yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, @@ -89,7 +92,7 @@ class IterationNode(BaseNode[IterationNodeData]): ) return - iterator_list_value = iterator_list_segment.to_object() + iterator_list_value = variable.to_object() if not isinstance(iterator_list_value, list): raise InvalidIteratorValueError(f"Invalid iterator value: {iterator_list_value}, please provide a list.") From e068bbec737ed649ba2294fb51b547bec6ea1c22 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 15:53:03 +0800 Subject: [PATCH 05/19] feat: add RequestBodyError for invalid request body handling (#11994) Signed-off-by: -LAN- --- api/core/workflow/nodes/http_request/exc.py | 4 ++++ api/core/workflow/nodes/http_request/executor.py | 9 +++++++++ api/core/workflow/nodes/http_request/node.py | 6 +++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/http_request/exc.py b/api/core/workflow/nodes/http_request/exc.py index 7a5ab7dbc1..a815f277be 100644 --- a/api/core/workflow/nodes/http_request/exc.py +++ b/api/core/workflow/nodes/http_request/exc.py @@ -16,3 +16,7 @@ class InvalidHttpMethodError(HttpRequestNodeError): class ResponseSizeError(HttpRequestNodeError): """Raised when the response size exceeds the allowed threshold.""" + + +class RequestBodyError(HttpRequestNodeError): + """Raised when the request body is invalid.""" diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index 3b7e193319..575db15d36 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -23,6 +23,7 @@ from .exc import ( FileFetchError, HttpRequestNodeError, InvalidHttpMethodError, + RequestBodyError, ResponseSizeError, ) @@ -143,13 +144,19 @@ class Executor: case "none": self.content = "" case "raw-text": + if len(data) != 1: + raise RequestBodyError("raw-text body type should have exactly one item") self.content = self.variable_pool.convert_template(data[0].value).text case "json": + if len(data) != 1: + raise RequestBodyError("json body type should have exactly one item") json_string = self.variable_pool.convert_template(data[0].value).text json_object = json.loads(json_string, strict=False) self.json = json_object # self.json = self._parse_object_contains_variables(json_object) case "binary": + if len(data) != 1: + raise RequestBodyError("binary body type should have exactly one item") file_selector = data[0].file file_variable = self.variable_pool.get_file(file_selector) if file_variable is None: @@ -317,6 +324,8 @@ class Executor: elif self.json: body = json.dumps(self.json) elif self.node_data.body.type == "raw-text": + if len(self.node_data.body.data) != 1: + raise RequestBodyError("raw-text body type should have exactly one item") body = self.node_data.body.data[0].value if body: raw += f"Content-Length: {len(body)}\r\n" diff --git a/api/core/workflow/nodes/http_request/node.py b/api/core/workflow/nodes/http_request/node.py index 171389a34c..ebed690f6f 100644 --- a/api/core/workflow/nodes/http_request/node.py +++ b/api/core/workflow/nodes/http_request/node.py @@ -20,7 +20,7 @@ from .entities import ( HttpRequestNodeTimeout, Response, ) -from .exc import HttpRequestNodeError +from .exc import HttpRequestNodeError, RequestBodyError HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout( connect=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT, @@ -136,9 +136,13 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): data = node_data.body.data match body_type: case "binary": + if len(data) != 1: + raise RequestBodyError("invalid body data, should have only one item") selector = data[0].file selectors.append(VariableSelector(variable="#" + ".".join(selector) + "#", value_selector=selector)) case "json" | "raw-text": + if len(data) != 1: + raise RequestBodyError("invalid body data, should have only one item") selectors += variable_template_parser.extract_selectors_from_template(data[0].key) selectors += variable_template_parser.extract_selectors_from_template(data[0].value) case "x-www-form-urlencoded": From ef95b1268ebfbb9d9e77c4a62ebe97789b859020 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Mon, 23 Dec 2024 15:55:50 +0800 Subject: [PATCH 06/19] Fix/workflow retry (#11999) --- .../workflow/hooks/use-workflow-run.ts | 71 ++++++++++++++++--- .../_base/components/retry/retry-on-node.tsx | 4 +- web/app/components/workflow/run/index.tsx | 39 ++++++++-- .../workflow/run/iteration-result-panel.tsx | 40 ++++++++--- web/app/components/workflow/run/node.tsx | 5 ++ 5 files changed, 130 insertions(+), 29 deletions(-) diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index 822aa490db..cc1b0724a9 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -387,6 +387,9 @@ export const useWorkflowRun = () => { if (nodeIndex !== -1) { currIteration[nodeIndex] = { ...currIteration[nodeIndex], + ...(currIteration[nodeIndex].retryDetail + ? { retryDetail: currIteration[nodeIndex].retryDetail } + : {}), ...data, } as any } @@ -626,6 +629,8 @@ export const useWorkflowRun = () => { const { workflowRunningData, setWorkflowRunningData, + iterParallelLogMap, + setIterParallelLogMap, } = workflowStore.getState() const { getNodes, @@ -633,19 +638,65 @@ export const useWorkflowRun = () => { } = store.getState() const nodes = getNodes() - setWorkflowRunningData(produce(workflowRunningData!, (draft) => { - const tracing = draft.tracing! - const currentRetryNodeIndex = tracing.findIndex(trace => trace.node_id === data.node_id) + const currentNode = nodes.find(node => node.id === data.node_id)! + const nodeParent = nodes.find(node => node.id === currentNode.parentId) + if (nodeParent) { + if (!data.execution_metadata.parallel_mode_run_id) { + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + const tracing = draft.tracing! + const iteration = tracing.find(trace => trace.node_id === nodeParent.id) - if (currentRetryNodeIndex > -1) { - const currentRetryNode = tracing[currentRetryNodeIndex] - if (currentRetryNode.retryDetail) - draft.tracing![currentRetryNodeIndex].retryDetail!.push(data as NodeTracing) + if (iteration && iteration.details?.length) { + const currentNodeRetry = iteration.details[nodeParent.data._iterationIndex - 1]?.find(item => item.node_id === data.node_id) - else - draft.tracing![currentRetryNodeIndex].retryDetail = [data as NodeTracing] + if (currentNodeRetry) { + if (currentNodeRetry?.retryDetail) + currentNodeRetry?.retryDetail.push(data as NodeTracing) + else + currentNodeRetry.retryDetail = [data as NodeTracing] + } + } + })) } - })) + else { + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + const tracing = draft.tracing! + const iteration = tracing.find(trace => trace.node_id === nodeParent.id) + + if (iteration && iteration.details?.length) { + const iterRunID = data.execution_metadata?.parallel_mode_run_id + + const currIteration = iterParallelLogMap.get(iteration.node_id)?.get(iterRunID) + const currentNodeRetry = currIteration?.find(item => item.node_id === data.node_id) + + if (currentNodeRetry) { + if (currentNodeRetry?.retryDetail) + currentNodeRetry?.retryDetail.push(data as NodeTracing) + else + currentNodeRetry.retryDetail = [data as NodeTracing] + } + setIterParallelLogMap(iterParallelLogMap) + const iterLogMap = iterParallelLogMap.get(iteration.node_id) + if (iterLogMap) + iteration.details = Array.from(iterLogMap.values()) + } + })) + } + } + else { + setWorkflowRunningData(produce(workflowRunningData!, (draft) => { + const tracing = draft.tracing! + const currentRetryNodeIndex = tracing.findIndex(trace => trace.node_id === data.node_id) + + if (currentRetryNodeIndex > -1) { + const currentRetryNode = tracing[currentRetryNodeIndex] + if (currentRetryNode.retryDetail) + draft.tracing![currentRetryNodeIndex].retryDetail!.push(data as NodeTracing) + else + draft.tracing![currentRetryNodeIndex].retryDetail = [data as NodeTracing] + } + })) + } const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(node => node.id === data.node_id)! diff --git a/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx b/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx index f5d2f08ac8..9e1d8e1da6 100644 --- a/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx +++ b/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx @@ -31,7 +31,7 @@ const RetryOnNode = ({ }, [data._runningStatus, showSelectedBorder]) const showDefault = !isRunning && !isSuccessful && !isException && !isFailed - if (!retry_config) + if (!retry_config?.retry_enabled) return null return ( @@ -74,7 +74,7 @@ const RetryOnNode = ({ }
{ - !showDefault && ( + !showDefault && !!data._retryIndex && (
{data._retryIndex}/{data.retry_config?.max_retries}
diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index 520c59bf4c..8b0319cabe 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -78,11 +78,24 @@ const RunPanel: FC = ({ hideResult, activeTab = 'RESULT', runID, getRe const groupMap = nodeGroupMap.get(iterationNode.node_id)! - if (!groupMap.has(runId)) + if (!groupMap.has(runId)) { groupMap.set(runId, [item]) + } + else { + if (item.status === 'retry') { + const retryNode = groupMap.get(runId)!.find(node => node.node_id === item.node_id) - else - groupMap.get(runId)!.push(item) + if (retryNode) { + if (retryNode?.retryDetail) + retryNode.retryDetail.push(item) + else + retryNode.retryDetail = [item] + } + } + else { + groupMap.get(runId)!.push(item) + } + } if (item.status === 'failed') { iterationNode.status = 'failed' @@ -94,10 +107,24 @@ const RunPanel: FC = ({ hideResult, activeTab = 'RESULT', runID, getRe const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => { const { details } = iterationNode if (details) { - if (!details[index]) + if (!details[index]) { details[index] = [item] - else - details[index].push(item) + } + else { + if (item.status === 'retry') { + const retryNode = details[index].find(node => node.node_id === item.node_id) + + if (retryNode) { + if (retryNode?.retryDetail) + retryNode.retryDetail.push(item) + else + retryNode.retryDetail = [item] + } + } + else { + details[index].push(item) + } + } } if (item.status === 'failed') { diff --git a/web/app/components/workflow/run/iteration-result-panel.tsx b/web/app/components/workflow/run/iteration-result-panel.tsx index b13eadec99..b809e1e669 100644 --- a/web/app/components/workflow/run/iteration-result-panel.tsx +++ b/web/app/components/workflow/run/iteration-result-panel.tsx @@ -11,6 +11,7 @@ import { import { ArrowNarrowLeft } from '../../base/icons/src/vender/line/arrows' import { NodeRunningStatus } from '../types' import TracingPanel from './tracing-panel' +import RetryResultPanel from './retry-result-panel' import { Iteration } from '@/app/components/base/icons/src/vender/workflow' import cn from '@/utils/classnames' import type { IterationDurationMap, NodeTracing } from '@/types/workflow' @@ -41,8 +42,8 @@ const IterationResultPanel: FC = ({ })) }, []) const countIterDuration = (iteration: NodeTracing[], iterDurationMap: IterationDurationMap): string => { - const IterRunIndex = iteration[0].execution_metadata.iteration_index as number - const iterRunId = iteration[0].execution_metadata.parallel_mode_run_id + const IterRunIndex = iteration[0]?.execution_metadata?.iteration_index as number + const iterRunId = iteration[0]?.execution_metadata?.parallel_mode_run_id const iterItem = iterDurationMap[iterRunId || IterRunIndex] const duration = iterItem return `${(duration && duration > 0.01) ? duration.toFixed(2) : 0.01}s` @@ -74,6 +75,10 @@ const IterationResultPanel: FC = ({ ) } + const [retryRunResult, setRetryRunResult] = useState | undefined>() + const handleRetryDetail = (v: number, detail?: NodeTracing[]) => { + setRetryRunResult({ ...retryRunResult, [v]: detail }) + } const main = ( <> @@ -116,15 +121,28 @@ const IterationResultPanel: FC = ({ {expandedIterations[index] &&
} -
- -
+ { + !retryRunResult?.[index] && ( +
+ handleRetryDetail(index, v)} + /> +
+ ) + } + { + retryRunResult?.[index] && ( + handleRetryDetail(index, undefined)} + /> + ) + }
))} diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index bb07bd1e8c..d2da319a02 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -216,6 +216,11 @@ const NodePanel: FC = ({ {nodeInfo.error} )} + {nodeInfo.status === 'retry' && ( + + {nodeInfo.error} + + )} {nodeInfo.inputs && (
From c4091c4c6627e257765e15807e94763a63218db0 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 16:28:54 +0800 Subject: [PATCH 07/19] fix: improve error handling for file retrieval in AwsS3Storage (#12002) Signed-off-by: -LAN- --- api/extensions/storage/aws_s3_storage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/extensions/storage/aws_s3_storage.py b/api/extensions/storage/aws_s3_storage.py index ab2d0fba3b..ce36c2e7de 100644 --- a/api/extensions/storage/aws_s3_storage.py +++ b/api/extensions/storage/aws_s3_storage.py @@ -67,7 +67,9 @@ class AwsS3Storage(BaseStorage): yield from response["Body"].iter_chunks() except ClientError as ex: if ex.response["Error"]["Code"] == "NoSuchKey": - raise FileNotFoundError("File not found") + raise FileNotFoundError("file not found") + elif "reached max retries" in str(ex): + raise ValueError("please do not request the same file too frequently") else: raise From dfc25dbdd036746d93e9cf6cf7ecd09317644abc Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 23 Dec 2024 16:30:04 +0800 Subject: [PATCH 08/19] fix: drop useless and wrong code in Account (#11961) Signed-off-by: yihong0618 --- api/models/account.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/models/account.py b/api/models/account.py index 932ba1da57..a8602d10a9 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -99,11 +99,6 @@ class Account(UserMixin, db.Model): return db.session.query(Account).filter(Account.id == account_integrate.account_id).one_or_none() return None - def get_integrates(self) -> list[db.Model]: - ai = db.Model - return db.session.query(ai).filter(ai.account_id == self.id).all() - - # check current_user.current_tenant.current_role in ['admin', 'owner'] @property def is_admin_or_owner(self): return TenantAccountRole.is_privileged_role(self._current_tenant.current_role) From dc19cd5d9d1bfb354a522c86839e38501d973709 Mon Sep 17 00:00:00 2001 From: Novice <857526207@qq.com> Date: Mon, 23 Dec 2024 16:42:28 +0800 Subject: [PATCH 09/19] fix: add retry feature to code node (#12005) Co-authored-by: Novice Lee --- api/core/workflow/nodes/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/enums.py b/api/core/workflow/nodes/enums.py index 32fdc048d1..7970a49aa4 100644 --- a/api/core/workflow/nodes/enums.py +++ b/api/core/workflow/nodes/enums.py @@ -35,4 +35,4 @@ class FailBranchSourceHandle(StrEnum): CONTINUE_ON_ERROR_NODE_TYPE = [NodeType.LLM, NodeType.CODE, NodeType.TOOL, NodeType.HTTP_REQUEST] -RETRY_ON_ERROR_NODE_TYPE = [NodeType.LLM, NodeType.TOOL, NodeType.HTTP_REQUEST] +RETRY_ON_ERROR_NODE_TYPE = CONTINUE_ON_ERROR_NODE_TYPE From 2bf33c4dd2862818e0a0d1e90ca8f85785a9cf28 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Mon, 23 Dec 2024 17:18:09 +0800 Subject: [PATCH 10/19] Fix/workflow retry log (#12013) --- .../workflow/nodes/_base/components/retry/retry-on-node.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx b/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx index 9e1d8e1da6..34c3e28d2c 100644 --- a/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx +++ b/web/app/components/workflow/nodes/_base/components/retry/retry-on-node.tsx @@ -34,6 +34,9 @@ const RetryOnNode = ({ if (!retry_config?.retry_enabled) return null + if (!showDefault && !data._retryIndex) + return null + return (
Date: Mon, 23 Dec 2024 17:53:25 +0800 Subject: [PATCH 11/19] Fix/add retry mechanism to billing service request handling (#12006) Signed-off-by: -LAN- --- api/services/billing_service.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index 911d234641..edc5168217 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -1,6 +1,7 @@ import os -import requests +import httpx +from tenacity import retry, retry_if_not_exception_type, stop_before_delay, wait_fixed from extensions.ext_database import db from models.account import TenantAccountJoin, TenantAccountRole @@ -39,11 +40,17 @@ class BillingService: return cls._send_request("GET", "/invoices", params=params) @classmethod + @retry( + wait=wait_fixed(2), + stop=stop_before_delay(10), + retry=retry_if_not_exception_type(httpx.RequestError), + reraise=True, + ) def _send_request(cls, method, endpoint, json=None, params=None): headers = {"Content-Type": "application/json", "Billing-Api-Secret-Key": cls.secret_key} url = f"{cls.base_url}{endpoint}" - response = requests.request(method, url, json=json, params=params, headers=headers) + response = httpx.request(method, url, json=json, params=params, headers=headers) return response.json() From 75bce2822ed082ba6c3a1e2c78cd6e4b94b2001a Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 17:53:36 +0800 Subject: [PATCH 12/19] fix: add logging for missing edge mapping in StreamProcessor (#12008) Signed-off-by: -LAN- --- api/core/workflow/nodes/answer/base_stream_processor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/core/workflow/nodes/answer/base_stream_processor.py b/api/core/workflow/nodes/answer/base_stream_processor.py index 36c3fe180a..031347662e 100644 --- a/api/core/workflow/nodes/answer/base_stream_processor.py +++ b/api/core/workflow/nodes/answer/base_stream_processor.py @@ -1,3 +1,4 @@ +import logging from abc import ABC, abstractmethod from collections.abc import Generator @@ -5,6 +6,8 @@ from core.workflow.entities.variable_pool import VariablePool from core.workflow.graph_engine.entities.event import GraphEngineEvent, NodeRunSucceededEvent from core.workflow.graph_engine.entities.graph import Graph +logger = logging.getLogger(__name__) + class StreamProcessor(ABC): def __init__(self, graph: Graph, variable_pool: VariablePool) -> None: @@ -31,6 +34,9 @@ class StreamProcessor(ABC): if run_result.edge_source_handle: reachable_node_ids = [] unreachable_first_node_ids = [] + if finished_node_id not in self.graph.edge_mapping: + logger.warning(f"node {finished_node_id} has no edge mapping") + return for edge in self.graph.edge_mapping[finished_node_id]: if ( edge.run_condition From d0dd8b79554144208d403578e297eb9f49bc139a Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 17:53:42 +0800 Subject: [PATCH 13/19] fix: add UUID validation for tool file ID extraction (#12011) Signed-off-by: -LAN- --- api/core/workflow/nodes/tool/tool_node.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 3b56f94876..983fa7e623 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -1,5 +1,6 @@ from collections.abc import Mapping, Sequence from typing import Any +from uuid import UUID from sqlalchemy import select from sqlalchemy.orm import Session @@ -231,6 +232,10 @@ class ToolNode(BaseNode[ToolNodeData]): url = str(response.message) transfer_method = FileTransferMethod.TOOL_FILE tool_file_id = url.split("/")[-1].split(".")[0] + try: + UUID(tool_file_id) + except ValueError: + raise ToolFileError(f"cannot extract tool file id from url {url}") with Session(db.engine) as session: stmt = select(ToolFile).where(ToolFile.id == tool_file_id) tool_file = session.scalar(stmt) From af2888d3947654668ff01f9c6606fcfa0d0f6381 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 17:53:57 +0800 Subject: [PATCH 14/19] fix: remove json_schema if response format is disabled. (#12014) Signed-off-by: -LAN- --- api/core/model_runtime/model_providers/openai/llm/llm.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/core/model_runtime/model_providers/openai/llm/llm.py b/api/core/model_runtime/model_providers/openai/llm/llm.py index b73ce8752f..73cd7e3c34 100644 --- a/api/core/model_runtime/model_providers/openai/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai/llm/llm.py @@ -421,7 +421,11 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): # text completion model response = client.completions.create( - prompt=prompt_messages[0].content, model=model, stream=stream, **model_parameters, **extra_model_kwargs + prompt=prompt_messages[0].content, + model=model, + stream=stream, + **model_parameters, + **extra_model_kwargs, ) if stream: @@ -593,6 +597,8 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): model_parameters["response_format"] = {"type": "json_schema", "json_schema": schema} else: model_parameters["response_format"] = {"type": response_format} + elif "json_schema" in model_parameters: + del model_parameters["json_schema"] extra_model_kwargs = {} From c3c85276d194d4de738214fb84d39bfa2c15bd3d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 17:54:08 +0800 Subject: [PATCH 15/19] Fix/refactor invoke result handling in question classifier node (#12015) Signed-off-by: -LAN- --- .../question_classifier_node.py | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/api/core/workflow/nodes/question_classifier/question_classifier_node.py b/api/core/workflow/nodes/question_classifier/question_classifier_node.py index 5043e25e2b..31f8368d59 100644 --- a/api/core/workflow/nodes/question_classifier/question_classifier_node.py +++ b/api/core/workflow/nodes/question_classifier/question_classifier_node.py @@ -1,10 +1,8 @@ import json -import logging from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Optional, cast from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity -from core.llm_generator.output_parser.errors import OutputParserError from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance from core.model_runtime.entities import LLMUsage, ModelPropertyKey, PromptMessageRole @@ -96,27 +94,28 @@ class QuestionClassifierNode(LLMNode): jinja2_variables=[], ) - # handle invoke result - generator = self._invoke_llm( - node_data_model=node_data.model, - model_instance=model_instance, - prompt_messages=prompt_messages, - stop=stop, - ) - result_text = "" usage = LLMUsage.empty_usage() finish_reason = None - for event in generator: - if isinstance(event, ModelInvokeCompletedEvent): - result_text = event.text - usage = event.usage - finish_reason = event.finish_reason - break - category_name = node_data.classes[0].name - category_id = node_data.classes[0].id try: + # handle invoke result + generator = self._invoke_llm( + node_data_model=node_data.model, + model_instance=model_instance, + prompt_messages=prompt_messages, + stop=stop, + ) + + for event in generator: + if isinstance(event, ModelInvokeCompletedEvent): + result_text = event.text + usage = event.usage + finish_reason = event.finish_reason + break + + category_name = node_data.classes[0].name + category_id = node_data.classes[0].id result_text_json = parse_and_check_json_markdown(result_text, []) # result_text_json = json.loads(result_text.strip('```JSON\n')) if "category_name" in result_text_json and "category_id" in result_text_json: @@ -127,10 +126,6 @@ class QuestionClassifierNode(LLMNode): if category_id_result in category_ids: category_name = classes_map[category_id_result] category_id = category_id_result - - except OutputParserError: - logging.exception(f"Failed to parse result text: {result_text}") - try: process_data = { "model_mode": model_config.mode, "prompts": PromptMessageUtil.prompt_messages_to_prompt_for_saving( @@ -154,7 +149,7 @@ class QuestionClassifierNode(LLMNode): }, llm_usage=usage, ) - except Exception as e: + except ValueError as e: return NodeRunResult( status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, From e0f1410b48a7330b6a3b85f2103e9e889e9ccde1 Mon Sep 17 00:00:00 2001 From: yihong Date: Mon, 23 Dec 2024 18:56:59 +0800 Subject: [PATCH 16/19] fix: issue Multiple Paths Between IF/ELSE Branches (#11646) Signed-off-by: yihong0618 --- api/core/workflow/nodes/answer/answer_stream_processor.py | 1 - api/core/workflow/nodes/answer/base_stream_processor.py | 8 +++++++- .../core/workflow/graph_engine/test_graph_engine.py | 2 -- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/core/workflow/nodes/answer/answer_stream_processor.py b/api/core/workflow/nodes/answer/answer_stream_processor.py index d94f059058..ed033e7f28 100644 --- a/api/core/workflow/nodes/answer/answer_stream_processor.py +++ b/api/core/workflow/nodes/answer/answer_stream_processor.py @@ -60,7 +60,6 @@ class AnswerStreamProcessor(StreamProcessor): del self.current_stream_chunk_generating_node_ids[event.route_node_state.node_id] - # remove unreachable nodes self._remove_unreachable_nodes(event) # generate stream outputs diff --git a/api/core/workflow/nodes/answer/base_stream_processor.py b/api/core/workflow/nodes/answer/base_stream_processor.py index 031347662e..d785397e13 100644 --- a/api/core/workflow/nodes/answer/base_stream_processor.py +++ b/api/core/workflow/nodes/answer/base_stream_processor.py @@ -43,7 +43,13 @@ class StreamProcessor(ABC): and edge.run_condition.branch_identify and run_result.edge_source_handle == edge.run_condition.branch_identify ): - reachable_node_ids.extend(self._fetch_node_ids_in_reachable_branch(edge.target_node_id)) + # remove unreachable nodes + # FIXME: because of the code branch can combine directly, so for answer node + # we remove the node maybe shortcut the answer node, so comment this code for now + # there is not effect on the answer node and the workflow, when we have a better solution + # we can open this code. Issues: #11542 #9560 #10638 #10564 + + # reachable_node_ids.extend(self._fetch_node_ids_in_reachable_branch(edge.target_node_id)) continue else: unreachable_first_node_ids.append(edge.target_node_id) diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py b/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py index 9f1ba7b6af..b7d8f69e8c 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py @@ -488,14 +488,12 @@ def test_run_branch(mock_close, mock_remove): items = [] generator = graph_engine.run() for item in generator: - # print(type(item), item) items.append(item) assert len(items) == 10 assert items[3].route_node_state.node_id == "if-else-1" assert items[4].route_node_state.node_id == "if-else-1" assert isinstance(items[5], NodeRunStreamChunkEvent) - assert items[5].chunk_content == "1 " assert isinstance(items[6], NodeRunStreamChunkEvent) assert items[6].chunk_content == "takato" assert items[7].route_node_state.node_id == "answer-1" From e88ea71aefd2adca64683ce0cca836df015d0f4f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 19:15:48 +0800 Subject: [PATCH 17/19] chore/bump version to 0.14.2 (#12017) Signed-off-by: -LAN- --- api/configs/packaging/__init__.py | 2 +- api/services/app_dsl_service.py | 2 +- docker-legacy/docker-compose.yaml | 6 +++--- docker/docker-compose-template.yaml | 6 +++--- docker/docker-compose.yaml | 6 +++--- web/package.json | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 57cd74af1f..4a168a3fb1 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description="Dify version", - default="0.14.1", + default="0.14.2", ) COMMIT_SHA: str = Field( diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 0478903fa4..7c1a175988 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) IMPORT_INFO_REDIS_KEY_PREFIX = "app_import_info:" IMPORT_INFO_REDIS_EXPIRY = 180 # 3 minutes -CURRENT_DSL_VERSION = "0.1.4" +CURRENT_DSL_VERSION = "0.1.5" class ImportMode(StrEnum): diff --git a/docker-legacy/docker-compose.yaml b/docker-legacy/docker-compose.yaml index 3bf4333ad1..1cff58be7f 100644 --- a/docker-legacy/docker-compose.yaml +++ b/docker-legacy/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: # API service api: - image: langgenius/dify-api:0.14.1 + image: langgenius/dify-api:0.14.2 restart: always environment: # Startup mode, 'api' starts the API server. @@ -227,7 +227,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.14.1 + image: langgenius/dify-api:0.14.2 restart: always environment: CONSOLE_WEB_URL: '' @@ -397,7 +397,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.14.1 + image: langgenius/dify-web:0.14.2 restart: always environment: # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 8370d82daa..d4e0ba49d0 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:0.14.1 + image: langgenius/dify-api:0.14.2 restart: always environment: # Use the shared environment variables. @@ -25,7 +25,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.14.1 + image: langgenius/dify-api:0.14.2 restart: always environment: # Use the shared environment variables. @@ -47,7 +47,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.14.1 + image: langgenius/dify-web:0.14.2 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 99bc14c717..7122f4a6d0 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -390,7 +390,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:0.14.1 + image: langgenius/dify-api:0.14.2 restart: always environment: # Use the shared environment variables. @@ -413,7 +413,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.14.1 + image: langgenius/dify-api:0.14.2 restart: always environment: # Use the shared environment variables. @@ -435,7 +435,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.14.1 + image: langgenius/dify-web:0.14.2 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/web/package.json b/web/package.json index 25cf231a95..d9515645c8 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "0.14.1", + "version": "0.14.2", "private": true, "engines": { "node": ">=18.17.0" From 1c80941c697c3c869d601c106ff132f917d94a71 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 23 Dec 2024 22:36:44 +0800 Subject: [PATCH 18/19] fix: add FileNotFoundError to ignored errors in Sentry integration (#12023) Signed-off-by: -LAN- --- api/extensions/ext_sentry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py index 8016356a3e..3ec8ae6e1d 100644 --- a/api/extensions/ext_sentry.py +++ b/api/extensions/ext_sentry.py @@ -27,6 +27,7 @@ def init_app(app: DifyApp): ignore_errors=[ HTTPException, ValueError, + FileNotFoundError, openai.APIStatusError, InvokeRateLimitError, parse_error.defaultErrorResponse, From e0c24c0e99901e0531ed1dab4a05e9f19a827052 Mon Sep 17 00:00:00 2001 From: Yang Jingtao <83807321+You-keitou@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:30:31 +0900 Subject: [PATCH 19/19] fix: Fix session typo in workflow_trace method (#12031) --- api/core/ops/ops_trace_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index a04fc6ee78..4f41b6ed97 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -360,7 +360,7 @@ class TraceTask: raise ValueError("Workflow run not found") db.session.merge(workflow_run) - db.sessoin.refresh(workflow_run) + db.session.refresh(workflow_run) workflow_id = workflow_run.workflow_id tenant_id = workflow_run.tenant_id