diff --git a/.devcontainer/README.md b/.devcontainer/README.md index df12a3c2d6..2b18630a21 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -34,4 +34,4 @@ if you see such error message when you open this project in codespaces: ![Alt text](troubleshooting.png) a simple workaround is change `/signin` endpoint into another one, then login with GitHub account and close the tab, then change it back to `/signin` endpoint. Then all things will be fine. -The reason is `signin` endpoint is not allowed in codespaces, details can be found [here](https://github.com/orgs/community/discussions/5204) \ No newline at end of file +The reason is `signin` endpoint is not allowed in codespaces, details can be found [here](https://github.com/orgs/community/discussions/5204) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 339ad60ce0..8246544061 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/anaconda { "name": "Python 3.12", - "build": { + "build": { "context": "..", "dockerfile": "Dockerfile" }, diff --git a/.devcontainer/noop.txt b/.devcontainer/noop.txt index dde8dc3c10..49de88dbd4 100644 --- a/.devcontainer/noop.txt +++ b/.devcontainer/noop.txt @@ -1,3 +1,3 @@ This file copied into the container along with environment.yml* from the parent -folder. This file is included to prevents the Dockerfile COPY instruction from -failing if no environment.yml is found. \ No newline at end of file +folder. This file is included to prevents the Dockerfile COPY instruction from +failing if no environment.yml is found. diff --git a/web/.editorconfig b/.editorconfig similarity index 51% rename from web/.editorconfig rename to .editorconfig index e1d3f0b992..374da0b5d2 100644 --- a/web/.editorconfig +++ b/.editorconfig @@ -5,18 +5,35 @@ root = true # Unix-style newlines with a newline ending every file [*] +charset = utf-8 end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 +indent_style = space + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.toml] +indent_size = 4 +indent_style = space + +# Markdown and MDX are whitespace sensitive languages. +# Do not remove trailing spaces. +[*.{md,mdx}] +trim_trailing_whitespace = false # Matches multiple files with brace expansion notation # Set default charset [*.{js,tsx}] -charset = utf-8 indent_style = space indent_size = 2 - -# Matches the exact files either package.json or .travis.yml -[{package.json,.travis.yml}] +# Matches the exact files package.json +[package.json] indent_style = space indent_size = 2 diff --git a/.gitattributes b/.gitattributes index a10da53408..a32a39f65c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ # Ensure that .sh scripts use LF as line separator, even if they are checked out -# to Windows(NTFS) file-system, by a user of Docker for Windows. +# to Windows(NTFS) file-system, by a user of Docker for Windows. # These .sh scripts will be run from the Container after `docker compose up -d`. # If they appear to be CRLF style, Dash from the Container will fail to execute # them. diff --git a/.github/linters/editorconfig-checker.json b/.github/linters/editorconfig-checker.json new file mode 100644 index 0000000000..ce6e9ae341 --- /dev/null +++ b/.github/linters/editorconfig-checker.json @@ -0,0 +1,22 @@ +{ + "Verbose": false, + "Debug": false, + "IgnoreDefaults": false, + "SpacesAfterTabs": false, + "NoColor": false, + "Exclude": [ + "^web/public/vs/", + "^web/public/pdf.worker.min.mjs$", + "web/app/components/base/icons/src/vender/" + ], + "AllowedContentTypes": [], + "PassedFiles": [], + "Disable": { + "EndOfLine": false, + "Indentation": false, + "IndentSize": true, + "InsertFinalNewline": false, + "TrimTrailingWhitespace": false, + "MaxLineLength": false + } +} diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 98e5fd5150..56f9b433f3 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -9,6 +9,12 @@ concurrency: group: style-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + checks: write + statuses: write + contents: read + + jobs: python-style: name: Python Style @@ -163,3 +169,14 @@ jobs: VALIDATE_DOCKERFILE_HADOLINT: true VALIDATE_XML: true VALIDATE_YAML: true + + - name: EditorConfig checks + uses: super-linter/super-linter/slim@v7 + env: + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IGNORE_GENERATED_FILES: true + IGNORE_GITIGNORED_FILES: true + # EditorConfig validation + VALIDATE_EDITORCONFIG: true + EDITORCONFIG_FILE_NAME: editorconfig-checker.json diff --git a/CONTRIBUTING_ES.md b/CONTRIBUTING_ES.md index 261aa0fda1..98cbb5b457 100644 --- a/CONTRIBUTING_ES.md +++ b/CONTRIBUTING_ES.md @@ -90,4 +90,4 @@ Recomendamos revisar este documento cuidadosamente antes de proceder con la conf No dudes en contactarnos si encuentras algún problema durante el proceso de configuración. ## Obteniendo Ayuda -Si alguna vez te quedas atascado o tienes una pregunta urgente mientras contribuyes, simplemente envíanos tus consultas a través del issue relacionado de GitHub, o únete a nuestro [Discord](https://discord.gg/8Tpq4AcN9c) para una charla rápida. \ No newline at end of file +Si alguna vez te quedas atascado o tienes una pregunta urgente mientras contribuyes, simplemente envíanos tus consultas a través del issue relacionado de GitHub, o únete a nuestro [Discord](https://discord.gg/8Tpq4AcN9c) para una charla rápida. diff --git a/CONTRIBUTING_FR.md b/CONTRIBUTING_FR.md index c3418f86cc..fc8410dfd6 100644 --- a/CONTRIBUTING_FR.md +++ b/CONTRIBUTING_FR.md @@ -90,4 +90,4 @@ Nous recommandons de revoir attentivement ce document avant de procéder à la c N'hésitez pas à nous contacter si vous rencontrez des problèmes pendant le processus de configuration. ## Obtenir de l'aide -Si jamais vous êtes bloqué ou avez une question urgente en contribuant, envoyez-nous simplement vos questions via le problème GitHub concerné, ou rejoignez notre [Discord](https://discord.gg/8Tpq4AcN9c) pour une discussion rapide. \ No newline at end of file +Si jamais vous êtes bloqué ou avez une question urgente en contribuant, envoyez-nous simplement vos questions via le problème GitHub concerné, ou rejoignez notre [Discord](https://discord.gg/8Tpq4AcN9c) pour une discussion rapide. diff --git a/CONTRIBUTING_KR.md b/CONTRIBUTING_KR.md index fcf44d495a..78d3f38c47 100644 --- a/CONTRIBUTING_KR.md +++ b/CONTRIBUTING_KR.md @@ -90,4 +90,4 @@ PR 설명에 기존 이슈를 연결하거나 새 이슈를 여는 것을 잊지 설정 과정에서 문제가 발생하면 언제든지 연락해 주세요. ## 도움 받기 -기여하는 동안 막히거나 긴급한 질문이 있으면, 관련 GitHub 이슈를 통해 질문을 보내거나, 빠른 대화를 위해 우리의 [Discord](https://discord.gg/8Tpq4AcN9c)에 참여하세요. \ No newline at end of file +기여하는 동안 막히거나 긴급한 질문이 있으면, 관련 GitHub 이슈를 통해 질문을 보내거나, 빠른 대화를 위해 우리의 [Discord](https://discord.gg/8Tpq4AcN9c)에 참여하세요. diff --git a/CONTRIBUTING_PT.md b/CONTRIBUTING_PT.md index bba76c17ee..7347fd7f9c 100644 --- a/CONTRIBUTING_PT.md +++ b/CONTRIBUTING_PT.md @@ -90,4 +90,4 @@ Recomendamos revisar este documento cuidadosamente antes de prosseguir com a con Sinta-se à vontade para entrar em contato se encontrar quaisquer problemas durante o processo de configuração. ## Obtendo Ajuda -Se você ficar preso ou tiver uma dúvida urgente enquanto contribui, simplesmente envie suas perguntas através do problema relacionado no GitHub, ou entre no nosso [Discord](https://discord.gg/8Tpq4AcN9c) para uma conversa rápida. \ No newline at end of file +Se você ficar preso ou tiver uma dúvida urgente enquanto contribui, simplesmente envie suas perguntas através do problema relacionado no GitHub, ou entre no nosso [Discord](https://discord.gg/8Tpq4AcN9c) para uma conversa rápida. diff --git a/CONTRIBUTING_TR.md b/CONTRIBUTING_TR.md index 4e216d22a4..681f05689b 100644 --- a/CONTRIBUTING_TR.md +++ b/CONTRIBUTING_TR.md @@ -90,4 +90,4 @@ Kuruluma geçmeden önce bu belgeyi dikkatlice incelemenizi öneririz, çünkü Kurulum süreci sırasında herhangi bir sorunla karşılaşırsanız bizimle iletişime geçmekten çekinmeyin. ## Yardım Almak -Katkıda bulunurken takılırsanız veya yanıcı bir sorunuz olursa, sorularınızı ilgili GitHub sorunu aracılığıyla bize gönderin veya hızlı bir sohbet için [Discord'umuza](https://discord.gg/8Tpq4AcN9c) katılın. \ No newline at end of file +Katkıda bulunurken takılırsanız veya yanıcı bir sorunuz olursa, sorularınızı ilgili GitHub sorunu aracılığıyla bize gönderin veya hızlı bir sohbet için [Discord'umuza](https://discord.gg/8Tpq4AcN9c) katılın. diff --git a/README_SI.md b/README_SI.md index caa5975973..9a38b558b4 100644 --- a/README_SI.md +++ b/README_SI.md @@ -1,259 +1,259 @@ -![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab) - -

- 📌 Predstavljamo nalaganje datotek Dify Workflow: znova ustvarite Google NotebookLM Podcast -

- -

- Dify Cloud · - Samostojno gostovanje · - Dokumentacija · - Pregled ponudb izdelkov Dify -

- -

- - Static Badge - - Static Badge - - chat on Discord - - follow on X(Twitter) - - follow on LinkedIn - - Docker Pulls - - Commits last month - - Issues closed - - Discussion posts -

- -

- README in English - 简体中文版自述文件 - 日本語のREADME - README en Español - README en Français - README tlhIngan Hol - README in Korean - README بالعربية - Türkçe README - README Tiếng Việt - README Slovenščina - README in বাংলা -

- - -Dify je odprtokodna platforma za razvoj aplikacij LLM. Njegov intuitivni vmesnik združuje agentski potek dela z umetno inteligenco, cevovod RAG, zmogljivosti agentov, upravljanje modelov, funkcije opazovanja in več, kar vam omogoča hiter prehod od prototipa do proizvodnje. - -## Hitri začetek -> Preden namestite Dify, se prepričajte, da vaša naprava izpolnjuje naslednje minimalne sistemske zahteve: -> ->- CPU >= 2 Core ->- RAM >= 4 GiB - -
- -Najlažji način za zagon strežnika Dify je prek docker compose . Preden zaženete Dify z naslednjimi ukazi, se prepričajte, da sta Docker in Docker Compose nameščena na vašem računalniku: - -```bash -cd dify -cd docker -cp .env.example .env -docker compose up -d -``` - -Po zagonu lahko dostopate do nadzorne plošče Dify v brskalniku na [http://localhost/install](http://localhost/install) in začnete postopek inicializacije. - -#### Iskanje pomoči -Prosimo, glejte naša pogosta vprašanja [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) če naletite na težave pri nastavitvi Dify. Če imate še vedno težave, se obrnite na [skupnost ali nas](#community--contact). - -> Če želite prispevati k Difyju ali narediti dodaten razvoj, glejte naš vodnik za [uvajanje iz izvorne kode](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code) - -## Ključne značilnosti -**1. Potek dela**: - Zgradite in preizkusite zmogljive poteke dela AI na vizualnem platnu, pri čemer izkoristite vse naslednje funkcije in več. - - - https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa - - - -**2. Celovita podpora za modele**: - Brezhibna integracija s stotinami lastniških/odprtokodnih LLM-jev ducatov ponudnikov sklepanja in samostojnih rešitev, ki pokrivajo GPT, Mistral, Llama3 in vse modele, združljive z API-jem OpenAI. Celoten seznam podprtih ponudnikov modelov najdete [tukaj](https://docs.dify.ai/getting-started/readme/model-providers). - -![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3) - - -**3. Prompt IDE**: - intuitivni vmesnik za ustvarjanje pozivov, primerjavo zmogljivosti modela in dodajanje dodatnih funkcij, kot je pretvorba besedila v govor, aplikaciji, ki temelji na klepetu. - -**4. RAG Pipeline**: - E Obsežne zmogljivosti RAG, ki pokrivajo vse od vnosa dokumenta do priklica, s podporo za ekstrakcijo besedila iz datotek PDF, PPT in drugih običajnih formatov dokumentov. - -**5. Agent capabilities**: - definirate lahko agente, ki temeljijo na klicanju funkcij LLM ali ReAct, in dodate vnaprej izdelana orodja ali orodja po meri za agenta. Dify ponuja več kot 50 vgrajenih orodij za agente AI, kot so Google Search, DALL·E, Stable Diffusion in WolframAlpha. - -**6. LLMOps**: - Spremljajte in analizirajte dnevnike aplikacij in učinkovitost skozi čas. Pozive, nabore podatkov in modele lahko nenehno izboljšujete na podlagi proizvodnih podatkov in opomb. - -**7. Backend-as-a-Service**: - AVse ponudbe Difyja so opremljene z ustreznimi API-ji, tako da lahko Dify brez težav integrirate v svojo poslovno logiko. - -## Primerjava Funkcij - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FunkcijaDify.AILangChainFlowiseOpenAI Assistants API
Programski pristopAPI + usmerjeno v aplikacijePython kodaUsmerjeno v aplikacijeUsmerjeno v API
Podprti LLM-jiBogata izbiraBogata izbiraBogata izbiraSamo OpenAI
RAG pogon
Agent
Potek dela
Spremljanje
Funkcija za podjetja (SSO/nadzor dostopa)
Lokalna namestitev
- -## Uporaba Dify - -- **Cloud
** -Gostimo storitev Dify Cloud za vsakogar, ki jo lahko preizkusite brez nastavitev. Zagotavlja vse zmožnosti različice za samostojno namestitev in vključuje 200 brezplačnih klicev GPT-4 v načrtu peskovnika. - -- **Self-hosting Dify Community Edition
** -Hitro zaženite Dify v svojem okolju s tem [začetnim vodnikom](#quick-start) . Za dodatne reference in podrobnejša navodila uporabite našo [dokumentacijo](https://docs.dify.ai) . - - -- **Dify za podjetja/organizacije
** -Ponujamo dodatne funkcije, osredotočene na podjetja. Zabeležite svoja vprašanja prek tega klepetalnega robota ali nam pošljite e-pošto, da se pogovorimo o potrebah podjetja.
- > Za novoustanovljena podjetja in mala podjetja, ki uporabljajo AWS, si oglejte Dify Premium na AWS Marketplace in ga z enim klikom uvedite v svoj AWS VPC. To je cenovno ugodna ponudba AMI z možnostjo ustvarjanja aplikacij z logotipom in blagovno znamko po meri. - - -## Staying ahead - -Star Dify on GitHub and be instantly notified of new releases. - -![star-us](https://github.com/langgenius/dify/assets/13230914/b823edc1-6388-4e25-ad45-2f6b187adbb4) - - -## Napredne nastavitve - -Če morate prilagoditi konfiguracijo, si oglejte komentarje v naši datoteki .env.example in posodobite ustrezne vrednosti v svoji .env datoteki. Poleg tega boste morda morali prilagoditi docker-compose.yamlsamo datoteko, na primer spremeniti različice slike, preslikave vrat ali namestitve nosilca, glede na vaše specifično okolje in zahteve za uvajanje. Po kakršnih koli spremembah ponovno zaženite docker-compose up -d. Celoten seznam razpoložljivih spremenljivk okolja najdete tukaj . - -Če želite konfigurirati visoko razpoložljivo nastavitev, so na voljo Helm Charts in datoteke YAML, ki jih prispeva skupnost, ki omogočajo uvedbo Difyja v Kubernetes. - -- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) -- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) -- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) -- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) - -#### Uporaba Terraform za uvajanje - -namestite Dify v Cloud Platform z enim klikom z uporabo [terraform](https://www.terraform.io/) - -##### Azure Global -- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform) - -##### Google Cloud -- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) - -#### Uporaba AWS CDK za uvajanje - -Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/) - -##### AWS -- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) - -## Prispevam - -Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkrati vas prosimo, da podprete Dify tako, da ga delite na družbenih medijih ter na dogodkih in konferencah. - - - -> Iščemo sodelavce za pomoč pri prevajanju Difyja v jezike, ki niso mandarinščina ali angleščina. Če želite pomagati, si oglejte i18n README za več informacij in nam pustite komentar v global-userskanalu našega strežnika skupnosti Discord . - -## Skupnost in stik - -* [Github Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj. -* [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). -* [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. -* [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. - -**Contributors** - - - - - -## Star history - -[![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date) - - -## Varnostno razkritje - -Zaradi zaščite vaše zasebnosti se izogibajte objavljanju varnostnih vprašanj na GitHub. Namesto tega pošljite vprašanja na security@dify.ai in zagotovili vam bomo podrobnejši odgovor. - -## Licenca - -To skladišče je na voljo pod [odprtokodno licenco Dify](LICENSE) , ki je v bistvu Apache 2.0 z nekaj dodatnimi omejitvami. +![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab) + +

+ 📌 Predstavljamo nalaganje datotek Dify Workflow: znova ustvarite Google NotebookLM Podcast +

+ +

+ Dify Cloud · + Samostojno gostovanje · + Dokumentacija · + Pregled ponudb izdelkov Dify +

+ +

+ + Static Badge + + Static Badge + + chat on Discord + + follow on X(Twitter) + + follow on LinkedIn + + Docker Pulls + + Commits last month + + Issues closed + + Discussion posts +

+ +

+ README in English + 简体中文版自述文件 + 日本語のREADME + README en Español + README en Français + README tlhIngan Hol + README in Korean + README بالعربية + Türkçe README + README Tiếng Việt + README Slovenščina + README in বাংলা +

+ + +Dify je odprtokodna platforma za razvoj aplikacij LLM. Njegov intuitivni vmesnik združuje agentski potek dela z umetno inteligenco, cevovod RAG, zmogljivosti agentov, upravljanje modelov, funkcije opazovanja in več, kar vam omogoča hiter prehod od prototipa do proizvodnje. + +## Hitri začetek +> Preden namestite Dify, se prepričajte, da vaša naprava izpolnjuje naslednje minimalne sistemske zahteve: +> +>- CPU >= 2 Core +>- RAM >= 4 GiB + +
+ +Najlažji način za zagon strežnika Dify je prek docker compose . Preden zaženete Dify z naslednjimi ukazi, se prepričajte, da sta Docker in Docker Compose nameščena na vašem računalniku: + +```bash +cd dify +cd docker +cp .env.example .env +docker compose up -d +``` + +Po zagonu lahko dostopate do nadzorne plošče Dify v brskalniku na [http://localhost/install](http://localhost/install) in začnete postopek inicializacije. + +#### Iskanje pomoči +Prosimo, glejte naša pogosta vprašanja [FAQ](https://docs.dify.ai/getting-started/install-self-hosted/faqs) če naletite na težave pri nastavitvi Dify. Če imate še vedno težave, se obrnite na [skupnost ali nas](#community--contact). + +> Če želite prispevati k Difyju ali narediti dodaten razvoj, glejte naš vodnik za [uvajanje iz izvorne kode](https://docs.dify.ai/getting-started/install-self-hosted/local-source-code) + +## Ključne značilnosti +**1. Potek dela**: + Zgradite in preizkusite zmogljive poteke dela AI na vizualnem platnu, pri čemer izkoristite vse naslednje funkcije in več. + + + https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa + + + +**2. Celovita podpora za modele**: + Brezhibna integracija s stotinami lastniških/odprtokodnih LLM-jev ducatov ponudnikov sklepanja in samostojnih rešitev, ki pokrivajo GPT, Mistral, Llama3 in vse modele, združljive z API-jem OpenAI. Celoten seznam podprtih ponudnikov modelov najdete [tukaj](https://docs.dify.ai/getting-started/readme/model-providers). + +![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3) + + +**3. Prompt IDE**: + intuitivni vmesnik za ustvarjanje pozivov, primerjavo zmogljivosti modela in dodajanje dodatnih funkcij, kot je pretvorba besedila v govor, aplikaciji, ki temelji na klepetu. + +**4. RAG Pipeline**: + E Obsežne zmogljivosti RAG, ki pokrivajo vse od vnosa dokumenta do priklica, s podporo za ekstrakcijo besedila iz datotek PDF, PPT in drugih običajnih formatov dokumentov. + +**5. Agent capabilities**: + definirate lahko agente, ki temeljijo na klicanju funkcij LLM ali ReAct, in dodate vnaprej izdelana orodja ali orodja po meri za agenta. Dify ponuja več kot 50 vgrajenih orodij za agente AI, kot so Google Search, DALL·E, Stable Diffusion in WolframAlpha. + +**6. LLMOps**: + Spremljajte in analizirajte dnevnike aplikacij in učinkovitost skozi čas. Pozive, nabore podatkov in modele lahko nenehno izboljšujete na podlagi proizvodnih podatkov in opomb. + +**7. Backend-as-a-Service**: + AVse ponudbe Difyja so opremljene z ustreznimi API-ji, tako da lahko Dify brez težav integrirate v svojo poslovno logiko. + +## Primerjava Funkcij + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FunkcijaDify.AILangChainFlowiseOpenAI Assistants API
Programski pristopAPI + usmerjeno v aplikacijePython kodaUsmerjeno v aplikacijeUsmerjeno v API
Podprti LLM-jiBogata izbiraBogata izbiraBogata izbiraSamo OpenAI
RAG pogon
Agent
Potek dela
Spremljanje
Funkcija za podjetja (SSO/nadzor dostopa)
Lokalna namestitev
+ +## Uporaba Dify + +- **Cloud
** +Gostimo storitev Dify Cloud za vsakogar, ki jo lahko preizkusite brez nastavitev. Zagotavlja vse zmožnosti različice za samostojno namestitev in vključuje 200 brezplačnih klicev GPT-4 v načrtu peskovnika. + +- **Self-hosting Dify Community Edition
** +Hitro zaženite Dify v svojem okolju s tem [začetnim vodnikom](#quick-start) . Za dodatne reference in podrobnejša navodila uporabite našo [dokumentacijo](https://docs.dify.ai) . + + +- **Dify za podjetja/organizacije
** +Ponujamo dodatne funkcije, osredotočene na podjetja. Zabeležite svoja vprašanja prek tega klepetalnega robota ali nam pošljite e-pošto, da se pogovorimo o potrebah podjetja.
+ > Za novoustanovljena podjetja in mala podjetja, ki uporabljajo AWS, si oglejte Dify Premium na AWS Marketplace in ga z enim klikom uvedite v svoj AWS VPC. To je cenovno ugodna ponudba AMI z možnostjo ustvarjanja aplikacij z logotipom in blagovno znamko po meri. + + +## Staying ahead + +Star Dify on GitHub and be instantly notified of new releases. + +![star-us](https://github.com/langgenius/dify/assets/13230914/b823edc1-6388-4e25-ad45-2f6b187adbb4) + + +## Napredne nastavitve + +Če morate prilagoditi konfiguracijo, si oglejte komentarje v naši datoteki .env.example in posodobite ustrezne vrednosti v svoji .env datoteki. Poleg tega boste morda morali prilagoditi docker-compose.yamlsamo datoteko, na primer spremeniti različice slike, preslikave vrat ali namestitve nosilca, glede na vaše specifično okolje in zahteve za uvajanje. Po kakršnih koli spremembah ponovno zaženite docker-compose up -d. Celoten seznam razpoložljivih spremenljivk okolja najdete tukaj . + +Če želite konfigurirati visoko razpoložljivo nastavitev, so na voljo Helm Charts in datoteke YAML, ki jih prispeva skupnost, ki omogočajo uvedbo Difyja v Kubernetes. + +- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) +- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) +- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) +- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) + +#### Uporaba Terraform za uvajanje + +namestite Dify v Cloud Platform z enim klikom z uporabo [terraform](https://www.terraform.io/) + +##### Azure Global +- [Azure Terraform by @nikawang](https://github.com/nikawang/dify-azure-terraform) + +##### Google Cloud +- [Google Cloud Terraform by @sotazum](https://github.com/DeNA/dify-google-cloud-terraform) + +#### Uporaba AWS CDK za uvajanje + +Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/) + +##### AWS +- [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + +## Prispevam + +Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkrati vas prosimo, da podprete Dify tako, da ga delite na družbenih medijih ter na dogodkih in konferencah. + + + +> Iščemo sodelavce za pomoč pri prevajanju Difyja v jezike, ki niso mandarinščina ali angleščina. Če želite pomagati, si oglejte i18n README za več informacij in nam pustite komentar v global-userskanalu našega strežnika skupnosti Discord . + +## Skupnost in stik + +* [Github Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj. +* [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). +* [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. +* [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo. + +**Contributors** + + + + + +## Star history + +[![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date) + + +## Varnostno razkritje + +Zaradi zaščite vaše zasebnosti se izogibajte objavljanju varnostnih vprašanj na GitHub. Namesto tega pošljite vprašanja na security@dify.ai in zagotovili vam bomo podrobnejši odgovor. + +## Licenca + +To skladišče je na voljo pod [odprtokodno licenco Dify](LICENSE) , ki je v bistvu Apache 2.0 z nekaj dodatnimi omejitvami. diff --git a/api/.dockerignore b/api/.dockerignore index 447edcda08..a0ce59d221 100644 --- a/api/.dockerignore +++ b/api/.dockerignore @@ -16,4 +16,4 @@ logs .ruff_cache # venv -.venv \ No newline at end of file +.venv diff --git a/api/app_factory.py b/api/app_factory.py index 9648d770ab..586f2ded9e 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -52,7 +52,6 @@ def initialize_extensions(app: DifyApp): ext_mail, ext_migrate, ext_otel, - ext_otel_patch, ext_proxy_fix, ext_redis, ext_repositories, @@ -85,7 +84,6 @@ def initialize_extensions(app: DifyApp): ext_proxy_fix, ext_blueprints, ext_commands, - ext_otel_patch, # Apply patch before initializing OpenTelemetry ext_otel, ] for ext in extensions: diff --git a/api/commands.py b/api/commands.py index 3881439ddf..07bc6cd927 100644 --- a/api/commands.py +++ b/api/commands.py @@ -444,13 +444,13 @@ def convert_to_agent_apps(): WHERE a.mode = 'chat' AND am.agent_mode is not null AND ( - am.agent_mode like '%"strategy": "function_call"%' + am.agent_mode like '%"strategy": "function_call"%' OR am.agent_mode like '%"strategy": "react"%' - ) + ) AND ( - am.agent_mode like '{"enabled": true%' + am.agent_mode like '{"enabled": true%' OR am.agent_mode like '{"max_iteration": %' - ) ORDER BY a.created_at DESC LIMIT 1000 + ) ORDER BY a.created_at DESC LIMIT 1000 """ with db.engine.begin() as conn: @@ -818,8 +818,9 @@ def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[ click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green")) +@click.option("-f", "--force", is_flag=True, help="Skip user confirmation and force the command to execute.") @click.command("clear-orphaned-file-records", help="Clear orphaned file records.") -def clear_orphaned_file_records(): +def clear_orphaned_file_records(force: bool): """ Clear orphaned file records in the database. """ @@ -845,7 +846,15 @@ def clear_orphaned_file_records(): # notify user and ask for confirmation click.echo( - click.style("This command will find and delete orphaned file records in the following tables:", fg="yellow") + click.style( + "This command will first find and delete orphaned file records from the message_files table,", fg="yellow" + ) + ) + click.echo( + click.style( + "and then it will find and delete orphaned file records in the following tables:", + fg="yellow", + ) ) for files_table in files_tables: click.echo(click.style(f"- {files_table['table']}", fg="yellow")) @@ -878,11 +887,55 @@ def clear_orphaned_file_records(): fg="yellow", ) ) - click.confirm("Do you want to proceed?", abort=True) + if not force: + click.confirm("Do you want to proceed?", abort=True) # start the cleanup process click.echo(click.style("Starting orphaned file records cleanup.", fg="white")) + # clean up the orphaned records in the message_files table where message_id doesn't exist in messages table + try: + click.echo( + click.style("- Listing message_files records where message_id doesn't exist in messages table", fg="white") + ) + query = ( + "SELECT mf.id, mf.message_id " + "FROM message_files mf LEFT JOIN messages m ON mf.message_id = m.id " + "WHERE m.id IS NULL" + ) + orphaned_message_files = [] + with db.engine.begin() as conn: + rs = conn.execute(db.text(query)) + for i in rs: + orphaned_message_files.append({"id": str(i[0]), "message_id": str(i[1])}) + + if orphaned_message_files: + click.echo(click.style(f"Found {len(orphaned_message_files)} orphaned message_files records:", fg="white")) + for record in orphaned_message_files: + click.echo(click.style(f" - id: {record['id']}, message_id: {record['message_id']}", fg="black")) + + if not force: + click.confirm( + ( + f"Do you want to proceed " + f"to delete all {len(orphaned_message_files)} orphaned message_files records?" + ), + abort=True, + ) + + click.echo(click.style("- Deleting orphaned message_files records", fg="white")) + query = "DELETE FROM message_files WHERE id IN :ids" + with db.engine.begin() as conn: + conn.execute(db.text(query), {"ids": tuple([record["id"] for record in orphaned_message_files])}) + click.echo( + click.style(f"Removed {len(orphaned_message_files)} orphaned message_files records.", fg="green") + ) + else: + click.echo(click.style("No orphaned message_files records found. There is nothing to delete.", fg="green")) + except Exception as e: + click.echo(click.style(f"Error deleting orphaned message_files records: {str(e)}", fg="red")) + + # clean up the orphaned records in the rest of the *_files tables try: # fetch file id and keys from each table all_files_in_tables = [] @@ -964,7 +1017,8 @@ def clear_orphaned_file_records(): click.echo(click.style(f"Found {len(orphaned_files)} orphaned file records.", fg="white")) for file in orphaned_files: click.echo(click.style(f"- orphaned file id: {file}", fg="black")) - click.confirm(f"Do you want to proceed to delete all {len(orphaned_files)} orphaned file records?", abort=True) + if not force: + click.confirm(f"Do you want to proceed to delete all {len(orphaned_files)} orphaned file records?", abort=True) # delete orphaned records for each file try: @@ -979,8 +1033,9 @@ def clear_orphaned_file_records(): click.echo(click.style(f"Removed {len(orphaned_files)} orphaned file records.", fg="green")) +@click.option("-f", "--force", is_flag=True, help="Skip user confirmation and force the command to execute.") @click.command("remove-orphaned-files-on-storage", help="Remove orphaned files on the storage.") -def remove_orphaned_files_on_storage(): +def remove_orphaned_files_on_storage(force: bool): """ Remove orphaned files on the storage. """ @@ -1028,7 +1083,8 @@ def remove_orphaned_files_on_storage(): fg="yellow", ) ) - click.confirm("Do you want to proceed?", abort=True) + if not force: + click.confirm("Do you want to proceed?", abort=True) # start the cleanup process click.echo(click.style("Starting orphaned files cleanup.", fg="white")) @@ -1069,7 +1125,8 @@ def remove_orphaned_files_on_storage(): click.echo(click.style(f"Found {len(orphaned_files)} orphaned files.", fg="white")) for file in orphaned_files: click.echo(click.style(f"- orphaned file: {file}", fg="black")) - click.confirm(f"Do you want to proceed to remove all {len(orphaned_files)} orphaned files?", abort=True) + if not force: + click.confirm(f"Do you want to proceed to remove all {len(orphaned_files)} orphaned files?", abort=True) # delete orphaned files removed_files = 0 diff --git a/api/configs/middleware/vdb/opensearch_config.py b/api/configs/middleware/vdb/opensearch_config.py index 81dde4c04d..96f478e9a6 100644 --- a/api/configs/middleware/vdb/opensearch_config.py +++ b/api/configs/middleware/vdb/opensearch_config.py @@ -1,4 +1,5 @@ -from typing import Optional +import enum +from typing import Literal, Optional from pydantic import Field, PositiveInt from pydantic_settings import BaseSettings @@ -9,6 +10,14 @@ class OpenSearchConfig(BaseSettings): Configuration settings for OpenSearch """ + class AuthMethod(enum.StrEnum): + """ + Authentication method for OpenSearch + """ + + BASIC = "basic" + AWS_MANAGED_IAM = "aws_managed_iam" + OPENSEARCH_HOST: Optional[str] = Field( description="Hostname or IP address of the OpenSearch server (e.g., 'localhost' or 'opensearch.example.com')", default=None, @@ -19,6 +28,16 @@ class OpenSearchConfig(BaseSettings): default=9200, ) + OPENSEARCH_SECURE: bool = Field( + description="Whether to use SSL/TLS encrypted connection for OpenSearch (True for HTTPS, False for HTTP)", + default=False, + ) + + OPENSEARCH_AUTH_METHOD: AuthMethod = Field( + description="Authentication method for OpenSearch connection (default is 'basic')", + default=AuthMethod.BASIC, + ) + OPENSEARCH_USER: Optional[str] = Field( description="Username for authenticating with OpenSearch", default=None, @@ -29,7 +48,11 @@ class OpenSearchConfig(BaseSettings): default=None, ) - OPENSEARCH_SECURE: bool = Field( - description="Whether to use SSL/TLS encrypted connection for OpenSearch (True for HTTPS, False for HTTP)", - default=False, + OPENSEARCH_AWS_REGION: Optional[str] = Field( + description="AWS region for OpenSearch (e.g. 'us-west-2')", + default=None, + ) + + OPENSEARCH_AWS_SERVICE: Optional[Literal["es", "aoss"]] = Field( + description="AWS service for OpenSearch (e.g. 'aoss' for OpenSearch Serverless)", default=None ) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index a33c7727dc..c7960e1356 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="1.3.0", + default="1.3.1", ) COMMIT_SHA: str = Field( diff --git a/api/constants/__init__.py b/api/constants/__init__.py index 9162357466..a84de0a451 100644 --- a/api/constants/__init__.py +++ b/api/constants/__init__.py @@ -16,11 +16,25 @@ AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS]) if dify_config.ETL_TYPE == "Unstructured": - DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls"] + DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "vtt", "properties"] DOCUMENT_EXTENSIONS.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub")) if dify_config.UNSTRUCTURED_API_URL: DOCUMENT_EXTENSIONS.append("ppt") DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS]) else: - DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"] + DOCUMENT_EXTENSIONS = [ + "txt", + "markdown", + "md", + "mdx", + "pdf", + "html", + "htm", + "xlsx", + "xls", + "docx", + "csv", + "vtt", + "properties", + ] DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS]) diff --git a/api/controllers/files/image_preview.py b/api/controllers/files/image_preview.py index 3fac8bbbd8..9199069585 100644 --- a/api/controllers/files/image_preview.py +++ b/api/controllers/files/image_preview.py @@ -89,7 +89,7 @@ class FilePreviewApi(Resource): if args["as_attachment"]: encoded_filename = quote(upload_file.name) response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}" - response.headers["Content-Type"] = "application/octet-stream" + response.headers["Content-Type"] = "application/octet-stream" return response diff --git a/api/core/agent/prompt/template.py b/api/core/agent/prompt/template.py index ef64fd29fc..f5ba2119f4 100644 --- a/api/core/agent/prompt/template.py +++ b/api/core/agent/prompt/template.py @@ -1,4 +1,4 @@ -ENGLISH_REACT_COMPLETION_PROMPT_TEMPLATES = """Respond to the human as helpfully and accurately as possible. +ENGLISH_REACT_COMPLETION_PROMPT_TEMPLATES = """Respond to the human as helpfully and accurately as possible. {{instruction}} @@ -47,7 +47,7 @@ Thought:""" # noqa: E501 ENGLISH_REACT_COMPLETION_AGENT_SCRATCHPAD_TEMPLATES = """Observation: {{observation}} Thought:""" -ENGLISH_REACT_CHAT_PROMPT_TEMPLATES = """Respond to the human as helpfully and accurately as possible. +ENGLISH_REACT_CHAT_PROMPT_TEMPLATES = """Respond to the human as helpfully and accurately as possible. {{instruction}} diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index d720eff73d..09e2ee74e6 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -381,6 +381,8 @@ class WorkflowCycleManage: workflow_node_execution.elapsed_time = elapsed_time workflow_node_execution.execution_metadata = execution_metadata + self._workflow_node_execution_repository.update(workflow_node_execution) + return workflow_node_execution def _handle_workflow_node_execution_retried( diff --git a/api/core/external_data_tool/api/__builtin__ b/api/core/external_data_tool/api/__builtin__ index 56a6051ca2..d00491fd7e 100644 --- a/api/core/external_data_tool/api/__builtin__ +++ b/api/core/external_data_tool/api/__builtin__ @@ -1 +1 @@ -1 \ No newline at end of file +1 diff --git a/api/core/helper/code_executor/javascript/javascript_transformer.py b/api/core/helper/code_executor/javascript/javascript_transformer.py index d67a0903aa..62489cdf29 100644 --- a/api/core/helper/code_executor/javascript/javascript_transformer.py +++ b/api/core/helper/code_executor/javascript/javascript_transformer.py @@ -10,13 +10,13 @@ class NodeJsTemplateTransformer(TemplateTransformer): f""" // declare main function {cls._code_placeholder} - + // decode and prepare input object var inputs_obj = JSON.parse(Buffer.from('{cls._inputs_placeholder}', 'base64').toString('utf-8')) - + // execute main function var output_obj = main(inputs_obj) - + // convert output to json and print var output_json = JSON.stringify(output_obj) var result = `<>${{output_json}}<>` diff --git a/api/core/helper/code_executor/jinja2/jinja2_transformer.py b/api/core/helper/code_executor/jinja2/jinja2_transformer.py index 63d58edbc7..54c78cdf92 100644 --- a/api/core/helper/code_executor/jinja2/jinja2_transformer.py +++ b/api/core/helper/code_executor/jinja2/jinja2_transformer.py @@ -21,20 +21,20 @@ class Jinja2TemplateTransformer(TemplateTransformer): import jinja2 template = jinja2.Template('''{cls._code_placeholder}''') return template.render(**inputs) - + import json from base64 import b64decode - + # decode and prepare input dict inputs_obj = json.loads(b64decode('{cls._inputs_placeholder}').decode('utf-8')) - + # execute main function output = main(**inputs_obj) - + # convert output and print result = f'''<>{{output}}<>''' print(result) - + """) return runner_script @@ -43,15 +43,15 @@ class Jinja2TemplateTransformer(TemplateTransformer): preload_script = dedent(""" import jinja2 from base64 import b64decode - + def _jinja2_preload_(): # prepare jinja2 environment, load template and render before to avoid sandbox issue template = jinja2.Template('{{s}}') template.render(s='a') - + if __name__ == '__main__': _jinja2_preload_() - + """) return preload_script diff --git a/api/core/helper/code_executor/python3/python3_transformer.py b/api/core/helper/code_executor/python3/python3_transformer.py index 75a5a44d08..836fd273ae 100644 --- a/api/core/helper/code_executor/python3/python3_transformer.py +++ b/api/core/helper/code_executor/python3/python3_transformer.py @@ -9,16 +9,16 @@ class Python3TemplateTransformer(TemplateTransformer): runner_script = dedent(f""" # declare main function {cls._code_placeholder} - + import json from base64 import b64decode - + # decode and prepare input dict inputs_obj = json.loads(b64decode('{cls._inputs_placeholder}').decode('utf-8')) - + # execute main function output_obj = main(**inputs_obj) - + # convert output to json and print output_json = json.dumps(output_obj, indent=4) result = f'''<>{{output_json}}<>''' diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index d5d2ca60fa..e5dbc30689 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -3,6 +3,8 @@ import logging import re from typing import Optional, cast +import json_repair + from core.llm_generator.output_parser.rule_config_generator import RuleConfigGeneratorOutputParser from core.llm_generator.output_parser.suggested_questions_after_answer import SuggestedQuestionsAfterAnswerOutputParser from core.llm_generator.prompts import ( @@ -366,7 +368,20 @@ class LLMGenerator: ), ) - generated_json_schema = cast(str, response.message.content) + raw_content = response.message.content + + if not isinstance(raw_content, str): + raise ValueError(f"LLM response content must be a string, got: {type(raw_content)}") + + try: + parsed_content = json.loads(raw_content) + except json.JSONDecodeError: + parsed_content = json_repair.loads(raw_content) + + if not isinstance(parsed_content, dict | list): + raise ValueError(f"Failed to parse structured output from llm: {raw_content}") + + generated_json_schema = json.dumps(parsed_content, indent=2, ensure_ascii=False) return {"output": generated_json_schema, "error": ""} except InvokeError as e: diff --git a/api/core/llm_generator/prompts.py b/api/core/llm_generator/prompts.py index fad7cea01c..34ea3aec26 100644 --- a/api/core/llm_generator/prompts.py +++ b/api/core/llm_generator/prompts.py @@ -1,5 +1,5 @@ # Written by YORKI MINAKO🤡, Edited by Xiaoyi -CONVERSATION_TITLE_PROMPT = """You need to decompose the user's input into "subject" and "intention" in order to accurately figure out what the user's input language actually is. +CONVERSATION_TITLE_PROMPT = """You need to decompose the user's input into "subject" and "intention" in order to accurately figure out what the user's input language actually is. Notice: the language type user uses could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc. ENSURE your output is in the SAME language as the user's input! Your output is restricted only to: (Input language) Intention + Subject(short as possible) @@ -58,7 +58,7 @@ User Input: yo, 你今天咋样? "Your Output": "查询今日我的状态☺️" } -User Input: +User Input: """ # noqa: E501 PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE = ( @@ -163,11 +163,11 @@ Here is a task description for which I would like you to create a high-quality p {{TASK_DESCRIPTION}} Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include: -- Do not include or section and variables in the prompt, assume user will add them at their own will. -- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag. -- Relevant examples if needed to clarify the task further, demarcated with tags. Do not include variables in the prompt. Give three pairs of input and output examples. +- Do not include or section and variables in the prompt, assume user will add them at their own will. +- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag. +- Relevant examples if needed to clarify the task further, demarcated with tags. Do not include variables in the prompt. Give three pairs of input and output examples. - Include other relevant sections demarcated with appropriate XML tags like , . -- Use the same language as task description. +- Use the same language as task description. - Output in ``` xml ``` and start with Please generate the full prompt template with at least 300 words and output only the prompt template. """ # noqa: E501 @@ -178,28 +178,28 @@ Here is a task description for which I would like you to create a high-quality p {{TASK_DESCRIPTION}} Based on task description, please create a well-structured prompt template that another AI could use to consistently complete the task. The prompt template should include: -- Descriptive variable names surrounded by {{ }} (two curly brackets) to indicate where the actual values will be substituted in. Choose variable names that clearly indicate the type of value expected. Variable names have to be composed of number, english alphabets and underline and nothing else. -- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag. -- Relevant examples if needed to clarify the task further, demarcated with tags. Do not use curly brackets any other than in section. +- Descriptive variable names surrounded by {{ }} (two curly brackets) to indicate where the actual values will be substituted in. Choose variable names that clearly indicate the type of value expected. Variable names have to be composed of number, english alphabets and underline and nothing else. +- Clear instructions for the AI that will be using this prompt, demarcated with tags. The instructions should provide step-by-step directions on how to complete the task using the input variables. Also Specifies in the instructions that the output should not contain any xml tag. +- Relevant examples if needed to clarify the task further, demarcated with tags. Do not use curly brackets any other than in section. - Any other relevant sections demarcated with appropriate XML tags like , , etc. -- Use the same language as task description. +- Use the same language as task description. - Output in ``` xml ``` and start with Please generate the full prompt template and output only the prompt template. """ # noqa: E501 RULE_CONFIG_PARAMETER_GENERATE_TEMPLATE = """ -I need to extract the following information from the input text. The tag specifies the 'type', 'description' and 'required' of the information to be extracted. +I need to extract the following information from the input text. The tag specifies the 'type', 'description' and 'required' of the information to be extracted. -variables name bounded two double curly brackets. Variable name has to be composed of number, english alphabets and underline and nothing else. +variables name bounded two double curly brackets. Variable name has to be composed of number, english alphabets and underline and nothing else. Step 1: Carefully read the input and understand the structure of the expected output. -Step 2: Extract relevant parameters from the provided text based on the name and description of object. +Step 2: Extract relevant parameters from the provided text based on the name and description of object. Step 3: Structure the extracted parameters to JSON object as specified in . -Step 4: Ensure that the list of variable_names is properly formatted and valid. The output should not contain any XML tags. Output an empty list if there is no valid variable name in input text. +Step 4: Ensure that the list of variable_names is properly formatted and valid. The output should not contain any XML tags. Output an empty list if there is no valid variable name in input text. ### Structure -Here is the structure of the expected output, I should always follow the output structure. +Here is the structure of the expected output, I should always follow the output structure. ["variable_name_1", "variable_name_2"] ### Input Text @@ -214,13 +214,13 @@ I should always output a valid list. Output nothing other than the list of varia RULE_CONFIG_STATEMENT_GENERATE_TEMPLATE = """ -Step 1: Identify the purpose of the chatbot from the variable {{TASK_DESCRIPTION}} and infer chatbot's tone (e.g., friendly, professional, etc.) to add personality traits. +Step 1: Identify the purpose of the chatbot from the variable {{TASK_DESCRIPTION}} and infer chatbot's tone (e.g., friendly, professional, etc.) to add personality traits. Step 2: Create a coherent and engaging opening statement. Step 3: Ensure the output is welcoming and clearly explains what the chatbot is designed to do. Do not include any XML tags in the output. -Please use the same language as the user's input language. If user uses chinese then generate opening statement in chinese, if user uses english then generate opening statement in english. -Example Input: +Please use the same language as the user's input language. If user uses chinese then generate opening statement in chinese, if user uses english then generate opening statement in english. +Example Input: Provide customer support for an e-commerce website -Example Output: +Example Output: Welcome! I'm here to assist you with any questions or issues you might have with your shopping experience. Whether you're looking for product information, need help with your order, or have any other inquiries, feel free to ask. I'm friendly, helpful, and ready to support you in any way I can. Here is the task description: {{INPUT_TEXT}} @@ -276,15 +276,15 @@ Your task is to convert simple user descriptions into properly formatted JSON Sc { "type": "object", "properties": { - "email": { + "email": { "type": "string", "format": "email" }, - "password": { + "password": { "type": "string", "minLength": 8 }, - "age": { + "age": { "type": "integer", "minimum": 18 } diff --git a/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md b/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md index 2d71e99fce..d845c4bd09 100644 --- a/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md +++ b/api/core/model_runtime/docs/en_US/customizable_model_scale_out.md @@ -307,4 +307,4 @@ Runtime Errors: """ ``` -For interface method details, see: [Interfaces](./interfaces.md). For specific implementations, refer to: [llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py). \ No newline at end of file +For interface method details, see: [Interfaces](./interfaces.md). For specific implementations, refer to: [llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py). diff --git a/api/core/model_runtime/docs/en_US/predefined_model_scale_out.md b/api/core/model_runtime/docs/en_US/predefined_model_scale_out.md index 3e16257452..a770ed157b 100644 --- a/api/core/model_runtime/docs/en_US/predefined_model_scale_out.md +++ b/api/core/model_runtime/docs/en_US/predefined_model_scale_out.md @@ -170,4 +170,4 @@ Runtime Errors: """ ``` -For interface method explanations, see: [Interfaces](./interfaces.md). For detailed implementation, refer to: [llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py). \ No newline at end of file +For interface method explanations, see: [Interfaces](./interfaces.md). For detailed implementation, refer to: [llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py). diff --git a/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md b/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md index 88ec6861fe..7d30655469 100644 --- a/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md +++ b/api/core/model_runtime/docs/zh_Hans/customizable_model_scale_out.md @@ -294,4 +294,4 @@ provider_credential_schema: """ ``` -接口方法说明见:[Interfaces](./interfaces.md),具体实现可参考:[llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py)。 \ No newline at end of file +接口方法说明见:[Interfaces](./interfaces.md),具体实现可参考:[llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py)。 diff --git a/api/core/model_runtime/docs/zh_Hans/predefined_model_scale_out.md b/api/core/model_runtime/docs/zh_Hans/predefined_model_scale_out.md index b33dc7c94b..80e7982e9f 100644 --- a/api/core/model_runtime/docs/zh_Hans/predefined_model_scale_out.md +++ b/api/core/model_runtime/docs/zh_Hans/predefined_model_scale_out.md @@ -169,4 +169,4 @@ pricing: # 价格信息 """ ``` -接口方法说明见:[Interfaces](./interfaces.md),具体实现可参考:[llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py)。 \ No newline at end of file +接口方法说明见:[Interfaces](./interfaces.md),具体实现可参考:[llm.py](https://github.com/langgenius/dify-runtime/blob/main/lib/model_providers/anthropic/llm/llm.py)。 diff --git a/api/core/moderation/api/__builtin__ b/api/core/moderation/api/__builtin__ index e440e5c842..00750edc07 100644 --- a/api/core/moderation/api/__builtin__ +++ b/api/core/moderation/api/__builtin__ @@ -1 +1 @@ -3 \ No newline at end of file +3 diff --git a/api/core/moderation/keywords/__builtin__ b/api/core/moderation/keywords/__builtin__ index d8263ee986..0cfbf08886 100644 --- a/api/core/moderation/keywords/__builtin__ +++ b/api/core/moderation/keywords/__builtin__ @@ -1 +1 @@ -2 \ No newline at end of file +2 diff --git a/api/core/moderation/openai_moderation/__builtin__ b/api/core/moderation/openai_moderation/__builtin__ index 56a6051ca2..d00491fd7e 100644 --- a/api/core/moderation/openai_moderation/__builtin__ +++ b/api/core/moderation/openai_moderation/__builtin__ @@ -1 +1 @@ -1 \ No newline at end of file +1 diff --git a/api/core/plugin/backwards_invocation/app.py b/api/core/plugin/backwards_invocation/app.py index 484f52e33c..4e43561a15 100644 --- a/api/core/plugin/backwards_invocation/app.py +++ b/api/core/plugin/backwards_invocation/app.py @@ -72,7 +72,7 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation): raise ValueError("missing query") return cls.invoke_chat_app(app, user, conversation_id, query, stream, inputs, files) - elif app.mode == AppMode.WORKFLOW.value: + elif app.mode == AppMode.WORKFLOW: return cls.invoke_workflow_app(app, user, stream, inputs, files) elif app.mode == AppMode.COMPLETION: return cls.invoke_completion_app(app, user, stream, inputs, files) diff --git a/api/core/plugin/backwards_invocation/model.py b/api/core/plugin/backwards_invocation/model.py index 490a475c16..5ec9620f22 100644 --- a/api/core/plugin/backwards_invocation/model.py +++ b/api/core/plugin/backwards_invocation/model.py @@ -239,8 +239,8 @@ class PluginModelBackwardsInvocation(BaseBackwardsInvocation): content = payload.text SUMMARY_PROMPT = """You are a professional language researcher, you are interested in the language -and you can quickly aimed at the main point of an webpage and reproduce it in your own words but -retain the original meaning and keep the key points. +and you can quickly aimed at the main point of an webpage and reproduce it in your own words but +retain the original meaning and keep the key points. however, the text you got is too long, what you got is possible a part of the text. Please summarize the text you got. diff --git a/api/core/prompt/prompt_templates/baichuan_chat.json b/api/core/prompt/prompt_templates/baichuan_chat.json index 03b6a53cff..b3f7cdaa18 100644 --- a/api/core/prompt/prompt_templates/baichuan_chat.json +++ b/api/core/prompt/prompt_templates/baichuan_chat.json @@ -10,4 +10,4 @@ ], "query_prompt": "\n\n用户:{{#query#}}", "stops": ["用户:"] -} \ No newline at end of file +} diff --git a/api/core/prompt/prompt_templates/baichuan_completion.json b/api/core/prompt/prompt_templates/baichuan_completion.json index ae8c0dac53..cee9ea47cd 100644 --- a/api/core/prompt/prompt_templates/baichuan_completion.json +++ b/api/core/prompt/prompt_templates/baichuan_completion.json @@ -6,4 +6,4 @@ ], "query_prompt": "{{#query#}}", "stops": null -} \ No newline at end of file +} diff --git a/api/core/prompt/prompt_templates/common_completion.json b/api/core/prompt/prompt_templates/common_completion.json index c148772010..706a8140d1 100644 --- a/api/core/prompt/prompt_templates/common_completion.json +++ b/api/core/prompt/prompt_templates/common_completion.json @@ -6,4 +6,4 @@ ], "query_prompt": "{{#query#}}", "stops": null -} \ No newline at end of file +} diff --git a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_sql.py b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_sql.py index c1792943bb..14481b1f10 100644 --- a/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_sql.py +++ b/api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_sql.py @@ -156,8 +156,8 @@ class AnalyticdbVectorBySql: values = [] id_prefix = str(uuid.uuid4()) + "_" sql = f""" - INSERT INTO {self.table_name} - (id, ref_doc_id, vector, page_content, metadata_, to_tsvector) + INSERT INTO {self.table_name} + (id, ref_doc_id, vector, page_content, metadata_, to_tsvector) VALUES (%s, %s, %s, %s, %s, to_tsvector('zh_cn', %s)); """ for i, doc in enumerate(documents): @@ -242,7 +242,7 @@ class AnalyticdbVectorBySql: where_clause += f"AND metadata_->>'document_id' IN ({document_ids})" with self._get_cursor() as cur: cur.execute( - f"""SELECT id, vector, page_content, metadata_, + f"""SELECT id, vector, page_content, metadata_, ts_rank(to_tsvector, to_tsquery_from_text(%s, 'zh_cn'), 32) AS score FROM {self.table_name} WHERE to_tsvector@@to_tsquery_from_text(%s, 'zh_cn') {where_clause} diff --git a/api/core/rag/datasource/vdb/milvus/milvus_vector.py b/api/core/rag/datasource/vdb/milvus/milvus_vector.py index 100bcb198c..7b3f826082 100644 --- a/api/core/rag/datasource/vdb/milvus/milvus_vector.py +++ b/api/core/rag/datasource/vdb/milvus/milvus_vector.py @@ -27,8 +27,8 @@ class MilvusConfig(BaseModel): uri: str # Milvus server URI token: Optional[str] = None # Optional token for authentication - user: str # Username for authentication - password: str # Password for authentication + user: Optional[str] = None # Username for authentication + password: Optional[str] = None # Password for authentication batch_size: int = 100 # Batch size for operations database: str = "default" # Database name enable_hybrid_search: bool = False # Flag to enable hybrid search @@ -43,10 +43,11 @@ class MilvusConfig(BaseModel): """ if not values.get("uri"): raise ValueError("config MILVUS_URI is required") - if not values.get("user"): - raise ValueError("config MILVUS_USER is required") - if not values.get("password"): - raise ValueError("config MILVUS_PASSWORD is required") + if not values.get("token"): + if not values.get("user"): + raise ValueError("config MILVUS_USER is required") + if not values.get("password"): + raise ValueError("config MILVUS_PASSWORD is required") return values def to_milvus_params(self): @@ -356,11 +357,14 @@ class MilvusVector(BaseVector): ) redis_client.set(collection_exist_cache_key, 1, ex=3600) - def _init_client(self, config) -> MilvusClient: + def _init_client(self, config: MilvusConfig) -> MilvusClient: """ Initialize and return a Milvus client. """ - client = MilvusClient(uri=config.uri, user=config.user, password=config.password, db_name=config.database) + if config.token: + client = MilvusClient(uri=config.uri, token=config.token, db_name=config.database) + else: + client = MilvusClient(uri=config.uri, user=config.user, password=config.password, db_name=config.database) return client diff --git a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py index ae6b0c51ab..2b47d179d2 100644 --- a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py +++ b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py @@ -203,7 +203,7 @@ class OceanBaseVector(BaseVector): full_sql = f"""SELECT metadata, text, MATCH (text) AGAINST (:query) AS score FROM {self._collection_name} - WHERE MATCH (text) AGAINST (:query) > 0 + WHERE MATCH (text) AGAINST (:query) > 0 {where_clause} ORDER BY score DESC LIMIT {top_k}""" diff --git a/api/core/rag/datasource/vdb/opengauss/opengauss.py b/api/core/rag/datasource/vdb/opengauss/opengauss.py index dae908f67d..2548881b9c 100644 --- a/api/core/rag/datasource/vdb/opengauss/opengauss.py +++ b/api/core/rag/datasource/vdb/opengauss/opengauss.py @@ -59,12 +59,12 @@ CREATE TABLE IF NOT EXISTS {table_name} ( """ SQL_CREATE_INDEX_PQ = """ -CREATE INDEX IF NOT EXISTS embedding_{table_name}_pq_idx ON {table_name} +CREATE INDEX IF NOT EXISTS embedding_{table_name}_pq_idx ON {table_name} USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64, enable_pq=on, pq_m={pq_m}); """ SQL_CREATE_INDEX = """ -CREATE INDEX IF NOT EXISTS embedding_cosine_{table_name}_idx ON {table_name} +CREATE INDEX IF NOT EXISTS embedding_cosine_{table_name}_idx ON {table_name} USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); """ diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py index 6636646cff..e23b8d197f 100644 --- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py +++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py @@ -1,10 +1,9 @@ import json import logging -import ssl -from typing import Any, Optional +from typing import Any, Literal, Optional from uuid import uuid4 -from opensearchpy import OpenSearch, helpers +from opensearchpy import OpenSearch, Urllib3AWSV4SignerAuth, Urllib3HttpConnection, helpers from opensearchpy.helpers import BulkIndexError from pydantic import BaseModel, model_validator @@ -24,9 +23,12 @@ logger = logging.getLogger(__name__) class OpenSearchConfig(BaseModel): host: str port: int + secure: bool = False + auth_method: Literal["basic", "aws_managed_iam"] = "basic" user: Optional[str] = None password: Optional[str] = None - secure: bool = False + aws_region: Optional[str] = None + aws_service: Optional[str] = None @model_validator(mode="before") @classmethod @@ -35,24 +37,40 @@ class OpenSearchConfig(BaseModel): raise ValueError("config OPENSEARCH_HOST is required") if not values.get("port"): raise ValueError("config OPENSEARCH_PORT is required") + if values.get("auth_method") == "aws_managed_iam": + if not values.get("aws_region"): + raise ValueError("config OPENSEARCH_AWS_REGION is required for AWS_MANAGED_IAM auth method") + if not values.get("aws_service"): + raise ValueError("config OPENSEARCH_AWS_SERVICE is required for AWS_MANAGED_IAM auth method") return values - def create_ssl_context(self) -> ssl.SSLContext: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Disable Certificate Validation - return ssl_context + def create_aws_managed_iam_auth(self) -> Urllib3AWSV4SignerAuth: + import boto3 # type: ignore + + return Urllib3AWSV4SignerAuth( + credentials=boto3.Session().get_credentials(), + region=self.aws_region, + service=self.aws_service, # type: ignore[arg-type] + ) def to_opensearch_params(self) -> dict[str, Any]: params = { "hosts": [{"host": self.host, "port": self.port}], "use_ssl": self.secure, "verify_certs": self.secure, + "connection_class": Urllib3HttpConnection, + "pool_maxsize": 20, } - if self.user and self.password: + + if self.auth_method == "basic": + logger.info("Using basic authentication for OpenSearch Vector DB") + params["http_auth"] = (self.user, self.password) - if self.secure: - params["ssl_context"] = self.create_ssl_context() + elif self.auth_method == "aws_managed_iam": + logger.info("Using AWS managed IAM role for OpenSearch Vector DB") + + params["http_auth"] = self.create_aws_managed_iam_auth() + return params @@ -76,16 +94,23 @@ class OpenSearchVector(BaseVector): action = { "_op_type": "index", "_index": self._collection_name.lower(), - "_id": uuid4().hex, "_source": { Field.CONTENT_KEY.value: documents[i].page_content, Field.VECTOR.value: embeddings[i], # Make sure you pass an array here Field.METADATA_KEY.value: documents[i].metadata, }, } + # See https://github.com/langchain-ai/langchainjs/issues/4346#issuecomment-1935123377 + if self._client_config.aws_service not in ["aoss"]: + action["_id"] = uuid4().hex actions.append(action) - helpers.bulk(self._client, actions) + helpers.bulk( + client=self._client, + actions=actions, + timeout=30, + max_retries=3, + ) def get_ids_by_metadata_field(self, key: str, value: str): query = {"query": {"term": {f"{Field.METADATA_KEY.value}.{key}": value}}} @@ -234,6 +259,7 @@ class OpenSearchVector(BaseVector): }, } + logger.info(f"Creating OpenSearch index {self._collection_name.lower()}") self._client.indices.create(index=self._collection_name.lower(), body=index_body) redis_client.set(collection_exist_cache_key, 1, ex=3600) @@ -252,9 +278,12 @@ class OpenSearchVectorFactory(AbstractVectorFactory): open_search_config = OpenSearchConfig( host=dify_config.OPENSEARCH_HOST or "localhost", port=dify_config.OPENSEARCH_PORT, + secure=dify_config.OPENSEARCH_SECURE, + auth_method=dify_config.OPENSEARCH_AUTH_METHOD.value, user=dify_config.OPENSEARCH_USER, password=dify_config.OPENSEARCH_PASSWORD, - secure=dify_config.OPENSEARCH_SECURE, + aws_region=dify_config.OPENSEARCH_AWS_REGION, + aws_service=dify_config.OPENSEARCH_AWS_SERVICE, ) return OpenSearchVector(collection_name=collection_name, config=open_search_config) diff --git a/api/core/rag/datasource/vdb/oracle/oraclevector.py b/api/core/rag/datasource/vdb/oracle/oraclevector.py index 63695e6f3f..0a3738ac93 100644 --- a/api/core/rag/datasource/vdb/oracle/oraclevector.py +++ b/api/core/rag/datasource/vdb/oracle/oraclevector.py @@ -59,8 +59,8 @@ CREATE TABLE IF NOT EXISTS {table_name} ( ) """ SQL_CREATE_INDEX = """ -CREATE INDEX IF NOT EXISTS idx_docs_{table_name} ON {table_name}(text) -INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS +CREATE INDEX IF NOT EXISTS idx_docs_{table_name} ON {table_name}(text) +INDEXTYPE IS CTXSYS.CONTEXT PARAMETERS ('FILTER CTXSYS.NULL_FILTER SECTION GROUP CTXSYS.HTML_SECTION_GROUP LEXER world_lexer') """ @@ -164,7 +164,7 @@ class OracleVector(BaseVector): with conn.cursor() as cur: try: cur.execute( - f"""INSERT INTO {self.table_name} (id, text, meta, embedding) + f"""INSERT INTO {self.table_name} (id, text, meta, embedding) VALUES (:1, :2, :3, :4)""", value, ) @@ -227,8 +227,8 @@ class OracleVector(BaseVector): conn.outputtypehandler = self.output_type_handler with conn.cursor() as cur: cur.execute( - f"""SELECT meta, text, vector_distance(embedding,(select to_vector(:1) from dual),cosine) - AS distance FROM {self.table_name} + f"""SELECT meta, text, vector_distance(embedding,(select to_vector(:1) from dual),cosine) + AS distance FROM {self.table_name} {where_clause} ORDER BY distance fetch first {top_k} rows only""", [numpy.array(query_vector)], ) @@ -290,7 +290,7 @@ class OracleVector(BaseVector): document_ids = ", ".join(f"'{id}'" for id in document_ids_filter) where_clause = f" AND metadata->>'document_id' in ({document_ids}) " cur.execute( - f"""select meta, text, embedding FROM {self.table_name} + f"""select meta, text, embedding FROM {self.table_name} WHERE CONTAINS(text, :kk, 1) > 0 {where_clause} order by score(1) desc fetch first {top_k} rows only""", kk=" ACCUM ".join(entities), diff --git a/api/core/rag/datasource/vdb/pgvector/pgvector.py b/api/core/rag/datasource/vdb/pgvector/pgvector.py index eab51ab01d..366a21c381 100644 --- a/api/core/rag/datasource/vdb/pgvector/pgvector.py +++ b/api/core/rag/datasource/vdb/pgvector/pgvector.py @@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( """ SQL_CREATE_INDEX = """ -CREATE INDEX IF NOT EXISTS embedding_cosine_v1_idx ON {table_name} +CREATE INDEX IF NOT EXISTS embedding_cosine_v1_idx ON {table_name} USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); """ diff --git a/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py b/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py index a61d571e16..156730ff37 100644 --- a/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py +++ b/api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py @@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS {table_name} ( """ SQL_CREATE_INDEX = """ -CREATE INDEX IF NOT EXISTS embedding_cosine_v1_idx ON {table_name} +CREATE INDEX IF NOT EXISTS embedding_cosine_v1_idx ON {table_name} USING hnsw (embedding floatvector_cosine_ops) WITH (m = 16, ef_construction = 64); """ diff --git a/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py b/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py index 00229ce700..61c68b939e 100644 --- a/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py +++ b/api/core/rag/datasource/vdb/tidb_vector/tidb_vector.py @@ -205,9 +205,9 @@ class TiDBVector(BaseVector): with Session(self._engine) as session: select_statement = sql_text(f""" - SELECT meta, text, distance + SELECT meta, text, distance FROM ( - SELECT + SELECT meta, text, {tidb_dist_func}(vector, :query_vector_str) AS distance diff --git a/api/core/rag/rerank/rerank_model.py b/api/core/rag/rerank/rerank_model.py index ac7a3f8bb8..693535413a 100644 --- a/api/core/rag/rerank/rerank_model.py +++ b/api/core/rag/rerank/rerank_model.py @@ -52,14 +52,16 @@ class RerankModelRunner(BaseRerankRunner): rerank_documents = [] for result in rerank_result.docs: - # format document - rerank_document = Document( - page_content=result.text, - metadata=documents[result.index].metadata, - provider=documents[result.index].provider, - ) - if rerank_document.metadata is not None: - rerank_document.metadata["score"] = result.score - rerank_documents.append(rerank_document) + if score_threshold is None or result.score >= score_threshold: + # format document + rerank_document = Document( + page_content=result.text, + metadata=documents[result.index].metadata, + provider=documents[result.index].provider, + ) + if rerank_document.metadata is not None: + rerank_document.metadata["score"] = result.score + rerank_documents.append(rerank_document) - return rerank_documents + rerank_documents.sort(key=lambda x: x.metadata.get("score", 0.0), reverse=True) + return rerank_documents[:top_n] if top_n else rerank_documents diff --git a/api/core/rag/retrieval/template_prompts.py b/api/core/rag/retrieval/template_prompts.py index 7abd55d798..acde842702 100644 --- a/api/core/rag/retrieval/template_prompts.py +++ b/api/core/rag/retrieval/template_prompts.py @@ -50,7 +50,7 @@ You are a text metadata extract engine that extract text's metadata based on use # Your task is to ONLY extract the metadatas that exist in the input text from the provided metadata list and Use the following operators ["=", "!=", ">", "<", ">=", "<="] to express logical relationships, then return result in JSON format with the key "metadata_fields" and value "metadata_field_value" and comparison operator "comparison_operator". ### Format The input text is in the variable input_text. Metadata are specified as a list in the variable metadata_fields. -### Constraint +### Constraint DO NOT include anything other than the JSON array in your response. ### Example Here is the chat example between human and assistant, inside XML tags. @@ -59,7 +59,7 @@ User:{{"input_text": ["I want to know which company’s email address test@examp Assistant:{{"metadata_map": [{{"metadata_field_name": "email", "metadata_field_value": "test@example.com", "comparison_operator": "="}}]}} User:{{"input_text": "What are the movies with a score of more than 9 in 2024?", "metadata_fields": ["name", "year", "rating", "country"]}} Assistant:{{"metadata_map": [{{"metadata_field_name": "year", "metadata_field_value": "2024", "comparison_operator": "="}, {{"metadata_field_name": "rating", "metadata_field_value": "9", "comparison_operator": ">"}}]}} - + ### User Input {{"input_text" : "{input_text}", "metadata_fields" : {metadata_fields}}} ### Assistant Output diff --git a/api/core/tools/builtin_tool/tool.py b/api/core/tools/builtin_tool/tool.py index 7f37f98d0c..724a2291c6 100644 --- a/api/core/tools/builtin_tool/tool.py +++ b/api/core/tools/builtin_tool/tool.py @@ -6,8 +6,8 @@ from core.tools.entities.tool_entities import ToolProviderType from core.tools.utils.model_invocation_utils import ModelInvocationUtils _SUMMARY_PROMPT = """You are a professional language researcher, you are interested in the language -and you can quickly aimed at the main point of an webpage and reproduce it in your own words but -retain the original meaning and keep the key points. +and you can quickly aimed at the main point of an webpage and reproduce it in your own words but +retain the original meaning and keep the key points. however, the text you got is too long, what you got is possible a part of the text. Please summarize the text you got. """ diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 997917f31c..3dce1ca293 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -246,7 +246,7 @@ class ToolEngine: + "you do not need to create it, just tell the user to check it now." ) elif response.type == ToolInvokeMessage.MessageType.JSON: - result = json.dumps( + result += json.dumps( cast(ToolInvokeMessage.JsonMessage, response.message).json_object, ensure_ascii=False ) else: diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index 960d0c3961..8fb1baec89 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -11,6 +11,7 @@ import docx import pandas as pd import pypandoc # type: ignore import pypdfium2 # type: ignore +import webvtt # type: ignore import yaml # type: ignore from docx.document import Document from docx.oxml.table import CT_Tbl @@ -132,6 +133,10 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str: return _extract_text_from_json(file_content) case "application/x-yaml" | "text/yaml": return _extract_text_from_yaml(file_content) + case "text/vtt": + return _extract_text_from_vtt(file_content) + case "text/properties": + return _extract_text_from_properties(file_content) case _: raise UnsupportedFileTypeError(f"Unsupported MIME type: {mime_type}") @@ -139,7 +144,7 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str: def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) -> str: """Extract text from a file based on its file extension.""" match file_extension: - case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml" | ".vtt": + case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml": return _extract_text_from_plain_text(file_content) case ".json": return _extract_text_from_json(file_content) @@ -165,6 +170,10 @@ def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) return _extract_text_from_eml(file_content) case ".msg": return _extract_text_from_msg(file_content) + case ".vtt": + return _extract_text_from_vtt(file_content) + case ".properties": + return _extract_text_from_properties(file_content) case _: raise UnsupportedFileTypeError(f"Unsupported Extension Type: {file_extension}") @@ -214,8 +223,8 @@ def _extract_text_from_doc(file_content: bytes) -> str: """ from unstructured.partition.api import partition_via_api - if not (dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY): - raise TextExtractionError("UNSTRUCTURED_API_URL and UNSTRUCTURED_API_KEY must be set") + if not dify_config.UNSTRUCTURED_API_URL: + raise TextExtractionError("UNSTRUCTURED_API_URL must be set") try: with tempfile.NamedTemporaryFile(suffix=".doc", delete=False) as temp_file: @@ -226,7 +235,7 @@ def _extract_text_from_doc(file_content: bytes) -> str: file=file, metadata_filename=temp_file.name, api_url=dify_config.UNSTRUCTURED_API_URL, - api_key=dify_config.UNSTRUCTURED_API_KEY, + api_key=dify_config.UNSTRUCTURED_API_KEY, # type: ignore ) os.unlink(temp_file.name) return "\n".join([getattr(element, "text", "") for element in elements]) @@ -462,3 +471,68 @@ def _extract_text_from_msg(file_content: bytes) -> str: return "\n".join([str(element) for element in elements]) except Exception as e: raise TextExtractionError(f"Failed to extract text from MSG: {str(e)}") from e + + +def _extract_text_from_vtt(vtt_bytes: bytes) -> str: + text = _extract_text_from_plain_text(vtt_bytes) + + # remove bom + text = text.lstrip("\ufeff") + + raw_results = [] + for caption in webvtt.from_string(text): + raw_results.append((caption.voice, caption.text)) + + # Merge consecutive utterances by the same speaker + merged_results = [] + if raw_results: + current_speaker, current_text = raw_results[0] + + for i in range(1, len(raw_results)): + spk, txt = raw_results[i] + if spk == None: + merged_results.append((None, current_text)) + continue + + if spk == current_speaker: + # If it is the same speaker, merge the utterances (joined by space) + current_text += " " + txt + else: + # If the speaker changes, register the utterance so far and move on + merged_results.append((current_speaker, current_text)) + current_speaker, current_text = spk, txt + + # Add the last element + merged_results.append((current_speaker, current_text)) + else: + merged_results = raw_results + + # Return the result in the specified format: Speaker "text" style + formatted = [f'{spk or ""} "{txt}"' for spk, txt in merged_results] + return "\n".join(formatted) + + +def _extract_text_from_properties(file_content: bytes) -> str: + try: + text = _extract_text_from_plain_text(file_content) + lines = text.splitlines() + result = [] + for line in lines: + line = line.strip() + # Preserve comments and empty lines + if not line or line.startswith("#") or line.startswith("!"): + result.append(line) + continue + + if "=" in line: + key, value = line.split("=", 1) + elif ":" in line: + key, value = line.split(":", 1) + else: + key, value = line, "" + + result.append(f"{key.strip()}: {value.strip()}") + + return "\n".join(result) + except Exception as e: + raise TextExtractionError(f"Failed to extract text from properties file: {str(e)}") from e diff --git a/api/core/workflow/nodes/knowledge_retrieval/template_prompts.py b/api/core/workflow/nodes/knowledge_retrieval/template_prompts.py index 7abd55d798..acde842702 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/template_prompts.py +++ b/api/core/workflow/nodes/knowledge_retrieval/template_prompts.py @@ -50,7 +50,7 @@ You are a text metadata extract engine that extract text's metadata based on use # Your task is to ONLY extract the metadatas that exist in the input text from the provided metadata list and Use the following operators ["=", "!=", ">", "<", ">=", "<="] to express logical relationships, then return result in JSON format with the key "metadata_fields" and value "metadata_field_value" and comparison operator "comparison_operator". ### Format The input text is in the variable input_text. Metadata are specified as a list in the variable metadata_fields. -### Constraint +### Constraint DO NOT include anything other than the JSON array in your response. ### Example Here is the chat example between human and assistant, inside XML tags. @@ -59,7 +59,7 @@ User:{{"input_text": ["I want to know which company’s email address test@examp Assistant:{{"metadata_map": [{{"metadata_field_name": "email", "metadata_field_value": "test@example.com", "comparison_operator": "="}}]}} User:{{"input_text": "What are the movies with a score of more than 9 in 2024?", "metadata_fields": ["name", "year", "rating", "country"]}} Assistant:{{"metadata_map": [{{"metadata_field_name": "year", "metadata_field_value": "2024", "comparison_operator": "="}, {{"metadata_field_name": "rating", "metadata_field_value": "9", "comparison_operator": ">"}}]}} - + ### User Input {{"input_text" : "{input_text}", "metadata_fields" : {metadata_fields}}} ### Assistant Output diff --git a/api/core/workflow/nodes/parameter_extractor/prompts.py b/api/core/workflow/nodes/parameter_extractor/prompts.py index 6c3155ac9a..ab7ddcc32a 100644 --- a/api/core/workflow/nodes/parameter_extractor/prompts.py +++ b/api/core/workflow/nodes/parameter_extractor/prompts.py @@ -17,7 +17,7 @@ Some additional information is provided below. Always adhere to these instructio Steps: 1. Review the chat history provided within the tags. -2. Extract the relevant information based on the criteria given, output multiple values if there is multiple relevant information that match the criteria in the given text. +2. Extract the relevant information based on the criteria given, output multiple values if there is multiple relevant information that match the criteria in the given text. 3. Generate a well-formatted output using the defined functions and arguments. 4. Use the `extract_parameter` function to create structured outputs with appropriate parameters. 5. Do not include any XML tags in your output. @@ -89,13 +89,13 @@ Some extra information are provided below, I should always follow the instructio ### Extract parameter Workflow -I need to extract the following information from the input text. The tag specifies the 'type', 'description' and 'required' of the information to be extracted. +I need to extract the following information from the input text. The tag specifies the 'type', 'description' and 'required' of the information to be extracted. {{ structure }} Step 1: Carefully read the input and understand the structure of the expected output. -Step 2: Extract relevant parameters from the provided text based on the name and description of object. +Step 2: Extract relevant parameters from the provided text based on the name and description of object. Step 3: Structure the extracted parameters to JSON object as specified in . Step 4: Ensure that the JSON object is properly formatted and valid. The output should not contain any XML tags. Only the JSON object should be outputted. @@ -106,10 +106,10 @@ Here are the chat histories between human and assistant, inside ### Structure -Here is the structure of the expected output, I should always follow the output structure. +Here is the structure of the expected output, I should always follow the output structure. {{γγγ - 'properties1': 'relevant text extracted from input', - 'properties2': 'relevant text extracted from input', + 'properties1': 'relevant text extracted from input', + 'properties2': 'relevant text extracted from input', }}γγγ ### Input Text @@ -119,7 +119,7 @@ Inside XML tags, there is a text that I should extract parameters ### Answer -I should always output a valid JSON object. Output nothing other than the JSON object. +I should always output a valid JSON object. Output nothing other than the JSON object. ```JSON """ # noqa: E501 diff --git a/api/core/workflow/nodes/question_classifier/template_prompts.py b/api/core/workflow/nodes/question_classifier/template_prompts.py index 70178ed934..a615c32383 100644 --- a/api/core/workflow/nodes/question_classifier/template_prompts.py +++ b/api/core/workflow/nodes/question_classifier/template_prompts.py @@ -55,7 +55,7 @@ You are a text classification engine that analyzes text data and assigns categor Your task is to assign one categories ONLY to the input text and only one category may be assigned returned in the output. Additionally, you need to extract the key words from the text that are related to the classification. ### Format The input text is in the variable input_text. Categories are specified as a category list with two filed category_id and category_name in the variable categories. Classification instructions may be included to improve the classification accuracy. -### Constraint +### Constraint DO NOT include anything other than the JSON array in your response. ### Example Here is the chat example between human and assistant, inside XML tags. @@ -64,7 +64,7 @@ User:{{"input_text": ["I recently had a great experience with your company. The Assistant:{{"keywords": ["recently", "great experience", "company", "service", "prompt", "staff", "friendly"],"category_id": "f5660049-284f-41a7-b301-fd24176a711c","category_name": "Customer Service"}} User:{{"input_text": ["bad service, slow to bring the food"], "categories": [{{"category_id":"80fb86a0-4454-4bf5-924c-f253fdd83c02","category_name":"Food Quality"}},{{"category_id":"f6ff5bc3-aca0-4e4a-8627-e760d0aca78f","category_name":"Experience"}},{{"category_id":"cc771f63-74e7-4c61-882e-3eda9d8ba5d7","category_name":"Price"}}], "classification_instructions": []}} Assistant:{{"keywords": ["bad service", "slow", "food", "tip", "terrible", "waitresses"],"category_id": "f6ff5bc3-aca0-4e4a-8627-e760d0aca78f","category_name": "Experience"}} - + ### Memory Here are the chat histories between human and assistant, inside XML tags. diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index 50118a401c..7648947fca 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -9,6 +9,7 @@ from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import InvokeFrom from core.file.models import File from core.workflow.callbacks import WorkflowCallback +from core.workflow.constants import ENVIRONMENT_VARIABLE_NODE_ID from core.workflow.entities.variable_pool import VariablePool from core.workflow.errors import WorkflowNodeRunFailedError from core.workflow.graph_engine.entities.event import GraphEngineEvent, GraphRunFailedEvent, InNodeEvent @@ -364,4 +365,5 @@ class WorkflowEntry: input_value = file_factory.build_from_mappings(mappings=input_value, tenant_id=tenant_id) # append variable and value to variable pool - variable_pool.add([variable_node_id] + variable_key_list, input_value) + if variable_node_id != ENVIRONMENT_VARIABLE_NODE_ID: + variable_pool.add([variable_node_id] + variable_key_list, input_value) diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 68f3c65a4b..18d4f4885d 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -20,7 +20,8 @@ if [[ "${MODE}" == "worker" ]]; then CONCURRENCY_OPTION="-c ${CELERY_WORKER_AMOUNT:-1}" fi - exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION --loglevel ${LOG_LEVEL:-INFO} \ + exec celery -A app.celery worker -P ${CELERY_WORKER_CLASS:-gevent} $CONCURRENCY_OPTION \ + --max-tasks-per-child ${MAX_TASK_PRE_CHILD:-50} --loglevel ${LOG_LEVEL:-INFO} \ -Q ${CELERY_QUEUES:-dataset,mail,ops_trace,app_deletion} elif [[ "${MODE}" == "beat" ]]; then diff --git a/api/extensions/ext_otel.py b/api/extensions/ext_otel.py index 29fa46902e..be47fdc6d6 100644 --- a/api/extensions/ext_otel.py +++ b/api/extensions/ext_otel.py @@ -8,192 +8,197 @@ from typing import Union from celery.signals import worker_init # type: ignore from flask_login import user_loaded_from_request, user_logged_in # type: ignore -from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter -from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter -from opentelemetry.instrumentation.celery import CeleryInstrumentor -from opentelemetry.instrumentation.flask import FlaskInstrumentor -from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor -from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider -from opentelemetry.propagate import set_global_textmap -from opentelemetry.propagators.b3 import B3Format -from opentelemetry.propagators.composite import CompositePropagator -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import ( - BatchSpanProcessor, - ConsoleSpanExporter, -) -from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio -from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.trace import Span, get_current_span, get_tracer_provider, set_tracer_provider -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from opentelemetry.trace.status import StatusCode from configs import dify_config from dify_app import DifyApp -class ExceptionLoggingHandler(logging.Handler): - """Custom logging handler that creates spans for logging.exception() calls""" - - def emit(self, record): - try: - if record.exc_info: - tracer = get_tracer_provider().get_tracer("dify.exception.logging") - with tracer.start_as_current_span( - "log.exception", - attributes={ - "log.level": record.levelname, - "log.message": record.getMessage(), - "log.logger": record.name, - "log.file.path": record.pathname, - "log.file.line": record.lineno, - }, - ) as span: - span.set_status(StatusCode.ERROR) - span.record_exception(record.exc_info[1]) - span.set_attribute("exception.type", record.exc_info[0].__name__) - span.set_attribute("exception.message", str(record.exc_info[1])) - except Exception: - pass - - @user_logged_in.connect @user_loaded_from_request.connect def on_user_loaded(_sender, user): - if user: - current_span = get_current_span() - if current_span: - current_span.set_attribute("service.tenant.id", user.current_tenant_id) - current_span.set_attribute("service.user.id", user.id) - - -def init_app(app: DifyApp): if dify_config.ENABLE_OTEL: - setup_context_propagation() - # Initialize OpenTelemetry - # Follow Semantic Convertions 1.32.0 to define resource attributes - resource = Resource( - attributes={ - ResourceAttributes.SERVICE_NAME: dify_config.APPLICATION_NAME, - ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}", - ResourceAttributes.PROCESS_PID: os.getpid(), - ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", - ResourceAttributes.HOST_NAME: socket.gethostname(), - ResourceAttributes.HOST_ARCH: platform.machine(), - "custom.deployment.git_commit": dify_config.COMMIT_SHA, - ResourceAttributes.HOST_ID: platform.node(), - ResourceAttributes.OS_TYPE: platform.system().lower(), - ResourceAttributes.OS_DESCRIPTION: platform.platform(), - ResourceAttributes.OS_VERSION: platform.version(), - } - ) - sampler = ParentBasedTraceIdRatio(dify_config.OTEL_SAMPLING_RATE) - provider = TracerProvider(resource=resource, sampler=sampler) - set_tracer_provider(provider) - exporter: Union[OTLPSpanExporter, ConsoleSpanExporter] - metric_exporter: Union[OTLPMetricExporter, ConsoleMetricExporter] - if dify_config.OTEL_EXPORTER_TYPE == "otlp": - exporter = OTLPSpanExporter( - endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/traces", - headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"}, - ) - metric_exporter = OTLPMetricExporter( - endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/metrics", - headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"}, - ) - else: - # Fallback to console exporter - exporter = ConsoleSpanExporter() - metric_exporter = ConsoleMetricExporter() - - provider.add_span_processor( - BatchSpanProcessor( - exporter, - max_queue_size=dify_config.OTEL_MAX_QUEUE_SIZE, - schedule_delay_millis=dify_config.OTEL_BATCH_EXPORT_SCHEDULE_DELAY, - max_export_batch_size=dify_config.OTEL_MAX_EXPORT_BATCH_SIZE, - export_timeout_millis=dify_config.OTEL_BATCH_EXPORT_TIMEOUT, - ) - ) - reader = PeriodicExportingMetricReader( - metric_exporter, - export_interval_millis=dify_config.OTEL_METRIC_EXPORT_INTERVAL, - export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT, - ) - set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader])) - if not is_celery_worker(): - init_flask_instrumentor(app) - CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument() - instrument_exception_logging() - init_sqlalchemy_instrumentor(app) - atexit.register(shutdown_tracer) + from opentelemetry.trace import get_current_span + + if user: + current_span = get_current_span() + if current_span: + current_span.set_attribute("service.tenant.id", user.current_tenant_id) + current_span.set_attribute("service.user.id", user.id) -def is_celery_worker(): - return "celery" in sys.argv[0].lower() +def init_app(app: DifyApp): + def is_celery_worker(): + return "celery" in sys.argv[0].lower() + def instrument_exception_logging(): + exception_handler = ExceptionLoggingHandler() + logging.getLogger().addHandler(exception_handler) -def instrument_exception_logging(): - exception_handler = ExceptionLoggingHandler() - logging.getLogger().addHandler(exception_handler) + def init_flask_instrumentor(app: DifyApp): + meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION) + _http_response_counter = meter.create_counter( + "http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}" + ) + def response_hook(span: Span, status: str, response_headers: list): + if span and span.is_recording(): + if status.startswith("2"): + span.set_status(StatusCode.OK) + else: + span.set_status(StatusCode.ERROR, status) + + status = status.split(" ")[0] + status_code = int(status) + status_class = f"{status_code // 100}xx" + _http_response_counter.add(1, {"status_code": status_code, "status_class": status_class}) + + instrumentor = FlaskInstrumentor() + if dify_config.DEBUG: + logging.info("Initializing Flask instrumentor") + instrumentor.instrument_app(app, response_hook=response_hook) + + def init_sqlalchemy_instrumentor(app: DifyApp): + with app.app_context(): + engines = list(app.extensions["sqlalchemy"].engines.values()) + SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines) + + def setup_context_propagation(): + # Configure propagators + set_global_textmap( + CompositePropagator( + [ + TraceContextTextMapPropagator(), # W3C trace context + B3Format(), # B3 propagation (used by many systems) + ] + ) + ) -def init_flask_instrumentor(app: DifyApp): - meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION) - _http_response_counter = meter.create_counter( - "http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}" + def shutdown_tracer(): + provider = trace.get_tracer_provider() + if hasattr(provider, "force_flush"): + provider.force_flush() + + class ExceptionLoggingHandler(logging.Handler): + """Custom logging handler that creates spans for logging.exception() calls""" + + def emit(self, record): + try: + if record.exc_info: + tracer = get_tracer_provider().get_tracer("dify.exception.logging") + with tracer.start_as_current_span( + "log.exception", + attributes={ + "log.level": record.levelname, + "log.message": record.getMessage(), + "log.logger": record.name, + "log.file.path": record.pathname, + "log.file.line": record.lineno, + }, + ) as span: + span.set_status(StatusCode.ERROR) + span.record_exception(record.exc_info[1]) + span.set_attribute("exception.type", record.exc_info[0].__name__) + span.set_attribute("exception.message", str(record.exc_info[1])) + except Exception: + pass + + from opentelemetry import trace + from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.instrumentation.celery import CeleryInstrumentor + from opentelemetry.instrumentation.flask import FlaskInstrumentor + from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor + from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider + from opentelemetry.propagate import set_global_textmap + from opentelemetry.propagators.b3 import B3Format + from opentelemetry.propagators.composite import CompositePropagator + from opentelemetry.sdk.metrics import MeterProvider + from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader + from opentelemetry.sdk.resources import Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, ) - - def response_hook(span: Span, status: str, response_headers: list): - if span and span.is_recording(): - if status.startswith("2"): - span.set_status(StatusCode.OK) - else: - span.set_status(StatusCode.ERROR, status) - - status = status.split(" ")[0] - status_code = int(status) - status_class = f"{status_code // 100}xx" - _http_response_counter.add(1, {"status_code": status_code, "status_class": status_class}) - - instrumentor = FlaskInstrumentor() - if dify_config.DEBUG: - logging.info("Initializing Flask instrumentor") - instrumentor.instrument_app(app, response_hook=response_hook) - - -def init_sqlalchemy_instrumentor(app: DifyApp): - with app.app_context(): - engines = list(app.extensions["sqlalchemy"].engines.values()) - SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines) - - -def setup_context_propagation(): - # Configure propagators - set_global_textmap( - CompositePropagator( - [ - TraceContextTextMapPropagator(), # W3C trace context - B3Format(), # B3 propagation (used by many systems) - ] + from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio + from opentelemetry.semconv.resource import ResourceAttributes + from opentelemetry.trace import Span, get_tracer_provider, set_tracer_provider + from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + from opentelemetry.trace.status import StatusCode + + setup_context_propagation() + # Initialize OpenTelemetry + # Follow Semantic Convertions 1.32.0 to define resource attributes + resource = Resource( + attributes={ + ResourceAttributes.SERVICE_NAME: dify_config.APPLICATION_NAME, + ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}", + ResourceAttributes.PROCESS_PID: os.getpid(), + ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", + ResourceAttributes.HOST_NAME: socket.gethostname(), + ResourceAttributes.HOST_ARCH: platform.machine(), + "custom.deployment.git_commit": dify_config.COMMIT_SHA, + ResourceAttributes.HOST_ID: platform.node(), + ResourceAttributes.OS_TYPE: platform.system().lower(), + ResourceAttributes.OS_DESCRIPTION: platform.platform(), + ResourceAttributes.OS_VERSION: platform.version(), + } + ) + sampler = ParentBasedTraceIdRatio(dify_config.OTEL_SAMPLING_RATE) + provider = TracerProvider(resource=resource, sampler=sampler) + set_tracer_provider(provider) + exporter: Union[OTLPSpanExporter, ConsoleSpanExporter] + metric_exporter: Union[OTLPMetricExporter, ConsoleMetricExporter] + if dify_config.OTEL_EXPORTER_TYPE == "otlp": + exporter = OTLPSpanExporter( + endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/traces", + headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"}, + ) + metric_exporter = OTLPMetricExporter( + endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/metrics", + headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"}, + ) + else: + # Fallback to console exporter + exporter = ConsoleSpanExporter() + metric_exporter = ConsoleMetricExporter() + + provider.add_span_processor( + BatchSpanProcessor( + exporter, + max_queue_size=dify_config.OTEL_MAX_QUEUE_SIZE, + schedule_delay_millis=dify_config.OTEL_BATCH_EXPORT_SCHEDULE_DELAY, + max_export_batch_size=dify_config.OTEL_MAX_EXPORT_BATCH_SIZE, + export_timeout_millis=dify_config.OTEL_BATCH_EXPORT_TIMEOUT, ) ) + reader = PeriodicExportingMetricReader( + metric_exporter, + export_interval_millis=dify_config.OTEL_METRIC_EXPORT_INTERVAL, + export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT, + ) + set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader])) + if not is_celery_worker(): + init_flask_instrumentor(app) + CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument() + instrument_exception_logging() + init_sqlalchemy_instrumentor(app) + atexit.register(shutdown_tracer) -@worker_init.connect(weak=False) -def init_celery_worker(*args, **kwargs): - tracer_provider = get_tracer_provider() - metric_provider = get_meter_provider() - if dify_config.DEBUG: - logging.info("Initializing OpenTelemetry for Celery worker") - CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument() +def is_enabled(): + return dify_config.ENABLE_OTEL -def shutdown_tracer(): - provider = trace.get_tracer_provider() - if hasattr(provider, "force_flush"): - provider.force_flush() +@worker_init.connect(weak=False) +def init_celery_worker(*args, **kwargs): + if dify_config.ENABLE_OTEL: + from opentelemetry.instrumentation.celery import CeleryInstrumentor + from opentelemetry.metrics import get_meter_provider + from opentelemetry.trace import get_tracer_provider + + tracer_provider = get_tracer_provider() + metric_provider = get_meter_provider() + if dify_config.DEBUG: + logging.info("Initializing OpenTelemetry for Celery worker") + CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument() diff --git a/api/extensions/ext_otel_patch.py b/api/extensions/ext_otel_patch.py deleted file mode 100644 index 58309fe4d1..0000000000 --- a/api/extensions/ext_otel_patch.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Patch for OpenTelemetry context detach method to handle None tokens gracefully. - -This patch addresses the issue where OpenTelemetry's context.detach() method raises a TypeError -when called with a None token. The error occurs in the contextvars_context.py file where it tries -to call reset() on a None token. - -Related GitHub issue: https://github.com/langgenius/dify/issues/18496 - -Error being fixed: -``` -Traceback (most recent call last): - File "opentelemetry/context/__init__.py", line 154, in detach - _RUNTIME_CONTEXT.detach(token) - File "opentelemetry/context/contextvars_context.py", line 50, in detach - self._current_context.reset(token) # type: ignore - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TypeError: expected an instance of Token, got None -``` - -Instead of modifying the third-party package directly, this patch monkey-patches the -context.detach method to gracefully handle None tokens. -""" - -import logging -from functools import wraps - -from opentelemetry import context - -logger = logging.getLogger(__name__) - -# Store the original detach method -original_detach = context.detach - - -# Create a patched version that handles None tokens -@wraps(original_detach) -def patched_detach(token): - """ - A patched version of context.detach that handles None tokens gracefully. - """ - if token is None: - logger.debug("Attempted to detach a None token, skipping") - return - - return original_detach(token) - - -def is_enabled(): - """ - Check if the extension is enabled. - Always enable this patch to prevent errors even when OpenTelemetry is disabled. - """ - return True - - -def init_app(app): - """ - Initialize the OpenTelemetry context patch. - """ - # Replace the original detach method with our patched version - context.detach = patched_detach - logger.info("OpenTelemetry context.detach patched to handle None tokens") diff --git a/api/migrations/versions/2024_10_10_0516-bbadea11becb_add_name_and_size_to_tool_files.py b/api/migrations/versions/2024_10_10_0516-bbadea11becb_add_name_and_size_to_tool_files.py index c17d1db77a..5b5656e7ed 100644 --- a/api/migrations/versions/2024_10_10_0516-bbadea11becb_add_name_and_size_to_tool_files.py +++ b/api/migrations/versions/2024_10_10_0516-bbadea11becb_add_name_and_size_to_tool_files.py @@ -21,14 +21,14 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### # Get the database connection conn = op.get_bind() - + # Use SQLAlchemy inspector to get the columns of the 'tool_files' table inspector = sa.inspect(conn) columns = [col['name'] for col in inspector.get_columns('tool_files')] # If 'name' or 'size' columns already exist, exit the upgrade function if 'name' in columns or 'size' in columns: - return + return with op.batch_alter_table('tool_files', schema=None) as batch_op: batch_op.add_column(sa.Column('name', sa.String(), nullable=True)) diff --git a/api/migrations/versions/2024_12_20_0628-e1944c35e15e_add_retry_index_field_to_node_execution_.py b/api/migrations/versions/2024_12_20_0628-e1944c35e15e_add_retry_index_field_to_node_execution_.py index 0facd0ecc0..ae9f2de9b1 100644 --- a/api/migrations/versions/2024_12_20_0628-e1944c35e15e_add_retry_index_field_to_node_execution_.py +++ b/api/migrations/versions/2024_12_20_0628-e1944c35e15e_add_retry_index_field_to_node_execution_.py @@ -35,4 +35,4 @@ def downgrade(): # batch_op.drop_column('retry_index') pass - # ### end Alembic commands ### \ No newline at end of file + # ### end Alembic commands ### diff --git a/api/migrations/versions/2024_12_23_1154-d7999dfa4aae_remove_workflow_node_executions_retry_.py b/api/migrations/versions/2024_12_23_1154-d7999dfa4aae_remove_workflow_node_executions_retry_.py index ea129d15f7..07454b0917 100644 --- a/api/migrations/versions/2024_12_23_1154-d7999dfa4aae_remove_workflow_node_executions_retry_.py +++ b/api/migrations/versions/2024_12_23_1154-d7999dfa4aae_remove_workflow_node_executions_retry_.py @@ -23,7 +23,7 @@ def upgrade(): conn = op.get_bind() inspector = inspect(conn) has_column = 'retry_index' in [col['name'] for col in inspector.get_columns('workflow_node_executions')] - + if has_column: with op.batch_alter_table('workflow_node_executions', schema=None) as batch_op: batch_op.drop_column('retry_index') diff --git a/api/migrations/versions/64b051264f32_init.py b/api/migrations/versions/64b051264f32_init.py index 8c45ae898d..b0fb3deac6 100644 --- a/api/migrations/versions/64b051264f32_init.py +++ b/api/migrations/versions/64b051264f32_init.py @@ -1,7 +1,7 @@ """init Revision ID: 64b051264f32 -Revises: +Revises: Create Date: 2023-05-13 14:26:59.085018 """ diff --git a/api/migrations/versions/de95f5c77138_migration_serpapi_api_key.py b/api/migrations/versions/de95f5c77138_migration_serpapi_api_key.py index fcca705d21..c18126286c 100644 --- a/api/migrations/versions/de95f5c77138_migration_serpapi_api_key.py +++ b/api/migrations/versions/de95f5c77138_migration_serpapi_api_key.py @@ -99,12 +99,12 @@ def upgrade(): id=id, tenant_id=tenant_id, user_id=user_id, - provider='google', + provider='google', encrypted_credentials=encrypted_credentials, created_at=created_at, updated_at=updated_at ) - + # ### end Alembic commands ### diff --git a/api/pyproject.toml b/api/pyproject.toml index 2a4ad6fea2..f3526ec717 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dify-api" -version = "1.3.0" +dynamic = ["version"] requires-python = ">=3.11,<3.13" dependencies = [ @@ -81,15 +81,19 @@ dependencies = [ "tokenizers~=0.15.0", "transformers~=4.35.0", "unstructured[docx,epub,md,ppt,pptx]~=0.16.1", - "validators==0.21.0", "weave~=0.51.34", "yarl~=1.18.3", + "webvtt-py~=0.5.1", ] # Before adding new dependency, consider place it in # alphabet order (a-z) and suitable group. +[tool.setuptools] +packages = [] + [tool.uv] default-groups = ["storage", "tools", "vdb"] +package = false [dependency-groups] @@ -191,6 +195,6 @@ vdb = [ "tidb-vector==0.0.9", "upstash-vector==0.6.0", "volcengine-compat~=1.0.156", - "weaviate-client~=3.21.0", + "weaviate-client~=3.24.0", "xinference-client~=1.2.2", ] diff --git a/api/services/external_knowledge_service.py b/api/services/external_knowledge_service.py index d9ee221a3c..6b75c29d95 100644 --- a/api/services/external_knowledge_service.py +++ b/api/services/external_knowledge_service.py @@ -2,9 +2,9 @@ import json from copy import deepcopy from datetime import UTC, datetime from typing import Any, Optional, Union, cast +from urllib.parse import urlparse import httpx -import validators from constants import HIDDEN_VALUE from core.helper import ssrf_proxy @@ -72,7 +72,9 @@ class ExternalDatasetService: endpoint = f"{settings['endpoint']}/retrieval" api_key = settings["api_key"] - if not validators.url(endpoint, simple_host=True): + + parsed_url = urlparse(endpoint) + if not all([parsed_url.scheme, parsed_url.netloc]): if not endpoint.startswith("http://") and not endpoint.startswith("https://"): raise ValueError(f"invalid endpoint: {endpoint} must start with http:// or https://") else: diff --git a/api/services/plugin/data_migration.py b/api/services/plugin/data_migration.py index 597585588b..1c5abfecba 100644 --- a/api/services/plugin/data_migration.py +++ b/api/services/plugin/data_migration.py @@ -86,9 +86,9 @@ limit 1000""" update_retrieval_model_sql = ", retrieval_model = :retrieval_model" params["retrieval_model"] = json.dumps(retrieval_model) - sql = f"""update {table_name} - set {provider_column_name} = - concat('{DEFAULT_PLUGIN_ID}/', {provider_column_name}, '/', {provider_column_name}) + sql = f"""update {table_name} + set {provider_column_name} = + concat('{DEFAULT_PLUGIN_ID}/', {provider_column_name}, '/', {provider_column_name}) {update_retrieval_model_sql} where id = :record_id""" conn.execute(db.text(sql), params) @@ -131,10 +131,10 @@ limit 1000""" while True: sql = f""" - SELECT id, {provider_column_name} AS provider_name + SELECT id, {provider_column_name} AS provider_name FROM {table_name} - WHERE {provider_column_name} NOT LIKE '%/%' - AND {provider_column_name} IS NOT NULL + WHERE {provider_column_name} NOT LIKE '%/%' + AND {provider_column_name} IS NOT NULL AND {provider_column_name} != '' AND id > :last_id ORDER BY id ASC @@ -183,8 +183,8 @@ limit 1000""" if batch_updates: update_sql = f""" - UPDATE {table_name} - SET {provider_column_name} = :updated_value + UPDATE {table_name} + SET {provider_column_name} = :updated_value WHERE id = :record_id """ conn.execute(db.text(update_sql), [{"updated_value": u, "record_id": r} for u, r in batch_updates]) diff --git a/api/templates/clean_document_job_mail_template-US.html b/api/templates/clean_document_job_mail_template-US.html index 88e78f41c7..0f7ddc62a9 100644 --- a/api/templates/clean_document_job_mail_template-US.html +++ b/api/templates/clean_document_job_mail_template-US.html @@ -77,7 +77,7 @@

Some Documents in Your Knowledge Base Have Been Disabled

Dear {{userName}},

- We're sorry for the inconvenience. To ensure optimal performance, documents + We're sorry for the inconvenience. To ensure optimal performance, documents that haven’t been updated or accessed in the past 30 days have been disabled in your knowledge bases:

@@ -97,4 +97,4 @@ - \ No newline at end of file + diff --git a/api/templates/delete_account_code_email_template_en-US.html b/api/templates/delete_account_code_email_template_en-US.html index 7707385334..eca3dedf72 100644 --- a/api/templates/delete_account_code_email_template_en-US.html +++ b/api/templates/delete_account_code_email_template_en-US.html @@ -122,4 +122,4 @@ - \ No newline at end of file + diff --git a/api/templates/delete_account_success_template_en-US.html b/api/templates/delete_account_success_template_en-US.html index c5df75cabc..b96eee1172 100644 --- a/api/templates/delete_account_success_template_en-US.html +++ b/api/templates/delete_account_success_template_en-US.html @@ -102,4 +102,4 @@ - \ No newline at end of file + diff --git a/api/tests/integration_tests/.gitignore b/api/tests/integration_tests/.gitignore index 426667562b..ed9875073f 100644 --- a/api/tests/integration_tests/.gitignore +++ b/api/tests/integration_tests/.gitignore @@ -1 +1 @@ -.env.test \ No newline at end of file +.env.test diff --git a/api/tests/integration_tests/vdb/opensearch/test_opensearch.py b/api/tests/integration_tests/vdb/opensearch/test_opensearch.py index 35eed75c2f..2d44dd2924 100644 --- a/api/tests/integration_tests/vdb/opensearch/test_opensearch.py +++ b/api/tests/integration_tests/vdb/opensearch/test_opensearch.py @@ -23,13 +23,70 @@ def setup_mock_redis(): ext_redis.redis_client.lock = MagicMock(return_value=mock_redis_lock) +class TestOpenSearchConfig: + def test_to_opensearch_params(self): + config = OpenSearchConfig( + host="localhost", + port=9200, + secure=True, + user="admin", + password="password", + ) + + params = config.to_opensearch_params() + + assert params["hosts"] == [{"host": "localhost", "port": 9200}] + assert params["use_ssl"] is True + assert params["verify_certs"] is True + assert params["connection_class"].__name__ == "Urllib3HttpConnection" + assert params["http_auth"] == ("admin", "password") + + @patch("boto3.Session") + @patch("core.rag.datasource.vdb.opensearch.opensearch_vector.Urllib3AWSV4SignerAuth") + def test_to_opensearch_params_with_aws_managed_iam( + self, mock_aws_signer_auth: MagicMock, mock_boto_session: MagicMock + ): + mock_credentials = MagicMock() + mock_boto_session.return_value.get_credentials.return_value = mock_credentials + + mock_auth_instance = MagicMock() + mock_aws_signer_auth.return_value = mock_auth_instance + + aws_region = "ap-southeast-2" + aws_service = "aoss" + host = f"aoss-endpoint.{aws_region}.aoss.amazonaws.com" + port = 9201 + + config = OpenSearchConfig( + host=host, + port=port, + secure=True, + auth_method="aws_managed_iam", + aws_region=aws_region, + aws_service=aws_service, + ) + + params = config.to_opensearch_params() + + assert params["hosts"] == [{"host": host, "port": port}] + assert params["use_ssl"] is True + assert params["verify_certs"] is True + assert params["connection_class"].__name__ == "Urllib3HttpConnection" + assert params["http_auth"] is mock_auth_instance + + mock_aws_signer_auth.assert_called_once_with( + credentials=mock_credentials, region=aws_region, service=aws_service + ) + assert mock_boto_session.return_value.get_credentials.called + + class TestOpenSearchVector: def setup_method(self): self.collection_name = "test_collection" self.example_doc_id = "example_doc_id" self.vector = OpenSearchVector( collection_name=self.collection_name, - config=OpenSearchConfig(host="localhost", port=9200, user="admin", password="password", secure=False), + config=OpenSearchConfig(host="localhost", port=9200, secure=False, user="admin", password="password"), ) self.vector._client = MagicMock() diff --git a/api/tests/unit_tests/.gitignore b/api/tests/unit_tests/.gitignore index 426667562b..ed9875073f 100644 --- a/api/tests/unit_tests/.gitignore +++ b/api/tests/unit_tests/.gitignore @@ -1 +1 @@ -.env.test \ No newline at end of file +.env.test 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 2a29ad3e41..f3dbd1836b 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 @@ -864,10 +864,11 @@ def test_condition_parallel_correct_output(mock_close, mock_remove, app): with patch.object(CodeNode, "_run", new=code_generator): generator = graph_engine.run() stream_content = "" - res_content = "VAT:\ndify 123" + wrong_content = ["Stamp Duty", "other"] for item in generator: if isinstance(item, NodeRunStreamChunkEvent): stream_content += f"{item.chunk_content}\n" if isinstance(item, GraphRunSucceededEvent): - assert item.outputs == {"answer": res_content} - assert stream_content == res_content + "\n" + assert item.outputs is not None + answer = item.outputs["answer"] + assert all(rc not in answer for rc in wrong_content) diff --git a/api/uv.lock b/api/uv.lock index d3009e8a66..9ae14dbd25 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1155,7 +1155,6 @@ wheels = [ [[package]] name = "dify-api" -version = "1.3.0" source = { virtual = "." } dependencies = [ { name = "authlib" }, @@ -1233,8 +1232,8 @@ dependencies = [ { name = "tokenizers" }, { name = "transformers" }, { name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] }, - { name = "validators" }, { name = "weave" }, + { name = "webvtt-py" }, { name = "yarl" }, ] @@ -1403,8 +1402,8 @@ requires-dist = [ { name = "tokenizers", specifier = "~=0.15.0" }, { name = "transformers", specifier = "~=4.35.0" }, { name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" }, - { name = "validators", specifier = "==0.21.0" }, { name = "weave", specifier = "~=0.51.34" }, + { name = "webvtt-py", specifier = "~=0.5.1" }, { name = "yarl", specifier = "~=1.18.3" }, ] @@ -1492,7 +1491,7 @@ vdb = [ { name = "tidb-vector", specifier = "==0.0.9" }, { name = "upstash-vector", specifier = "==0.6.0" }, { name = "volcengine-compat", specifier = "~=1.0.156" }, - { name = "weaviate-client", specifier = "~=3.21.0" }, + { name = "weaviate-client", specifier = "~=3.24.0" }, { name = "xinference-client", specifier = "~=1.2.2" }, ] @@ -6086,11 +6085,11 @@ wheels = [ [[package]] name = "validators" -version = "0.21.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/4095e7a5a6fecc2eca953ad058a3609135d833f986f84951f7e26790d651/validators-0.21.0.tar.gz", hash = "sha256:245b98ab778ed9352a7269c6a8f6c2a839bed5b2a7e3e60273ce399d247dd4b3", size = 20937 } +sdist = { url = "https://files.pythonhosted.org/packages/64/07/91582d69320f6f6daaf2d8072608a4ad8884683d4840e7e4f3a9dbdcc639/validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f", size = 70955 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/50/18dbf2ac594234ee6249bfe3425fa424c18eeb96f29dcd47f199ed6c51bc/validators-0.21.0-py3-none-any.whl", hash = "sha256:3470db6f2384c49727ee319afa2e97aec3f8fad736faa6067e0fd7f9eaf2c551", size = 27686 }, + { url = "https://files.pythonhosted.org/packages/6e/78/36828a4d857b25896f9774c875714ba4e9b3bc8a92d2debe3f4df3a83d4f/validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321", size = 43536 }, ] [[package]] @@ -6220,17 +6219,16 @@ wheels = [ [[package]] name = "weaviate-client" -version = "3.21.0" +version = "3.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "requests" }, - { name = "tqdm" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/a5/c6777a8507249d7a63f4f5d9696eb5f45beac87db0eddfa4438d408cc3b4/weaviate-client-3.21.0.tar.gz", hash = "sha256:ec94ac554883c765e94da8b2947c4f0fa4a0378ed3bbe9f3653df3a5b1745a6d", size = 186970 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/c1/3285a21d8885f2b09aabb65edb9a8e062a35c2d7175e1bb024fa096582ab/weaviate-client-3.24.2.tar.gz", hash = "sha256:6914c48c9a7e5ad0be9399271f9cb85d6f59ab77476c6d4e56a3925bf149edaa", size = 199332 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/5b/57b55ad36eb071b57e79f1ea7fba5bfe6a2fe49702607f56726569665d60/weaviate_client-3.21.0-py3-none-any.whl", hash = "sha256:420444ded7106fb000f4f8b2321b5f5fa2387825aa7a303d702accf61026f9d2", size = 99944 }, + { url = "https://files.pythonhosted.org/packages/ab/98/3136d05f93e30cf29e1db280eaadf766df18d812dfe7994bcced653b2340/weaviate_client-3.24.2-py3-none-any.whl", hash = "sha256:bc50ca5fcebcd48de0d00f66700b0cf7c31a97c4cd3d29b4036d77c5d1d9479b", size = 107968 }, ] [[package]] @@ -6282,6 +6280,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 }, ] +[[package]] +name = "webvtt-py" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f6/7c9c964681fb148e0293e6860108d378e09ccab2218f9063fd3eb87f840a/webvtt-py-0.5.1.tar.gz", hash = "sha256:2040dd325277ddadc1e0c6cc66cbc4a1d9b6b49b24c57a0c3364374c3e8a3dc1", size = 55128 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ed/aad7e0f5a462d679f7b4d2e0d8502c3096740c883b5bbed5103146480937/webvtt_py-0.5.1-py3-none-any.whl", hash = "sha256:9d517d286cfe7fc7825e9d4e2079647ce32f5678eb58e39ef544ffbb932610b7", size = 19802 }, +] + [[package]] name = "werkzeug" version = "3.1.3" diff --git a/dev/pytest/pytest_all_tests.sh b/dev/pytest/pytest_all_tests.sh index c4318fb922..f0c8a78548 100755 --- a/dev/pytest/pytest_all_tests.sh +++ b/dev/pytest/pytest_all_tests.sh @@ -11,4 +11,4 @@ dev/pytest/pytest_tools.sh dev/pytest/pytest_workflow.sh # Unit tests -dev/pytest/pytest_unit_tests.sh \ No newline at end of file +dev/pytest/pytest_unit_tests.sh diff --git a/dev/pytest/pytest_model_runtime.sh b/dev/pytest/pytest_model_runtime.sh index 63891eb9f8..dc6c6ac627 100755 --- a/dev/pytest/pytest_model_runtime.sh +++ b/dev/pytest/pytest_model_runtime.sh @@ -10,4 +10,4 @@ pytest api/tests/integration_tests/model_runtime/anthropic \ api/tests/integration_tests/model_runtime/fireworks \ api/tests/integration_tests/model_runtime/nomic \ api/tests/integration_tests/model_runtime/mixedbread \ - api/tests/integration_tests/model_runtime/voyage \ No newline at end of file + api/tests/integration_tests/model_runtime/voyage diff --git a/docker/.env.example b/docker/.env.example index 83d975cec5..7bff2975fb 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -39,6 +39,12 @@ APP_WEB_URL= # File preview or download Url prefix. # used to display File preview or download Url to the front-end or as Multi-model inputs; # Url is signed and has expiration time. +# Setting FILES_URL is required for file processing plugins. +# - For https://example.com, use FILES_URL=https://example.com +# - For http://example.com, use FILES_URL=http://example.com +# Recommendation: use a dedicated domain (e.g., https://upload.example.com). +# Alternatively, use http://:5001 or http://api:5001, +# ensuring port 5001 is externally accessible (see docker-compose.yaml). FILES_URL= # ------------------------------ @@ -520,9 +526,13 @@ RELYT_DATABASE=postgres # open search configuration, only available when VECTOR_STORE is `opensearch` OPENSEARCH_HOST=opensearch OPENSEARCH_PORT=9200 +OPENSEARCH_SECURE=true +OPENSEARCH_AUTH_METHOD=basic OPENSEARCH_USER=admin OPENSEARCH_PASSWORD=admin -OPENSEARCH_SECURE=true +# If using AWS managed IAM, e.g. Managed Cluster or OpenSearch Serverless +OPENSEARCH_AWS_REGION=ap-southeast-1 +OPENSEARCH_AWS_SERVICE=aoss # tencent vector configurations, only available when VECTOR_STORE is `tencent` TENCENT_VECTOR_DB_URL=http://127.0.0.1 diff --git a/docker/README.md b/docker/README.md index 38b11a677f..22dfe2c91c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -14,7 +14,6 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T - **Unified Vector Database Services**: All vector database services are now managed from a single Docker Compose file `docker-compose.yaml`. You can switch between different vector databases by setting the `VECTOR_STORE` environment variable in your `.env` file. - **Mandatory .env File**: A `.env` file is now required to run `docker compose up`. This file is crucial for configuring your deployment and for any custom settings to persist through upgrades. -- **Legacy Support**: Previous deployment files are now located in the `docker-legacy` directory and will no longer be maintained. ### How to Deploy Dify with `docker-compose.yaml` diff --git a/docker/couchbase-server/Dockerfile b/docker/couchbase-server/Dockerfile index bd8af64150..23e487e4ed 100644 --- a/docker/couchbase-server/Dockerfile +++ b/docker/couchbase-server/Dockerfile @@ -1,4 +1,4 @@ FROM couchbase/server:latest AS stage_base -# FROM couchbase:latest AS stage_base +# FROM couchbase:latest AS stage_base COPY init-cbserver.sh /opt/couchbase/init/ -RUN chmod +x /opt/couchbase/init/init-cbserver.sh \ No newline at end of file +RUN chmod +x /opt/couchbase/init/init-cbserver.sh diff --git a/docker/couchbase-server/init-cbserver.sh b/docker/couchbase-server/init-cbserver.sh index e66bc18530..e19a650f23 100755 --- a/docker/couchbase-server/init-cbserver.sh +++ b/docker/couchbase-server/init-cbserver.sh @@ -1,8 +1,8 @@ #!/bin/bash -# used to start couchbase server - can't get around this as docker compose only allows you to start one command - so we have to start couchbase like the standard couchbase Dockerfile would +# used to start couchbase server - can't get around this as docker compose only allows you to start one command - so we have to start couchbase like the standard couchbase Dockerfile would # https://github.com/couchbase/docker/blob/master/enterprise/couchbase-server/7.2.0/Dockerfile#L88 -/entrypoint.sh couchbase-server & +/entrypoint.sh couchbase-server & # track if setup is complete so we don't try to setup again FILE=/opt/couchbase/init/setupComplete.txt @@ -36,9 +36,9 @@ if ! [ -f "$FILE" ]; then --bucket-ramsize $COUCHBASE_BUCKET_RAMSIZE \ --bucket-type couchbase - # create file so we know that the cluster is setup and don't run the setup again + # create file so we know that the cluster is setup and don't run the setup again touch $FILE -fi +fi # docker compose will stop the container from running unless we do this # known issue and workaround tail -f /dev/null diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 9ab1304492..bfbfe6c19a 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:1.3.0 + image: langgenius/dify-api:1.3.1 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.3.0 + image: langgenius/dify-api:1.3.1 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.3.0 + image: langgenius/dify-web:1.3.1 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/docker/docker-compose.png b/docker/docker-compose.png index bdac113086..015d450236 100644 Binary files a/docker/docker-compose.png and b/docker/docker-compose.png differ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8edcd497c6..3ed0f60e96 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -225,9 +225,12 @@ x-shared-env: &shared-api-worker-env RELYT_DATABASE: ${RELYT_DATABASE:-postgres} OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch} OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200} + OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true} + OPENSEARCH_AUTH_METHOD: ${OPENSEARCH_AUTH_METHOD:-basic} OPENSEARCH_USER: ${OPENSEARCH_USER:-admin} OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-admin} - OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true} + OPENSEARCH_AWS_REGION: ${OPENSEARCH_AWS_REGION:-ap-southeast-1} + OPENSEARCH_AWS_SERVICE: ${OPENSEARCH_AWS_SERVICE:-aoss} TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1} TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify} TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30} @@ -488,7 +491,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.3.0 + image: langgenius/dify-api:1.3.1 restart: always environment: # Use the shared environment variables. @@ -517,7 +520,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.3.0 + image: langgenius/dify-api:1.3.1 restart: always environment: # Use the shared environment variables. @@ -543,7 +546,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.3.0 + image: langgenius/dify-web:1.3.1 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/docker/middleware.env.example b/docker/middleware.env.example index 1a4484a9b5..2ac09ea264 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -144,4 +144,4 @@ PLUGIN_AZURE_BLOB_STORAGE_CONNECTION_STRING= # Plugin oss tencent cos PLUGIN_TENCENT_COS_SECRET_KEY= PLUGIN_TENCENT_COS_SECRET_ID= -PLUGIN_TENCENT_COS_REGION= \ No newline at end of file +PLUGIN_TENCENT_COS_REGION= diff --git a/docker/nginx/docker-entrypoint.sh b/docker/nginx/docker-entrypoint.sh index 8e1110ffa9..763254e37b 100755 --- a/docker/nginx/docker-entrypoint.sh +++ b/docker/nginx/docker-entrypoint.sh @@ -39,4 +39,4 @@ envsubst "$env_vars" < /etc/nginx/proxy.conf.template > /etc/nginx/proxy.conf envsubst "$env_vars" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf # Start Nginx using the default entrypoint -exec nginx -g 'daemon off;' \ No newline at end of file +exec nginx -g 'daemon off;' diff --git a/docker/nginx/https.conf.template b/docker/nginx/https.conf.template index 95ea36f463..296908d8be 100644 --- a/docker/nginx/https.conf.template +++ b/docker/nginx/https.conf.template @@ -6,4 +6,4 @@ ssl_certificate_key ${SSL_CERTIFICATE_KEY_PATH}; ssl_protocols ${NGINX_SSL_PROTOCOLS}; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; -ssl_session_timeout 10m; \ No newline at end of file +ssl_session_timeout 10m; diff --git a/docker/nginx/nginx.conf.template b/docker/nginx/nginx.conf.template index 32a571653e..20446fae2e 100644 --- a/docker/nginx/nginx.conf.template +++ b/docker/nginx/nginx.conf.template @@ -31,4 +31,4 @@ http { client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE}; include /etc/nginx/conf.d/*.conf; -} \ No newline at end of file +} diff --git a/docker/ssrf_proxy/squid.conf.template b/docker/ssrf_proxy/squid.conf.template index c74c1fb67b..1775a1fff9 100644 --- a/docker/ssrf_proxy/squid.conf.template +++ b/docker/ssrf_proxy/squid.conf.template @@ -44,7 +44,7 @@ refresh_pattern . 0 20% 4320 # cache_dir ufs /var/spool/squid 100 16 256 # upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks -# cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default +# cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default ################################## Reverse Proxy To Sandbox ################################ http_port ${REVERSE_PROXY_PORT} accel vhost @@ -53,4 +53,4 @@ acl src_all src all http_access allow src_all # Unless the option's size is increased, an error will occur when uploading more than two files. -client_request_buffer_max_size 100 MB \ No newline at end of file +client_request_buffer_max_size 100 MB diff --git a/docker/startupscripts/init.sh b/docker/startupscripts/init.sh index c6e6e1966f..dcee1e1978 100755 --- a/docker/startupscripts/init.sh +++ b/docker/startupscripts/init.sh @@ -8,6 +8,6 @@ if [ -f ${DB_INITIALIZED} ]; then exit else echo 'File does not exist. Standards for first time Start up this DB' - "$ORACLE_HOME"/bin/sqlplus -s "/ as sysdba" @"/opt/oracle/scripts/startup/init_user.script"; + "$ORACLE_HOME"/bin/sqlplus -s "/ as sysdba" @"/opt/oracle/scripts/startup/init_user.script"; touch ${DB_INITIALIZED} fi diff --git a/docker/startupscripts/init_user.script b/docker/startupscripts/init_user.script index 0c5bff1ef6..e710d827e8 100755 --- a/docker/startupscripts/init_user.script +++ b/docker/startupscripts/init_user.script @@ -1,5 +1,5 @@ show pdbs; -ALTER SYSTEM SET PROCESSES=500 SCOPE=SPFILE; +ALTER SYSTEM SET PROCESSES=500 SCOPE=SPFILE; alter session set container= freepdb1; create user dify identified by dify DEFAULT TABLESPACE users quota unlimited on users; grant DB_DEVELOPER_ROLE to dify; diff --git a/docker/tidb/config/pd.toml b/docker/tidb/config/pd.toml index 042b251e46..01e352a86a 100644 --- a/docker/tidb/config/pd.toml +++ b/docker/tidb/config/pd.toml @@ -1,4 +1,4 @@ # PD Configuration File reference: # https://docs.pingcap.com/tidb/stable/pd-configuration-file#pd-configuration-file [replication] -max-replicas = 1 \ No newline at end of file +max-replicas = 1 diff --git a/docker/volumes/myscale/config/users.d/custom_users_config.xml b/docker/volumes/myscale/config/users.d/custom_users_config.xml index 67f24b69ee..b46e73a0e9 100644 --- a/docker/volumes/myscale/config/users.d/custom_users_config.xml +++ b/docker/volumes/myscale/config/users.d/custom_users_config.xml @@ -14,4 +14,4 @@ 1 - \ No newline at end of file + diff --git a/docker/volumes/oceanbase/init.d/vec_memory.sql b/docker/volumes/oceanbase/init.d/vec_memory.sql index f4c283fdf4..0d859e5f7c 100644 --- a/docker/volumes/oceanbase/init.d/vec_memory.sql +++ b/docker/volumes/oceanbase/init.d/vec_memory.sql @@ -1 +1 @@ -ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30; \ No newline at end of file +ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30; diff --git a/sdks/nodejs-client/.gitignore b/sdks/nodejs-client/.gitignore index 35d1a1461b..1d40ff2ece 100644 --- a/sdks/nodejs-client/.gitignore +++ b/sdks/nodejs-client/.gitignore @@ -45,4 +45,4 @@ package-lock.json .yarnrc.yml # pmpm -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml diff --git a/sdks/nodejs-client/index.d.ts b/sdks/nodejs-client/index.d.ts index a2e6e50aef..a8b7497f4f 100644 --- a/sdks/nodejs-client/index.d.ts +++ b/sdks/nodejs-client/index.d.ts @@ -26,7 +26,7 @@ export declare class DifyClient { params?: Params, stream?: boolean, headerParams?: HeaderParams - ): Promise; + ): Promise; messageFeedback(message_id: string, rating: number, user: User): Promise; @@ -64,9 +64,9 @@ export declare class ChatClient extends DifyClient { getConversations( - user: User, - first_id?: string | null, - limit?: number | null, + user: User, + first_id?: string | null, + limit?: number | null, pinned?: boolean | null ): Promise; @@ -80,7 +80,7 @@ export declare class ChatClient extends DifyClient { renameConversation(conversation_id: string, name: string, user: User,auto_generate:boolean): Promise; deleteConversation(conversation_id: string, user: User): Promise; - + audioToText(data: FormData): Promise; } @@ -88,4 +88,4 @@ export declare class WorkflowClient extends DifyClient { run(inputs: any, user: User, stream?: boolean,): Promise; stop(task_id: string, user: User): Promise; -} \ No newline at end of file +} diff --git a/sdks/nodejs-client/index.js b/sdks/nodejs-client/index.js index 858241ce5a..0ba7bba8bb 100644 --- a/sdks/nodejs-client/index.js +++ b/sdks/nodejs-client/index.js @@ -334,12 +334,12 @@ export class ChatClient extends DifyClient { export class WorkflowClient extends DifyClient { run(inputs,user,stream) { - const data = { - inputs, + const data = { + inputs, response_mode: stream ? "streaming" : "blocking", - user + user }; - + return this.sendRequest( routes.runWorkflow.method, routes.runWorkflow.url(), @@ -357,4 +357,4 @@ export class WorkflowClient extends DifyClient { data ); } -} \ No newline at end of file +} diff --git a/sdks/nodejs-client/index.test.js b/sdks/nodejs-client/index.test.js index f300b16fc9..1f5d6edb06 100644 --- a/sdks/nodejs-client/index.test.js +++ b/sdks/nodejs-client/index.test.js @@ -62,4 +62,4 @@ describe('Send Requests', () => { errorMessage ) }) -}) \ No newline at end of file +}) diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index cc27c5e0c0..cd3bcc4bce 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -32,4 +32,4 @@ "babel-jest": "^29.5.0", "jest": "^29.5.0" } -} \ No newline at end of file +} diff --git a/sdks/php-client/README.md b/sdks/php-client/README.md index 812980d834..91e77ad9ff 100644 --- a/sdks/php-client/README.md +++ b/sdks/php-client/README.md @@ -92,4 +92,4 @@ Replace 'your-api-key-here' with your actual Dify API key. ## License -This SDK is released under the MIT License. \ No newline at end of file +This SDK is released under the MIT License. diff --git a/sdks/php-client/dify-client.php b/sdks/php-client/dify-client.php index acb862093a..b6cf261b66 100644 --- a/sdks/php-client/dify-client.php +++ b/sdks/php-client/dify-client.php @@ -119,14 +119,14 @@ class ChatClient extends DifyClient { return $this->send_request('POST', 'chat-messages', $data, null, $response_mode === 'streaming'); } - + public function get_suggestions($message_id, $user) { $params = [ 'user' => $user ]; return $this->send_request('GET', "messages/{$message_id}/suggested", null, $params); } - + public function stop_message($task_id, $user) { $data = ['user' => $user]; return $this->send_request('POST', "chat-messages/{$task_id}/stop", $data); @@ -157,7 +157,7 @@ class ChatClient extends DifyClient { return $this->send_request('GET', 'messages', null, $params); } - + public function rename_conversation($conversation_id, $name,$auto_generate, $user) { $data = [ 'name' => $name, @@ -202,5 +202,5 @@ class WorkflowClient extends DifyClient{ ]; return $this->send_request('POST', "workflows/tasks/{$task_id}/stop",$data); } - -} \ No newline at end of file + +} diff --git a/sdks/python-client/MANIFEST.in b/sdks/python-client/MANIFEST.in index da331d5e5c..12f44237a2 100644 --- a/sdks/python-client/MANIFEST.in +++ b/sdks/python-client/MANIFEST.in @@ -1 +1 @@ -recursive-include dify_client *.py \ No newline at end of file +recursive-include dify_client *.py diff --git a/sdks/python-client/build.sh b/sdks/python-client/build.sh index ca1a762c99..525f57c1ef 100755 --- a/sdks/python-client/build.sh +++ b/sdks/python-client/build.sh @@ -6,4 +6,4 @@ rm -rf build dist *.egg-info pip install setuptools wheel twine python setup.py sdist bdist_wheel -twine upload dist/* \ No newline at end of file +twine upload dist/* diff --git a/web/.dockerignore b/web/.dockerignore index 45a8922ce9..31eb66c210 100644 --- a/web/.dockerignore +++ b/web/.dockerignore @@ -21,4 +21,4 @@ node_modules # Jetbrains -.idea \ No newline at end of file +.idea diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json index a9afbcc640..e0e72ce11e 100644 --- a/web/.vscode/extensions.json +++ b/web/.vscode/extensions.json @@ -4,4 +4,4 @@ "firsttris.vscode-jest-runner", "kisstkondoros.vscode-codemetrics" ] -} \ No newline at end of file +} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css index 16392a5b4b..45c7d197b4 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/style.module.css @@ -3,4 +3,4 @@ height: 0; border-radius: 16px 16px 0px 0px; box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 2px -1px rgba(0, 0, 0, 0.03); -} \ No newline at end of file +} diff --git a/web/app/(commonLayout)/list.module.css b/web/app/(commonLayout)/list.module.css index 2fc6469a6d..c4d3aec29f 100644 --- a/web/app/(commonLayout)/list.module.css +++ b/web/app/(commonLayout)/list.module.css @@ -214,4 +214,4 @@ .listItem:hover .unavailable { @apply opacity-100; -} \ No newline at end of file +} diff --git a/web/app/components/app-sidebar/style.module.css b/web/app/components/app-sidebar/style.module.css index 722b35bc71..ca0978b760 100644 --- a/web/app/components/app-sidebar/style.module.css +++ b/web/app/components/app-sidebar/style.module.css @@ -5,7 +5,7 @@ .completionPic { background-image: url('./completion.png') } - + .expertPic { background-image: url('./expert.png') -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/base/var-highlight/style.module.css b/web/app/components/app/configuration/base/var-highlight/style.module.css index cd5c8f8d77..2bcef0dabb 100644 --- a/web/app/components/app/configuration/base/var-highlight/style.module.css +++ b/web/app/components/app/configuration/base/var-highlight/style.module.css @@ -1,3 +1,3 @@ .item { background-color: rgba(21, 94, 239, 0.05); -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/base/warning-mask/style.module.css b/web/app/components/app/configuration/base/warning-mask/style.module.css index e1d6f10de9..87f226fd96 100644 --- a/web/app/components/app/configuration/base/warning-mask/style.module.css +++ b/web/app/components/app/configuration/base/warning-mask/style.module.css @@ -5,4 +5,4 @@ .icon { box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/config-prompt/style.module.css b/web/app/components/app/configuration/config-prompt/style.module.css index 66785620b3..224d59d9c8 100644 --- a/web/app/components/app/configuration/config-prompt/style.module.css +++ b/web/app/components/app/configuration/config-prompt/style.module.css @@ -25,4 +25,4 @@ .boxHeader:hover .optionWrap { display: flex; -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/config/automatic/style.module.css b/web/app/components/app/configuration/config/automatic/style.module.css index 7ad3180a5a..15bedd84ca 100644 --- a/web/app/components/app/configuration/config/automatic/style.module.css +++ b/web/app/components/app/configuration/config/automatic/style.module.css @@ -4,4 +4,4 @@ -webkit-text-fill-color: transparent; background-clip: text; text-fill-color: transparent; -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/ctrl-btn-group/style.module.css b/web/app/components/app/configuration/ctrl-btn-group/style.module.css index c7250b8f96..3e874868a9 100644 --- a/web/app/components/app/configuration/ctrl-btn-group/style.module.css +++ b/web/app/components/app/configuration/ctrl-btn-group/style.module.css @@ -3,4 +3,4 @@ right: -16px; bottom: -16px; border-top: 1px solid #F3F4F6; -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/dataset-config/card-item/style.module.css b/web/app/components/app/configuration/dataset-config/card-item/style.module.css index 4ddec9ea37..da07056cbc 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/style.module.css +++ b/web/app/components/app/configuration/dataset-config/card-item/style.module.css @@ -19,4 +19,4 @@ .settingBtn:hover { background-color: rgba(0, 0, 0, 0.05); -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css index 5bfea0cac9..ef9350645a 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.css @@ -4,4 +4,4 @@ .weightedScoreSliderTrack-1 { background: transparent !important; -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/style.module.css b/web/app/components/app/configuration/style.module.css index f0e57cefbf..01f2c93167 100644 --- a/web/app/components/app/configuration/style.module.css +++ b/web/app/components/app/configuration/style.module.css @@ -11,4 +11,4 @@ height: 3px; background-color: rgba(68, 76, 231, 0.18); transform: skewX(-30deg); -} \ No newline at end of file +} diff --git a/web/app/components/app/configuration/tools/index.tsx b/web/app/components/app/configuration/tools/index.tsx index cc34041ba3..ba586bb20d 100644 --- a/web/app/components/app/configuration/tools/index.tsx +++ b/web/app/components/app/configuration/tools/index.tsx @@ -87,7 +87,7 @@ const Tools = () => {
setExpanded(v => !v)} diff --git a/web/app/components/app/overview/embedded/style.module.css b/web/app/components/app/overview/embedded/style.module.css index bea829b059..f2a4d2d0f4 100644 --- a/web/app/components/app/overview/embedded/style.module.css +++ b/web/app/components/app/overview/embedded/style.module.css @@ -17,4 +17,4 @@ } .pluginInstallIcon { background-image: url(../assets/chromeplugin-install.svg); -} \ No newline at end of file +} diff --git a/web/app/components/base/action-button/index.css b/web/app/components/base/action-button/index.css index 2cabe7aecc..3c1a10b86f 100644 --- a/web/app/components/base/action-button/index.css +++ b/web/app/components/base/action-button/index.css @@ -42,4 +42,4 @@ @apply text-text-destructive bg-state-destructive-hover } -} \ No newline at end of file +} diff --git a/web/app/components/base/app-icon/style.module.css b/web/app/components/base/app-icon/style.module.css index 151bc6d3fc..4ee84fb444 100644 --- a/web/app/components/base/app-icon/style.module.css +++ b/web/app/components/base/app-icon/style.module.css @@ -20,4 +20,4 @@ .appIcon.rounded { @apply rounded-full; -} \ No newline at end of file +} diff --git a/web/app/components/base/audio-btn/style.module.css b/web/app/components/base/audio-btn/style.module.css index b8a4da6b68..7e3175aa13 100644 --- a/web/app/components/base/audio-btn/style.module.css +++ b/web/app/components/base/audio-btn/style.module.css @@ -7,4 +7,4 @@ background-image: url(~@/app/components/develop/secret-key/assets/pause.svg); background-position: center; background-repeat: no-repeat; -} \ No newline at end of file +} diff --git a/web/app/components/base/badge/index.css b/web/app/components/base/badge/index.css index 99db573c9c..24c62cdebc 100644 --- a/web/app/components/base/badge/index.css +++ b/web/app/components/base/badge/index.css @@ -25,4 +25,4 @@ .badge.badge-accent { @apply text-text-accent-secondary border border-text-accent-secondary } -} \ No newline at end of file +} diff --git a/web/app/components/base/button/index.css b/web/app/components/base/button/index.css index 5656cb9fdb..47e59142cc 100644 --- a/web/app/components/base/button/index.css +++ b/web/app/components/base/button/index.css @@ -22,7 +22,7 @@ } .btn-primary { - @apply + @apply shadow bg-components-button-primary-bg border-components-button-primary-border @@ -32,7 +32,7 @@ } .btn-primary.btn-destructive { - @apply + @apply bg-components-button-destructive-primary-bg border-components-button-destructive-primary-border hover:bg-components-button-destructive-primary-bg-hover @@ -41,7 +41,7 @@ } .btn-primary.btn-disabled { - @apply + @apply shadow-none bg-components-button-primary-bg-disabled border-components-button-primary-border-disabled @@ -49,7 +49,7 @@ } .btn-primary.btn-destructive.btn-disabled { - @apply + @apply shadow-none bg-components-button-destructive-primary-bg-disabled border-components-button-destructive-primary-border-disabled @@ -57,130 +57,130 @@ } .btn-secondary { - @apply + @apply border-[0.5px] shadow-xs - bg-components-button-secondary-bg - border-components-button-secondary-border - hover:bg-components-button-secondary-bg-hover - hover:border-components-button-secondary-border-hover + bg-components-button-secondary-bg + border-components-button-secondary-border + hover:bg-components-button-secondary-bg-hover + hover:border-components-button-secondary-border-hover text-components-button-secondary-text; } .btn-secondary.btn-disabled { - @apply - bg-components-button-secondary-bg-disabled - border-components-button-secondary-border-disabled + @apply + bg-components-button-secondary-bg-disabled + border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled; } .btn-secondary.btn-destructive { - @apply - bg-components-button-destructive-secondary-bg - border-components-button-destructive-secondary-border - hover:bg-components-button-destructive-secondary-bg-hover - hover:border-components-button-destructive-secondary-border-hover + @apply + bg-components-button-destructive-secondary-bg + border-components-button-destructive-secondary-border + hover:bg-components-button-destructive-secondary-bg-hover + hover:border-components-button-destructive-secondary-border-hover text-components-button-destructive-secondary-text; } .btn-secondary.btn-destructive.btn-disabled { - @apply - bg-components-button-destructive-secondary-bg-disabled - border-components-button-destructive-secondary-border-disabled + @apply + bg-components-button-destructive-secondary-bg-disabled + border-components-button-destructive-secondary-border-disabled text-components-button-destructive-secondary-text-disabled; } - + .btn-secondary-accent { - @apply + @apply border-[0.5px] shadow-xs - bg-components-button-secondary-bg - border-components-button-secondary-border - hover:bg-components-button-secondary-bg-hover - hover:border-components-button-secondary-border-hover + bg-components-button-secondary-bg + border-components-button-secondary-border + hover:bg-components-button-secondary-bg-hover + hover:border-components-button-secondary-border-hover text-components-button-secondary-accent-text; } .btn-secondary-accent.btn-disabled { - @apply - bg-components-button-secondary-bg-disabled - border-components-button-secondary-border-disabled + @apply + bg-components-button-secondary-bg-disabled + border-components-button-secondary-border-disabled text-components-button-secondary-accent-text-disabled; } .btn-warning { - @apply - bg-components-button-destructive-primary-bg - border-components-button-destructive-primary-border - hover:bg-components-button-destructive-primary-bg-hover - hover:border-components-button-destructive-primary-border-hover + @apply + bg-components-button-destructive-primary-bg + border-components-button-destructive-primary-border + hover:bg-components-button-destructive-primary-bg-hover + hover:border-components-button-destructive-primary-border-hover text-components-button-destructive-primary-text; } .btn-warning.btn-disabled { - @apply - bg-components-button-destructive-primary-bg-disabled - border-components-button-destructive-primary-border-disabled + @apply + bg-components-button-destructive-primary-bg-disabled + border-components-button-destructive-primary-border-disabled text-components-button-destructive-primary-text-disabled; } .btn-tertiary { - @apply - bg-components-button-tertiary-bg - hover:bg-components-button-tertiary-bg-hover + @apply + bg-components-button-tertiary-bg + hover:bg-components-button-tertiary-bg-hover text-components-button-tertiary-text; } .btn-tertiary.btn-disabled { - @apply - bg-components-button-tertiary-bg-disabled + @apply + bg-components-button-tertiary-bg-disabled text-components-button-tertiary-text-disabled; } .btn-tertiary.btn-destructive { - @apply - bg-components-button-destructive-tertiary-bg - hover:bg-components-button-destructive-tertiary-bg-hover + @apply + bg-components-button-destructive-tertiary-bg + hover:bg-components-button-destructive-tertiary-bg-hover text-components-button-destructive-tertiary-text; } .btn-tertiary.btn-destructive.btn-disabled { - @apply - bg-components-button-destructive-tertiary-bg-disabled + @apply + bg-components-button-destructive-tertiary-bg-disabled text-components-button-destructive-tertiary-text-disabled; } .btn-ghost { - @apply - hover:bg-components-button-ghost-bg-hover + @apply + hover:bg-components-button-ghost-bg-hover text-components-button-ghost-text; } .btn-ghost.btn-disabled { - @apply + @apply text-components-button-ghost-text-disabled; } .btn-ghost.btn-destructive { - @apply - hover:bg-components-button-destructive-ghost-bg-hover + @apply + hover:bg-components-button-destructive-ghost-bg-hover text-components-button-destructive-ghost-text; } .btn-ghost.btn-destructive.btn-disabled { - @apply + @apply text-components-button-destructive-ghost-text-disabled; } .btn-ghost-accent { - @apply + @apply hover:bg-state-accent-hover text-components-button-secondary-accent-text; } .btn-ghost-accent.btn-disabled { - @apply + @apply text-components-button-secondary-accent-text-disabled; } -} \ No newline at end of file +} diff --git a/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts b/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts index 67029cd163..bcc3ae628d 100644 --- a/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts +++ b/web/app/components/base/chat/chat/answer/__mocks__/markdownContentSVG.ts @@ -2,25 +2,25 @@ export const markdownContentSVG = ` \`\`\`svg - + 创意Logo设计 - + - + 科研 科学研究 - + 探索未知的灯塔, 照亮人类前进的道路。 科研,是永不熄灭的好奇心, 也是推动世界进步的引擎。 - + - + 探索 • 创新 • 进步 \`\`\` diff --git a/web/app/components/base/chat/chat/loading-anim/style.module.css b/web/app/components/base/chat/chat/loading-anim/style.module.css index 5a764db13c..b1371ec82a 100644 --- a/web/app/components/base/chat/chat/loading-anim/style.module.css +++ b/web/app/components/base/chat/chat/loading-anim/style.module.css @@ -79,4 +79,4 @@ .avatar::after { left: 5px; -} \ No newline at end of file +} diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx index c38a4546dd..49444d2d73 100644 --- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' -import React from 'react' -import { RiResetLeftLine } from '@remixicon/react' +import React, { useCallback, useEffect, useState } from 'react' +import { RiCollapseDiagonal2Line, RiExpandDiagonal2Line, RiResetLeftLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import type { Theme } from '../theme/theme-context' import { CssTransform } from '../theme/utils' @@ -36,6 +36,44 @@ const Header: FC = ({ currentConversationId, inputsForms, } = useEmbeddedChatbotContext() + + const isClient = typeof window !== 'undefined' + const isIframe = isClient ? window.self !== window.top : false + const [parentOrigin, setParentOrigin] = useState('') + const [showToggleExpandButton, setShowToggleExpandButton] = useState(false) + const [expanded, setExpanded] = useState(false) + + const handleMessageReceived = useCallback((event: MessageEvent) => { + let currentParentOrigin = parentOrigin + if (!currentParentOrigin && event.data.type === 'dify-chatbot-config') { + currentParentOrigin = event.origin + setParentOrigin(event.origin) + } + if (event.origin !== currentParentOrigin) + return + if (event.data.type === 'dify-chatbot-config') + setShowToggleExpandButton(event.data.payload.isToggledByButton && !event.data.payload.isDraggable) + }, [parentOrigin]) + + useEffect(() => { + if (!isIframe) return + + const listener = (event: MessageEvent) => handleMessageReceived(event) + window.addEventListener('message', listener) + + window.parent.postMessage({ type: 'dify-chatbot-iframe-ready' }, '*') + + return () => window.removeEventListener('message', listener) + }, [isIframe, handleMessageReceived]) + + const handleToggleExpand = useCallback(() => { + if (!isIframe || !showToggleExpandButton) return + setExpanded(!expanded) + window.parent.postMessage({ + type: 'dify-chatbot-expand-change', + }, parentOrigin) + }, [isIframe, parentOrigin, showToggleExpandButton, expanded]) + if (!isMobile) { return (
@@ -59,6 +97,21 @@ const Header: FC = ({ {currentConversationId && ( )} + { + showToggleExpandButton && ( + + + { + expanded + ? + : + } + + + ) + } {currentConversationId && allowResetChat && ( = ({
+ { + showToggleExpandButton && ( + + + { + expanded + ? + : + } + + + ) + } {currentConversationId && allowResetChat && ( = ({ { hovering => (
diff --git a/web/app/components/base/install-button/index.tsx b/web/app/components/base/install-button/index.tsx index d8983bb68e..0d9e953d5e 100644 --- a/web/app/components/base/install-button/index.tsx +++ b/web/app/components/base/install-button/index.tsx @@ -10,8 +10,8 @@ type InstallButtonProps = { const InstallButton = ({ loading, onInstall, t }: InstallButtonProps) => { return (