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:

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/api-tests.yml b/.github/workflows/api-tests.yml
index 02583cda06..f08befefb8 100644
--- a/.github/workflows/api-tests.yml
+++ b/.github/workflows/api-tests.yml
@@ -88,3 +88,6 @@ jobs:
- name: Run Workflow
run: uv run --project api bash dev/pytest/pytest_workflow.sh
+
+ - name: Run Tool
+ run: uv run --project api bash dev/pytest/pytest_tools.sh
diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml
index 98e5fd5150..30c0ff000d 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
@@ -43,8 +49,8 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
run: |
uv run --directory api ruff --version
- uv run --directory api ruff check ./
- uv run --directory api ruff format --check ./
+ uv run --directory api ruff check --diff ./
+ uv run --directory api ruff format --check --diff ./
- name: Dotenv check
if: steps.changed-files.outputs.any_changed == 'true'
@@ -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 @@
-
-
-
-
-
-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).
-
-
-
-
-**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
-
-
-
-
Funkcija
-
Dify.AI
-
LangChain
-
Flowise
-
OpenAI Assistants API
-
-
-
Programski pristop
-
API + usmerjeno v aplikacije
-
Python koda
-
Usmerjeno v aplikacije
-
Usmerjeno v API
-
-
-
Podprti LLM-ji
-
Bogata izbira
-
Bogata izbira
-
Bogata izbira
-
Samo 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.
-
-
-
-
-## 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
-
-[](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.
+
+
+
+
+
+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).
+
+
+
+
+**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
+
+
+
+
Funkcija
+
Dify.AI
+
LangChain
+
Flowise
+
OpenAI Assistants API
+
+
+
Programski pristop
+
API + usmerjeno v aplikacije
+
Python koda
+
Usmerjeno v aplikacije
+
Usmerjeno v API
+
+
+
Podprti LLM-ji
+
Bogata izbira
+
Bogata izbira
+
Bogata izbira
+
Samo 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.
+
+
+
+
+## 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
+
+[](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/.env.example b/api/.env.example
index 2cc6410cdd..3230e3d828 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -476,6 +476,7 @@ LOGIN_LOCKOUT_DURATION=86400
ENABLE_OTEL=false
OTLP_BASE_ENDPOINT=http://localhost:4318
OTLP_API_KEY=
+OTEL_EXPORTER_OTLP_PROTOCOL=
OTEL_EXPORTER_TYPE=otlp
OTEL_SAMPLING_RATE=0.1
OTEL_BATCH_EXPORT_SCHEDULE_DELAY=5000
diff --git a/api/README.md b/api/README.md
index c542f11b16..9308d5dc44 100644
--- a/api/README.md
+++ b/api/README.md
@@ -90,3 +90,4 @@
```bash
uv run -P api bash dev/pytest/pytest_all_tests.sh
```
+
diff --git a/api/app.py b/api/app.py
index 9830a80904..4f393f6c20 100644
--- a/api/app.py
+++ b/api/app.py
@@ -18,7 +18,7 @@ else:
# so we need to disable gevent in debug mode.
# If you are using debugpy and set GEVENT_SUPPORT=True, you can debug with gevent.
if (flask_debug := os.environ.get("FLASK_DEBUG", "0")) and flask_debug.lower() in {"false", "0", "no"}:
- from gevent import monkey # type: ignore
+ from gevent import monkey
# gevent
monkey.patch_all()
diff --git a/api/app_factory.py b/api/app_factory.py
index 9648d770ab..1c886ac5c7 100644
--- a/api/app_factory.py
+++ b/api/app_factory.py
@@ -52,10 +52,8 @@ def initialize_extensions(app: DifyApp):
ext_mail,
ext_migrate,
ext_otel,
- ext_otel_patch,
ext_proxy_fix,
ext_redis,
- ext_repositories,
ext_sentry,
ext_set_secretkey,
ext_storage,
@@ -76,7 +74,6 @@ def initialize_extensions(app: DifyApp):
ext_migrate,
ext_redis,
ext_storage,
- ext_repositories,
ext_celery,
ext_login,
ext_mail,
@@ -85,7 +82,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..66278a53a3 100644
--- a/api/commands.py
+++ b/api/commands.py
@@ -6,6 +6,7 @@ from typing import Optional
import click
from flask import current_app
+from sqlalchemy import select
from werkzeug.exceptions import NotFound
from configs import dify_config
@@ -297,11 +298,11 @@ def migrate_knowledge_vector_database():
page = 1
while True:
try:
- datasets = (
- Dataset.query.filter(Dataset.indexing_technique == "high_quality")
- .order_by(Dataset.created_at.desc())
- .paginate(page=page, per_page=50)
+ stmt = (
+ select(Dataset).filter(Dataset.indexing_technique == "high_quality").order_by(Dataset.created_at.desc())
)
+
+ datasets = db.paginate(select=stmt, page=page, per_page=50, max_per_page=50, error_out=False)
except NotFound:
break
@@ -444,13 +445,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:
@@ -551,11 +552,12 @@ def old_metadata_migration():
page = 1
while True:
try:
- documents = (
- DatasetDocument.query.filter(DatasetDocument.doc_metadata is not None)
+ stmt = (
+ select(DatasetDocument)
+ .filter(DatasetDocument.doc_metadata.is_not(None))
.order_by(DatasetDocument.created_at.desc())
- .paginate(page=page, per_page=50)
)
+ documents = db.paginate(select=stmt, page=page, per_page=50, max_per_page=50, error_out=False)
except NotFound:
break
if not documents:
@@ -592,11 +594,15 @@ def old_metadata_migration():
)
db.session.add(dataset_metadata_binding)
else:
- dataset_metadata_binding = DatasetMetadataBinding.query.filter(
- DatasetMetadataBinding.dataset_id == document.dataset_id,
- DatasetMetadataBinding.document_id == document.id,
- DatasetMetadataBinding.metadata_id == dataset_metadata.id,
- ).first()
+ dataset_metadata_binding = (
+ db.session.query(DatasetMetadataBinding) # type: ignore
+ .filter(
+ DatasetMetadataBinding.dataset_id == document.dataset_id,
+ DatasetMetadataBinding.document_id == document.id,
+ DatasetMetadataBinding.metadata_id == dataset_metadata.id,
+ )
+ .first()
+ )
if not dataset_metadata_binding:
dataset_metadata_binding = DatasetMetadataBinding(
tenant_id=document.tenant_id,
@@ -668,7 +674,7 @@ def upgrade_db():
click.echo(click.style("Starting database migration.", fg="green"))
# run db migration
- import flask_migrate # type: ignore
+ import flask_migrate
flask_migrate.upgrade()
@@ -818,8 +824,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 +852,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 +893,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 +1023,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 +1039,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 +1089,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 +1131,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/feature/__init__.py b/api/configs/feature/__init__.py
index f498dccbbc..a3da5c1b49 100644
--- a/api/configs/feature/__init__.py
+++ b/api/configs/feature/__init__.py
@@ -74,7 +74,7 @@ class CodeExecutionSandboxConfig(BaseSettings):
CODE_EXECUTION_ENDPOINT: HttpUrl = Field(
description="URL endpoint for the code execution service",
- default="http://sandbox:8194",
+ default=HttpUrl("http://sandbox:8194"),
)
CODE_EXECUTION_API_KEY: str = Field(
@@ -145,7 +145,7 @@ class PluginConfig(BaseSettings):
PLUGIN_DAEMON_URL: HttpUrl = Field(
description="Plugin API URL",
- default="http://localhost:5002",
+ default=HttpUrl("http://localhost:5002"),
)
PLUGIN_DAEMON_KEY: str = Field(
@@ -188,7 +188,7 @@ class MarketplaceConfig(BaseSettings):
MARKETPLACE_API_URL: HttpUrl = Field(
description="Marketplace API URL",
- default="https://marketplace.dify.ai",
+ default=HttpUrl("https://marketplace.dify.ai"),
)
@@ -398,6 +398,11 @@ class InnerAPIConfig(BaseSettings):
default=False,
)
+ INNER_API_KEY: Optional[str] = Field(
+ description="API key for accessing the internal API",
+ default=None,
+ )
+
class LoggingConfig(BaseSettings):
"""
diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py
index d285515998..1b015b3267 100644
--- a/api/configs/middleware/__init__.py
+++ b/api/configs/middleware/__init__.py
@@ -1,6 +1,6 @@
import os
from typing import Any, Literal, Optional
-from urllib.parse import quote_plus
+from urllib.parse import parse_qsl, quote_plus
from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt, computed_field
from pydantic_settings import BaseSettings
@@ -173,17 +173,31 @@ class DatabaseConfig(BaseSettings):
RETRIEVAL_SERVICE_EXECUTORS: NonNegativeInt = Field(
description="Number of processes for the retrieval service, default to CPU cores.",
- default=os.cpu_count(),
+ default=os.cpu_count() or 1,
)
- @computed_field
+ @computed_field # type: ignore[misc]
+ @property
def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
+ # Parse DB_EXTRAS for 'options'
+ db_extras_dict = dict(parse_qsl(self.DB_EXTRAS))
+ options = db_extras_dict.get("options", "")
+ # Always include timezone
+ timezone_opt = "-c timezone=UTC"
+ if options:
+ # Merge user options and timezone
+ merged_options = f"{options} {timezone_opt}"
+ else:
+ merged_options = timezone_opt
+
+ connect_args = {"options": merged_options}
+
return {
"pool_size": self.SQLALCHEMY_POOL_SIZE,
"max_overflow": self.SQLALCHEMY_MAX_OVERFLOW,
"pool_recycle": self.SQLALCHEMY_POOL_RECYCLE,
"pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING,
- "connect_args": {"options": "-c timezone=UTC"},
+ "connect_args": connect_args,
}
diff --git a/api/configs/middleware/cache/redis_config.py b/api/configs/middleware/cache/redis_config.py
index 2e98c31ec3..916f52e165 100644
--- a/api/configs/middleware/cache/redis_config.py
+++ b/api/configs/middleware/cache/redis_config.py
@@ -83,3 +83,13 @@ class RedisConfig(BaseSettings):
description="Password for Redis Clusters authentication (if required)",
default=None,
)
+
+ REDIS_SERIALIZATION_PROTOCOL: int = Field(
+ description="Redis serialization protocol (RESP) version",
+ default=3,
+ )
+
+ REDIS_ENABLE_CLIENT_SIDE_CACHE: bool = Field(
+ description="Enable client side cache in redis",
+ default=False,
+ )
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/observability/otel/otel_config.py b/api/configs/observability/otel/otel_config.py
index 568a800d10..1b88ddcfe6 100644
--- a/api/configs/observability/otel/otel_config.py
+++ b/api/configs/observability/otel/otel_config.py
@@ -27,6 +27,11 @@ class OTelConfig(BaseSettings):
default="otlp",
)
+ OTEL_EXPORTER_OTLP_PROTOCOL: str = Field(
+ description="OTLP exporter protocol ('grpc' or 'http')",
+ default="http",
+ )
+
OTEL_SAMPLING_RATE: float = Field(default=0.1, description="Sampling rate for traces (0.0 to 1.0)")
OTEL_BATCH_EXPORT_SCHEDULE_DELAY: int = Field(
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/constants/mimetypes.py b/api/constants/mimetypes.py
new file mode 100644
index 0000000000..38988cdd24
--- /dev/null
+++ b/api/constants/mimetypes.py
@@ -0,0 +1,7 @@
+# The two constants below should keep in sync.
+# Default content type for files which have no explicit content type.
+
+DEFAULT_MIME_TYPE = "application/octet-stream"
+# Default file extension for files which have no explicit content type, should
+# correspond to the `DEFAULT_MIME_TYPE` above.
+DEFAULT_EXTENSION = ".bin"
diff --git a/api/controllers/common/fields.py b/api/controllers/common/fields.py
index b1ebc444a5..3466eea1f6 100644
--- a/api/controllers/common/fields.py
+++ b/api/controllers/common/fields.py
@@ -1,4 +1,6 @@
-from flask_restful import fields # type: ignore
+from flask_restful import fields
+
+from libs.helper import AppIconUrlField
parameters__system_parameters = {
"image_file_size_limit": fields.Integer,
@@ -22,3 +24,20 @@ parameters_fields = {
"file_upload": fields.Raw,
"system_parameters": fields.Nested(parameters__system_parameters),
}
+
+site_fields = {
+ "title": fields.String,
+ "chat_color_theme": fields.String,
+ "chat_color_theme_inverted": fields.Boolean,
+ "icon_type": fields.String,
+ "icon": fields.String,
+ "icon_background": fields.String,
+ "icon_url": AppIconUrlField,
+ "description": fields.String,
+ "copyright": fields.String,
+ "privacy_policy": fields.String,
+ "custom_disclaimer": fields.String,
+ "default_language": fields.String,
+ "show_workflow_steps": fields.Boolean,
+ "use_icon_as_answer_icon": fields.Boolean,
+}
diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py
index 6e3273f5d4..8cb7ad9f5b 100644
--- a/api/controllers/console/admin.py
+++ b/api/controllers/console/admin.py
@@ -1,7 +1,7 @@
from functools import wraps
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound, Unauthorized
diff --git a/api/controllers/console/apikey.py b/api/controllers/console/apikey.py
index eb42507c63..47c93a15c6 100644
--- a/api/controllers/console/apikey.py
+++ b/api/controllers/console/apikey.py
@@ -1,7 +1,7 @@
from typing import Any
-import flask_restful # type: ignore
-from flask_login import current_user # type: ignore
+import flask_restful
+from flask_login import current_user
from flask_restful import Resource, fields, marshal_with
from sqlalchemy import select
from sqlalchemy.orm import Session
diff --git a/api/controllers/console/app/advanced_prompt_template.py b/api/controllers/console/app/advanced_prompt_template.py
index 8d0c5b84af..c228743fa5 100644
--- a/api/controllers/console/app/advanced_prompt_template.py
+++ b/api/controllers/console/app/advanced_prompt_template.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py
index 920cae0d85..d433415894 100644
--- a/api/controllers/console/app/agent.py
+++ b/api/controllers/console/app/agent.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model
diff --git a/api/controllers/console/app/annotation.py b/api/controllers/console/app/annotation.py
index 48353a63af..91058767eb 100644
--- a/api/controllers/console/app/annotation.py
+++ b/api/controllers/console/app/annotation.py
@@ -1,6 +1,6 @@
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py
index 3e908b76a7..f97209c369 100644
--- a/api/controllers/console/app/app.py
+++ b/api/controllers/console/app/app.py
@@ -1,8 +1,8 @@
import uuid
from typing import cast
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, inputs, marshal, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, inputs, marshal, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, Forbidden, abort
diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py
index a159d4c5c4..5dc6515ce0 100644
--- a/api/controllers/console/app/app_import.py
+++ b/api/controllers/console/app/app_import.py
@@ -1,7 +1,7 @@
from typing import cast
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py
index 7519ae96c0..5f2def8d8e 100644
--- a/api/controllers/console/app/audio.py
+++ b/api/controllers/console/app/audio.py
@@ -1,7 +1,7 @@
import logging
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import InternalServerError
import services
diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py
index c9820f70f7..732f5b799a 100644
--- a/api/controllers/console/app/completion.py
+++ b/api/controllers/console/app/completion.py
@@ -1,7 +1,7 @@
import logging
-import flask_login # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+import flask_login
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py
index 8827f129d9..70d6216497 100644
--- a/api/controllers/console/app/conversation.py
+++ b/api/controllers/console/app/conversation.py
@@ -1,9 +1,9 @@
from datetime import UTC, datetime
import pytz # pip install pytz
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
+from flask_restful.inputs import int_range
from sqlalchemy import func, or_
from sqlalchemy.orm import joinedload
from werkzeug.exceptions import Forbidden, NotFound
diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py
index c0a20b7160..d49f433ba1 100644
--- a/api/controllers/console/app/conversation_variables.py
+++ b/api/controllers/console/app/conversation_variables.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_restful import Resource, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
diff --git a/api/controllers/console/app/generator.py b/api/controllers/console/app/generator.py
index 4046417076..790369c052 100644
--- a/api/controllers/console/app/generator.py
+++ b/api/controllers/console/app/generator.py
@@ -1,7 +1,7 @@
import os
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.error import (
diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py
index b5828b6b4b..b7a4c31a15 100644
--- a/api/controllers/console/app/message.py
+++ b/api/controllers/console/app/message.py
@@ -1,8 +1,8 @@
import logging
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, fields, marshal_with, reqparse
+from flask_restful.inputs import int_range
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
from controllers.console import api
diff --git a/api/controllers/console/app/model_config.py b/api/controllers/console/app/model_config.py
index 8ecc8a9db5..f30e3e893c 100644
--- a/api/controllers/console/app/model_config.py
+++ b/api/controllers/console/app/model_config.py
@@ -2,8 +2,8 @@ import json
from typing import cast
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_login import current_user
+from flask_restful import Resource
from controllers.console import api
from controllers.console.app.wraps import get_app_model
diff --git a/api/controllers/console/app/ops_trace.py b/api/controllers/console/app/ops_trace.py
index 7176440e16..978c02412c 100644
--- a/api/controllers/console/app/ops_trace.py
+++ b/api/controllers/console/app/ops_trace.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import BadRequest
from controllers.console import api
diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py
index f15f9d4dae..3c3a359eeb 100644
--- a/api/controllers/console/app/site.py
+++ b/api/controllers/console/app/site.py
@@ -1,7 +1,7 @@
from datetime import UTC, datetime
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound
from constants.languages import supported_language
diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py
index a37d26b989..86aed77412 100644
--- a/api/controllers/console/app/statistic.py
+++ b/api/controllers/console/app/statistic.py
@@ -3,8 +3,8 @@ from decimal import Decimal
import pytz
from flask import jsonify
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model
diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py
index 2e077d2095..0c13adce9b 100644
--- a/api/controllers/console/app/workflow.py
+++ b/api/controllers/console/app/workflow.py
@@ -3,7 +3,7 @@ import logging
from typing import cast
from flask import abort, request
-from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
+from flask_restful import Resource, inputs, marshal_with, reqparse
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py
index d863747995..c475aea9fc 100644
--- a/api/controllers/console/app/workflow_app_log.py
+++ b/api/controllers/console/app/workflow_app_log.py
@@ -1,6 +1,6 @@
from dateutil.parser import isoparse
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import Resource, marshal_with, reqparse
+from flask_restful.inputs import int_range
from sqlalchemy.orm import Session
from controllers.console import api
diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py
index 25a99c1e15..08ab61bbb9 100644
--- a/api/controllers/console/app/workflow_run.py
+++ b/api/controllers/console/app/workflow_run.py
@@ -1,5 +1,5 @@
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import Resource, marshal_with, reqparse
+from flask_restful.inputs import int_range
from controllers.console import api
from controllers.console.app.wraps import get_app_model
diff --git a/api/controllers/console/app/workflow_statistic.py b/api/controllers/console/app/workflow_statistic.py
index 097bf7d188..6c7c73707b 100644
--- a/api/controllers/console/app/workflow_statistic.py
+++ b/api/controllers/console/app/workflow_statistic.py
@@ -3,8 +3,8 @@ from decimal import Decimal
import pytz
from flask import jsonify
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.app.wraps import get_app_model
diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py
index c56f551d49..1795563ff7 100644
--- a/api/controllers/console/auth/activate.py
+++ b/api/controllers/console/auth/activate.py
@@ -1,7 +1,7 @@
import datetime
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from constants.languages import supported_language
from controllers.console import api
diff --git a/api/controllers/console/auth/data_source_bearer_auth.py b/api/controllers/console/auth/data_source_bearer_auth.py
index 5f0762e4a5..b8c3c8f012 100644
--- a/api/controllers/console/auth/data_source_bearer_auth.py
+++ b/api/controllers/console/auth/data_source_bearer_auth.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/auth/data_source_oauth.py b/api/controllers/console/auth/data_source_oauth.py
index b4bd80fe2f..1049f864c3 100644
--- a/api/controllers/console/auth/data_source_oauth.py
+++ b/api/controllers/console/auth/data_source_oauth.py
@@ -2,8 +2,8 @@ import logging
import requests
from flask import current_app, redirect, request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_login import current_user
+from flask_restful import Resource
from werkzeug.exceptions import Forbidden
from configs import dify_config
diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py
index d4a33645ab..d73d8ce701 100644
--- a/api/controllers/console/auth/forgot_password.py
+++ b/api/controllers/console/auth/forgot_password.py
@@ -2,7 +2,7 @@ import base64
import secrets
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
diff --git a/api/controllers/console/auth/login.py b/api/controllers/console/auth/login.py
index 16c1dcc441..27864bab3d 100644
--- a/api/controllers/console/auth/login.py
+++ b/api/controllers/console/auth/login.py
@@ -1,8 +1,8 @@
from typing import cast
-import flask_login # type: ignore
+import flask_login
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
import services
from configs import dify_config
diff --git a/api/controllers/console/auth/oauth.py b/api/controllers/console/auth/oauth.py
index 33bafbf463..f5284cc43b 100644
--- a/api/controllers/console/auth/oauth.py
+++ b/api/controllers/console/auth/oauth.py
@@ -4,7 +4,7 @@ from typing import Optional
import requests
from flask import current_app, redirect, request
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import Unauthorized
diff --git a/api/controllers/console/billing/billing.py b/api/controllers/console/billing/billing.py
index fd7b7bd8cb..4b0c82ae6c 100644
--- a/api/controllers/console/billing/billing.py
+++ b/api/controllers/console/billing/billing.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required
diff --git a/api/controllers/console/billing/compliance.py b/api/controllers/console/billing/compliance.py
index 6d5d668709..9679632ac7 100644
--- a/api/controllers/console/billing/compliance.py
+++ b/api/controllers/console/billing/compliance.py
@@ -1,6 +1,6 @@
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from libs.helper import extract_remote_ip
from libs.login import login_required
diff --git a/api/controllers/console/datasets/data_source.py b/api/controllers/console/datasets/data_source.py
index 70bfb217eb..7b0d9373cf 100644
--- a/api/controllers/console/datasets/data_source.py
+++ b/api/controllers/console/datasets/data_source.py
@@ -2,8 +2,8 @@ import datetime
import json
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py
index 43615af709..981619b0cb 100644
--- a/api/controllers/console/datasets/datasets.py
+++ b/api/controllers/console/datasets/datasets.py
@@ -1,7 +1,7 @@
-import flask_restful # type: ignore
+import flask_restful
from flask import request
-from flask_login import current_user # type: ignore # type: ignore
-from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound
import services
@@ -526,14 +526,20 @@ class DatasetIndexingStatusApi(Resource):
)
documents_status = []
for document in documents:
- completed_segments = DocumentSegment.query.filter(
- DocumentSegment.completed_at.isnot(None),
- DocumentSegment.document_id == str(document.id),
- DocumentSegment.status != "re_segment",
- ).count()
- total_segments = DocumentSegment.query.filter(
- DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment"
- ).count()
+ completed_segments = (
+ db.session.query(DocumentSegment)
+ .filter(
+ DocumentSegment.completed_at.isnot(None),
+ DocumentSegment.document_id == str(document.id),
+ DocumentSegment.status != "re_segment",
+ )
+ .count()
+ )
+ total_segments = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment")
+ .count()
+ )
document.completed_segments = completed_segments
document.total_segments = total_segments
documents_status.append(marshal(document, document_status_fields))
diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py
index 3588abeff5..ca18c25e74 100644
--- a/api/controllers/console/datasets/datasets_document.py
+++ b/api/controllers/console/datasets/datasets_document.py
@@ -4,9 +4,9 @@ from datetime import UTC, datetime
from typing import cast
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, fields, marshal, marshal_with, reqparse # type: ignore
-from sqlalchemy import asc, desc
+from flask_login import current_user
+from flask_restful import Resource, fields, marshal, marshal_with, reqparse
+from sqlalchemy import asc, desc, select
from werkzeug.exceptions import Forbidden, NotFound
import services
@@ -112,7 +112,7 @@ class GetProcessRuleApi(Resource):
limits = DocumentService.DEFAULT_RULES["limits"]
if document_id:
# get the latest process rule
- document = Document.query.get_or_404(document_id)
+ document = db.get_or_404(Document, document_id)
dataset = DatasetService.get_dataset(document.dataset_id)
@@ -175,7 +175,7 @@ class DatasetDocumentListApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
- query = Document.query.filter_by(dataset_id=str(dataset_id), tenant_id=current_user.current_tenant_id)
+ query = select(Document).filter_by(dataset_id=str(dataset_id), tenant_id=current_user.current_tenant_id)
if search:
search = f"%{search}%"
@@ -209,18 +209,24 @@ class DatasetDocumentListApi(Resource):
desc(Document.position),
)
- paginated_documents = query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
+ paginated_documents = db.paginate(select=query, page=page, per_page=limit, max_per_page=100, error_out=False)
documents = paginated_documents.items
if fetch:
for document in documents:
- completed_segments = DocumentSegment.query.filter(
- DocumentSegment.completed_at.isnot(None),
- DocumentSegment.document_id == str(document.id),
- DocumentSegment.status != "re_segment",
- ).count()
- total_segments = DocumentSegment.query.filter(
- DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment"
- ).count()
+ completed_segments = (
+ db.session.query(DocumentSegment)
+ .filter(
+ DocumentSegment.completed_at.isnot(None),
+ DocumentSegment.document_id == str(document.id),
+ DocumentSegment.status != "re_segment",
+ )
+ .count()
+ )
+ total_segments = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment")
+ .count()
+ )
document.completed_segments = completed_segments
document.total_segments = total_segments
data = marshal(documents, document_with_segments_fields)
@@ -563,14 +569,20 @@ class DocumentBatchIndexingStatusApi(DocumentResource):
documents = self.get_batch_documents(dataset_id, batch)
documents_status = []
for document in documents:
- completed_segments = DocumentSegment.query.filter(
- DocumentSegment.completed_at.isnot(None),
- DocumentSegment.document_id == str(document.id),
- DocumentSegment.status != "re_segment",
- ).count()
- total_segments = DocumentSegment.query.filter(
- DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment"
- ).count()
+ completed_segments = (
+ db.session.query(DocumentSegment)
+ .filter(
+ DocumentSegment.completed_at.isnot(None),
+ DocumentSegment.document_id == str(document.id),
+ DocumentSegment.status != "re_segment",
+ )
+ .count()
+ )
+ total_segments = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment")
+ .count()
+ )
document.completed_segments = completed_segments
document.total_segments = total_segments
if document.is_paused:
@@ -589,14 +601,20 @@ class DocumentIndexingStatusApi(DocumentResource):
document_id = str(document_id)
document = self.get_document(dataset_id, document_id)
- completed_segments = DocumentSegment.query.filter(
- DocumentSegment.completed_at.isnot(None),
- DocumentSegment.document_id == str(document_id),
- DocumentSegment.status != "re_segment",
- ).count()
- total_segments = DocumentSegment.query.filter(
- DocumentSegment.document_id == str(document_id), DocumentSegment.status != "re_segment"
- ).count()
+ completed_segments = (
+ db.session.query(DocumentSegment)
+ .filter(
+ DocumentSegment.completed_at.isnot(None),
+ DocumentSegment.document_id == str(document_id),
+ DocumentSegment.status != "re_segment",
+ )
+ .count()
+ )
+ total_segments = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.document_id == str(document_id), DocumentSegment.status != "re_segment")
+ .count()
+ )
document.completed_segments = completed_segments
document.total_segments = total_segments
diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py
index 5c54ecbe81..eee09ac32e 100644
--- a/api/controllers/console/datasets/datasets_segments.py
+++ b/api/controllers/console/datasets/datasets_segments.py
@@ -2,8 +2,9 @@ import uuid
import pandas as pd
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal, reqparse
+from sqlalchemy import select
from werkzeug.exceptions import Forbidden, NotFound
import services
@@ -26,6 +27,7 @@ from controllers.console.wraps import (
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelType
+from extensions.ext_database import db
from extensions.ext_redis import redis_client
from fields.segment_fields import child_chunk_fields, segment_fields
from libs.login import login_required
@@ -74,9 +76,14 @@ class DatasetDocumentSegmentListApi(Resource):
hit_count_gte = args["hit_count_gte"]
keyword = args["keyword"]
- query = DocumentSegment.query.filter(
- DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).order_by(DocumentSegment.position.asc())
+ query = (
+ select(DocumentSegment)
+ .filter(
+ DocumentSegment.document_id == str(document_id),
+ DocumentSegment.tenant_id == current_user.current_tenant_id,
+ )
+ .order_by(DocumentSegment.position.asc())
+ )
if status_list:
query = query.filter(DocumentSegment.status.in_(status_list))
@@ -93,7 +100,7 @@ class DatasetDocumentSegmentListApi(Resource):
elif args["enabled"].lower() == "false":
query = query.filter(DocumentSegment.enabled == False)
- segments = query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
+ segments = db.paginate(select=query, page=page, per_page=limit, max_per_page=100, error_out=False)
response = {
"data": marshal(segments.items, segment_fields),
@@ -276,9 +283,11 @@ class DatasetDocumentSegmentUpdateApi(Resource):
raise ProviderNotInitializeError(ex.description)
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
@@ -320,9 +329,11 @@ class DatasetDocumentSegmentUpdateApi(Resource):
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
@@ -423,9 +434,11 @@ class ChildChunkAddApi(Resource):
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
if not current_user.is_dataset_editor:
@@ -478,9 +491,11 @@ class ChildChunkAddApi(Resource):
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
parser = reqparse.RequestParser()
@@ -523,9 +538,11 @@ class ChildChunkAddApi(Resource):
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
@@ -567,16 +584,20 @@ class ChildChunkUpdateApi(Resource):
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
# check child chunk
child_chunk_id = str(child_chunk_id)
- child_chunk = ChildChunk.query.filter(
- ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id
- ).first()
+ child_chunk = (
+ db.session.query(ChildChunk)
+ .filter(ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not child_chunk:
raise NotFound("Child chunk not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
@@ -612,16 +633,20 @@ class ChildChunkUpdateApi(Resource):
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
- segment = DocumentSegment.query.filter(
- DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
- ).first()
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not segment:
raise NotFound("Segment not found.")
# check child chunk
child_chunk_id = str(child_chunk_id)
- child_chunk = ChildChunk.query.filter(
- ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id
- ).first()
+ child_chunk = (
+ db.session.query(ChildChunk)
+ .filter(ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not child_chunk:
raise NotFound("Child chunk not found.")
# The role of the current user in the ta table must be admin, owner, dataset_operator, or editor
diff --git a/api/controllers/console/datasets/external.py b/api/controllers/console/datasets/external.py
index aee8323f23..cf9081e154 100644
--- a/api/controllers/console/datasets/external.py
+++ b/api/controllers/console/datasets/external.py
@@ -1,6 +1,6 @@
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal, reqparse
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services
@@ -209,6 +209,7 @@ class ExternalKnowledgeHitTestingApi(Resource):
parser = reqparse.RequestParser()
parser.add_argument("query", type=str, location="json")
parser.add_argument("external_retrieval_model", type=dict, required=False, location="json")
+ parser.add_argument("metadata_filtering_conditions", type=dict, required=False, location="json")
args = parser.parse_args()
HitTestingService.hit_testing_args_check(args)
@@ -219,6 +220,7 @@ class ExternalKnowledgeHitTestingApi(Resource):
query=args["query"],
account=current_user,
external_retrieval_model=args["external_retrieval_model"],
+ metadata_filtering_conditions=args["metadata_filtering_conditions"],
)
return response
diff --git a/api/controllers/console/datasets/hit_testing.py b/api/controllers/console/datasets/hit_testing.py
index d344e9d126..fba5d4c0f3 100644
--- a/api/controllers/console/datasets/hit_testing.py
+++ b/api/controllers/console/datasets/hit_testing.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from controllers.console import api
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
diff --git a/api/controllers/console/datasets/hit_testing_base.py b/api/controllers/console/datasets/hit_testing_base.py
index bd944602c1..3b4c076863 100644
--- a/api/controllers/console/datasets/hit_testing_base.py
+++ b/api/controllers/console/datasets/hit_testing_base.py
@@ -1,7 +1,7 @@
import logging
-from flask_login import current_user # type: ignore
-from flask_restful import marshal, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import marshal, reqparse
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services.dataset_service
diff --git a/api/controllers/console/datasets/metadata.py b/api/controllers/console/datasets/metadata.py
index e4cac40ca1..b1a83aa371 100644
--- a/api/controllers/console/datasets/metadata.py
+++ b/api/controllers/console/datasets/metadata.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
from werkzeug.exceptions import NotFound
from controllers.console import api
diff --git a/api/controllers/console/datasets/website.py b/api/controllers/console/datasets/website.py
index 33c926b4c9..4200a51709 100644
--- a/api/controllers/console/datasets/website.py
+++ b/api/controllers/console/datasets/website.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.datasets.error import WebsiteCrawlError
diff --git a/api/controllers/console/explore/audio.py b/api/controllers/console/explore/audio.py
index c7f9fec326..54bc590677 100644
--- a/api/controllers/console/explore/audio.py
+++ b/api/controllers/console/explore/audio.py
@@ -66,7 +66,7 @@ class ChatAudioApi(InstalledAppResource):
class ChatTextApi(InstalledAppResource):
def post(self, installed_app):
- from flask_restful import reqparse # type: ignore
+ from flask_restful import reqparse
app_model = installed_app.app
try:
diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py
index e693a5a71b..4367da1162 100644
--- a/api/controllers/console/explore/completion.py
+++ b/api/controllers/console/explore/completion.py
@@ -1,8 +1,8 @@
import logging
from datetime import UTC, datetime
-from flask_login import current_user # type: ignore
-from flask_restful import reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
diff --git a/api/controllers/console/explore/conversation.py b/api/controllers/console/explore/conversation.py
index 600e78e09e..d7c161cc6d 100644
--- a/api/controllers/console/explore/conversation.py
+++ b/api/controllers/console/explore/conversation.py
@@ -1,6 +1,6 @@
-from flask_login import current_user # type: ignore
-from flask_restful import marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_login import current_user
+from flask_restful import marshal_with, reqparse
+from flask_restful.inputs import int_range
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py
index 132da11878..4062972d08 100644
--- a/api/controllers/console/explore/installed_app.py
+++ b/api/controllers/console/explore/installed_app.py
@@ -2,8 +2,8 @@ from datetime import UTC, datetime
from typing import Any
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, inputs, marshal_with, reqparse
from sqlalchemy import and_
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
@@ -66,7 +66,7 @@ class InstalledAppsListApi(Resource):
parser.add_argument("app_id", type=str, required=True, help="Invalid app_id")
args = parser.parse_args()
- recommended_app = RecommendedApp.query.filter(RecommendedApp.app_id == args["app_id"]).first()
+ recommended_app = db.session.query(RecommendedApp).filter(RecommendedApp.app_id == args["app_id"]).first()
if recommended_app is None:
raise NotFound("App not found")
@@ -79,9 +79,11 @@ class InstalledAppsListApi(Resource):
if not app.is_public:
raise Forbidden("You can't install a non-public app")
- installed_app = InstalledApp.query.filter(
- and_(InstalledApp.app_id == args["app_id"], InstalledApp.tenant_id == current_tenant_id)
- ).first()
+ installed_app = (
+ db.session.query(InstalledApp)
+ .filter(and_(InstalledApp.app_id == args["app_id"], InstalledApp.tenant_id == current_tenant_id))
+ .first()
+ )
if installed_app is None:
# todo: position
diff --git a/api/controllers/console/explore/message.py b/api/controllers/console/explore/message.py
index ff12959a65..822777604a 100644
--- a/api/controllers/console/explore/message.py
+++ b/api/controllers/console/explore/message.py
@@ -1,8 +1,8 @@
import logging
-from flask_login import current_user # type: ignore
-from flask_restful import marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_login import current_user
+from flask_restful import marshal_with, reqparse
+from flask_restful.inputs import int_range
from werkzeug.exceptions import InternalServerError, NotFound
import services
diff --git a/api/controllers/console/explore/parameter.py b/api/controllers/console/explore/parameter.py
index bf9f0d6b28..a1280d91d1 100644
--- a/api/controllers/console/explore/parameter.py
+++ b/api/controllers/console/explore/parameter.py
@@ -1,4 +1,4 @@
-from flask_restful import marshal_with # type: ignore
+from flask_restful import marshal_with
from controllers.common import fields
from controllers.console import api
diff --git a/api/controllers/console/explore/recommended_app.py b/api/controllers/console/explore/recommended_app.py
index be6b1f5d21..ce85f495aa 100644
--- a/api/controllers/console/explore/recommended_app.py
+++ b/api/controllers/console/explore/recommended_app.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, fields, marshal_with, reqparse
from constants.languages import languages
from controllers.console import api
diff --git a/api/controllers/console/explore/saved_message.py b/api/controllers/console/explore/saved_message.py
index 3a1655d0ee..339e7007a0 100644
--- a/api/controllers/console/explore/saved_message.py
+++ b/api/controllers/console/explore/saved_message.py
@@ -1,6 +1,6 @@
-from flask_login import current_user # type: ignore
-from flask_restful import fields, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_login import current_user
+from flask_restful import fields, marshal_with, reqparse
+from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.console import api
diff --git a/api/controllers/console/explore/workflow.py b/api/controllers/console/explore/workflow.py
index a2653a94f6..3f625e6609 100644
--- a/api/controllers/console/explore/workflow.py
+++ b/api/controllers/console/explore/workflow.py
@@ -1,6 +1,6 @@
import logging
-from flask_restful import reqparse # type: ignore
+from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError
from controllers.console.app.error import (
diff --git a/api/controllers/console/explore/wraps.py b/api/controllers/console/explore/wraps.py
index b7ba81fba2..49ea81a8a0 100644
--- a/api/controllers/console/explore/wraps.py
+++ b/api/controllers/console/explore/wraps.py
@@ -1,7 +1,7 @@
from functools import wraps
-from flask_login import current_user # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_login import current_user
+from flask_restful import Resource
from werkzeug.exceptions import NotFound
from controllers.console.wraps import account_initialization_required
diff --git a/api/controllers/console/extension.py b/api/controllers/console/extension.py
index 833da0d03c..07a241ef86 100644
--- a/api/controllers/console/extension.py
+++ b/api/controllers/console/extension.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
from constants import HIDDEN_VALUE
from controllers.console import api
diff --git a/api/controllers/console/feature.py b/api/controllers/console/feature.py
index da1171412f..70ab4ff865 100644
--- a/api/controllers/console/feature.py
+++ b/api/controllers/console/feature.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_login import current_user
+from flask_restful import Resource
from libs.login import login_required
from services.feature_service import FeatureService
diff --git a/api/controllers/console/files.py b/api/controllers/console/files.py
index 8cf754bbd6..66b6214f82 100644
--- a/api/controllers/console/files.py
+++ b/api/controllers/console/files.py
@@ -1,8 +1,8 @@
from typing import Literal
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with
from werkzeug.exceptions import Forbidden
import services
diff --git a/api/controllers/console/init_validate.py b/api/controllers/console/init_validate.py
index cfed5fe7a4..b19e331d2e 100644
--- a/api/controllers/console/init_validate.py
+++ b/api/controllers/console/init_validate.py
@@ -1,7 +1,7 @@
import os
from flask import session
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from sqlalchemy import select
from sqlalchemy.orm import Session
diff --git a/api/controllers/console/ping.py b/api/controllers/console/ping.py
index 2a116112a3..cd28cc946e 100644
--- a/api/controllers/console/ping.py
+++ b/api/controllers/console/ping.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from controllers.console import api
diff --git a/api/controllers/console/remote_files.py b/api/controllers/console/remote_files.py
index 30afc930a8..b8cf019e4f 100644
--- a/api/controllers/console/remote_files.py
+++ b/api/controllers/console/remote_files.py
@@ -2,8 +2,8 @@ import urllib.parse
from typing import cast
import httpx
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
import services
from controllers.common import helpers
diff --git a/api/controllers/console/setup.py b/api/controllers/console/setup.py
index 3b47f8f12f..e1f19a87a3 100644
--- a/api/controllers/console/setup.py
+++ b/api/controllers/console/setup.py
@@ -1,5 +1,5 @@
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from configs import dify_config
from libs.helper import StrLen, email, extract_remote_ip
diff --git a/api/controllers/console/tag/tags.py b/api/controllers/console/tag/tags.py
index 0d0d7ae95f..cb5dedca21 100644
--- a/api/controllers/console/tag/tags.py
+++ b/api/controllers/console/tag/tags.py
@@ -1,6 +1,6 @@
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, marshal_with, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/version.py b/api/controllers/console/version.py
index 7773c99944..7dea8e554e 100644
--- a/api/controllers/console/version.py
+++ b/api/controllers/console/version.py
@@ -2,7 +2,7 @@ import json
import logging
import requests
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from packaging import version
from configs import dify_config
diff --git a/api/controllers/console/workspace/__init__.py b/api/controllers/console/workspace/__init__.py
index 7af2b44a4a..072e904caf 100644
--- a/api/controllers/console/workspace/__init__.py
+++ b/api/controllers/console/workspace/__init__.py
@@ -1,6 +1,6 @@
from functools import wraps
-from flask_login import current_user # type: ignore
+from flask_login import current_user
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py
index e9c25e6c5b..a9dbf44456 100644
--- a/api/controllers/console/workspace/account.py
+++ b/api/controllers/console/workspace/account.py
@@ -2,8 +2,8 @@ import datetime
import pytz
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, fields, marshal_with, reqparse
from configs import dify_config
from constants.languages import supported_language
diff --git a/api/controllers/console/workspace/agent_providers.py b/api/controllers/console/workspace/agent_providers.py
index a41d6c501c..88c37767e3 100644
--- a/api/controllers/console/workspace/agent_providers.py
+++ b/api/controllers/console/workspace/agent_providers.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_login import current_user
+from flask_restful import Resource
from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required
diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py
index aa1a78935d..eb53dcb16e 100644
--- a/api/controllers/console/workspace/endpoint.py
+++ b/api/controllers/console/workspace/endpoint.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/workspace/load_balancing_config.py b/api/controllers/console/workspace/load_balancing_config.py
index 6e1d87cb12..ba74e2c074 100644
--- a/api/controllers/console/workspace/load_balancing_config.py
+++ b/api/controllers/console/workspace/load_balancing_config.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py
index a2b41c1d38..a0031307fd 100644
--- a/api/controllers/console/workspace/members.py
+++ b/api/controllers/console/workspace/members.py
@@ -1,7 +1,7 @@
from urllib import parse
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, abort, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, abort, marshal_with, reqparse
import services
from configs import dify_config
@@ -71,7 +71,6 @@ class MemberInviteEmailApi(Resource):
invitation_results.append(
{"status": "success", "email": invitee_email, "url": f"{console_web_url}/signin"}
)
- break
except Exception as e:
invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)})
diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py
index d7d1cc8d00..ff0fcbda6e 100644
--- a/api/controllers/console/workspace/model_providers.py
+++ b/api/controllers/console/workspace/model_providers.py
@@ -1,8 +1,8 @@
import io
from flask import send_file
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py
index 8b72a1ea3d..37d0f6c764 100644
--- a/api/controllers/console/workspace/models.py
+++ b/api/controllers/console/workspace/models.py
@@ -1,7 +1,7 @@
import logging
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden
from controllers.console import api
diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py
index 6f9ae18750..fda5a7d3bb 100644
--- a/api/controllers/console/workspace/plugin.py
+++ b/api/controllers/console/workspace/plugin.py
@@ -1,8 +1,8 @@
import io
from flask import request, send_file
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden
from configs import dify_config
diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py
index 39ab454922..2b1379bfb2 100644
--- a/api/controllers/console/workspace/tool_providers.py
+++ b/api/controllers/console/workspace/tool_providers.py
@@ -1,8 +1,8 @@
import io
from flask import send_file
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, reqparse
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py
index 332ed00222..34af80bca7 100644
--- a/api/controllers/console/workspace/workspace.py
+++ b/api/controllers/console/workspace/workspace.py
@@ -1,8 +1,9 @@
import logging
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse
+from sqlalchemy import select
from werkzeug.exceptions import Unauthorized
import services
@@ -88,9 +89,8 @@ class WorkspaceListApi(Resource):
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args()
- tenants = Tenant.query.order_by(Tenant.created_at.desc()).paginate(
- page=args["page"], per_page=args["limit"], error_out=False
- )
+ stmt = select(Tenant).order_by(Tenant.created_at.desc())
+ tenants = db.paginate(select=stmt, page=args["page"], per_page=args["limit"], error_out=False)
has_more = False
if tenants.has_next:
@@ -162,7 +162,7 @@ class CustomConfigWorkspaceApi(Resource):
parser.add_argument("replace_webapp_logo", type=str, location="json")
args = parser.parse_args()
- tenant = Tenant.query.filter(Tenant.id == current_user.current_tenant_id).one_or_404()
+ tenant = db.get_or_404(Tenant, current_user.current_tenant_id)
custom_config_dict = {
"remove_webapp_brand": args["remove_webapp_brand"],
@@ -226,7 +226,7 @@ class WorkspaceInfoApi(Resource):
parser.add_argument("name", type=str, required=True, location="json")
args = parser.parse_args()
- tenant = Tenant.query.filter(Tenant.id == current_user.current_tenant_id).one_or_404()
+ tenant = db.get_or_404(Tenant, current_user.current_tenant_id)
tenant.name = args["name"]
db.session.commit()
diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py
index 6911181d82..360cbd9246 100644
--- a/api/controllers/console/wraps.py
+++ b/api/controllers/console/wraps.py
@@ -4,7 +4,7 @@ import time
from functools import wraps
from flask import abort, request
-from flask_login import current_user # type: ignore
+from flask_login import current_user
from configs import dify_config
from controllers.console.workspace.error import AccountNotInitializedError
diff --git a/api/controllers/files/image_preview.py b/api/controllers/files/image_preview.py
index 5adfe16a79..46c19e1fbb 100644
--- a/api/controllers/files/image_preview.py
+++ b/api/controllers/files/image_preview.py
@@ -1,7 +1,7 @@
from urllib.parse import quote
from flask import Response, request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import NotFound
import services
@@ -70,12 +70,26 @@ class FilePreviewApi(Resource):
direct_passthrough=True,
headers={},
)
+ # add Accept-Ranges header for audio/video files
+ if upload_file.mime_type in [
+ "audio/mpeg",
+ "audio/wav",
+ "audio/mp4",
+ "audio/ogg",
+ "audio/flac",
+ "audio/aac",
+ "video/mp4",
+ "video/webm",
+ "video/quicktime",
+ "audio/x-m4a",
+ ]:
+ response.headers["Accept-Ranges"] = "bytes"
if upload_file.size > 0:
response.headers["Content-Length"] = str(upload_file.size)
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/controllers/files/tool_files.py b/api/controllers/files/tool_files.py
index cfcce81247..1c3430ef4f 100644
--- a/api/controllers/files/tool_files.py
+++ b/api/controllers/files/tool_files.py
@@ -1,10 +1,14 @@
+from urllib.parse import quote
+
from flask import Response
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import Forbidden, NotFound
from controllers.files import api
from controllers.files.error import UnsupportedFileTypeError
+from core.tools.signature import verify_tool_file_signature
from core.tools.tool_file_manager import ToolFileManager
+from models import db as global_db
class ToolFilePreviewApi(Resource):
@@ -19,17 +23,14 @@ class ToolFilePreviewApi(Resource):
parser.add_argument("as_attachment", type=bool, required=False, default=False, location="args")
args = parser.parse_args()
-
- if not ToolFileManager.verify_file(
- file_id=file_id,
- timestamp=args["timestamp"],
- nonce=args["nonce"],
- sign=args["sign"],
+ if not verify_tool_file_signature(
+ file_id=file_id, timestamp=args["timestamp"], nonce=args["nonce"], sign=args["sign"]
):
raise Forbidden("Invalid request.")
try:
- stream, tool_file = ToolFileManager.get_file_generator_by_tool_file_id(
+ tool_file_manager = ToolFileManager(engine=global_db.engine)
+ stream, tool_file = tool_file_manager.get_file_generator_by_tool_file_id(
file_id,
)
@@ -47,7 +48,8 @@ class ToolFilePreviewApi(Resource):
if tool_file.size > 0:
response.headers["Content-Length"] = str(tool_file.size)
if args["as_attachment"]:
- response.headers["Content-Disposition"] = f"attachment; filename={tool_file.name}"
+ encoded_filename = quote(tool_file.name)
+ response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
return response
diff --git a/api/controllers/files/upload.py b/api/controllers/files/upload.py
index 28ee0eecf4..6641632169 100644
--- a/api/controllers/files/upload.py
+++ b/api/controllers/files/upload.py
@@ -1,7 +1,7 @@
from mimetypes import guess_extension
from flask import request
-from flask_restful import Resource, marshal_with # type: ignore
+from flask_restful import Resource, marshal_with
from werkzeug.exceptions import Forbidden
import services
@@ -53,7 +53,7 @@ class PluginUploadFileApi(Resource):
raise Forbidden("Invalid request.")
try:
- tool_file = ToolFileManager.create_file_by_raw(
+ tool_file = ToolFileManager().create_file_by_raw(
user_id=user.id,
tenant_id=tenant_id,
file_binary=file.read(),
diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py
index 061ad62a4a..f3a1bd8fa5 100644
--- a/api/controllers/inner_api/plugin/plugin.py
+++ b/api/controllers/inner_api/plugin/plugin.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from controllers.console.wraps import setup_required
from controllers.inner_api import api
diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py
index c31f9d22ed..709bba3f30 100644
--- a/api/controllers/inner_api/plugin/wraps.py
+++ b/api/controllers/inner_api/plugin/wraps.py
@@ -3,7 +3,7 @@ from functools import wraps
from typing import Optional
from flask import request
-from flask_restful import reqparse # type: ignore
+from flask_restful import reqparse
from pydantic import BaseModel
from sqlalchemy.orm import Session
diff --git a/api/controllers/inner_api/workspace/workspace.py b/api/controllers/inner_api/workspace/workspace.py
index 9dfa5d23c3..a2fc2d4675 100644
--- a/api/controllers/inner_api/workspace/workspace.py
+++ b/api/controllers/inner_api/workspace/workspace.py
@@ -1,6 +1,6 @@
import json
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from controllers.console.wraps import setup_required
from controllers.inner_api import api
diff --git a/api/controllers/inner_api/wraps.py b/api/controllers/inner_api/wraps.py
index 86d3ad3dc5..f3a9312dd0 100644
--- a/api/controllers/inner_api/wraps.py
+++ b/api/controllers/inner_api/wraps.py
@@ -18,7 +18,7 @@ def enterprise_inner_api_only(view):
# get header 'X-Inner-Api-Key'
inner_api_key = request.headers.get("X-Inner-Api-Key")
- if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY_FOR_PLUGIN:
+ if not inner_api_key or inner_api_key != dify_config.INNER_API_KEY:
abort(401)
return view(*args, **kwargs)
diff --git a/api/controllers/service_api/__init__.py b/api/controllers/service_api/__init__.py
index d97074e8b9..d964e27819 100644
--- a/api/controllers/service_api/__init__.py
+++ b/api/controllers/service_api/__init__.py
@@ -6,6 +6,6 @@ bp = Blueprint("service_api", __name__, url_prefix="/v1")
api = ExternalApi(bp)
from . import index
-from .app import annotation, app, audio, completion, conversation, file, message, workflow
+from .app import annotation, app, audio, completion, conversation, file, message, site, workflow
from .dataset import dataset, document, hit_testing, metadata, segment, upload_file
from .workspace import models
diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py
index 522a96b791..bd1a23b723 100644
--- a/api/controllers/service_api/app/annotation.py
+++ b/api/controllers/service_api/app/annotation.py
@@ -1,5 +1,5 @@
from flask import request
-from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
+from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden
from controllers.service_api import api
@@ -79,7 +79,7 @@ class AnnotationListApi(Resource):
class AnnotationUpdateDeleteApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
@marshal_with(annotation_fields)
- def post(self, app_model: App, end_user: EndUser, annotation_id):
+ def put(self, app_model: App, end_user: EndUser, annotation_id):
if not current_user.is_editor:
raise Forbidden()
diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py
index 7131e8a310..2c03aba33d 100644
--- a/api/controllers/service_api/app/app.py
+++ b/api/controllers/service_api/app/app.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource, marshal_with # type: ignore
+from flask_restful import Resource, marshal_with
from controllers.common import fields
from controllers.service_api import api
@@ -47,7 +47,7 @@ class AppInfoApi(Resource):
def get(self, app_model: App):
"""Get app information"""
tags = [tag.name for tag in app_model.tags]
- return {"name": app_model.name, "description": app_model.description, "tags": tags}
+ return {"name": app_model.name, "description": app_model.description, "tags": tags, "mode": app_model.mode}
api.add_resource(AppParameterApi, "/parameters")
diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py
index e6bcc0bfd2..2682c2e7f1 100644
--- a/api/controllers/service_api/app/audio.py
+++ b/api/controllers/service_api/app/audio.py
@@ -1,7 +1,7 @@
import logging
from flask import request
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import InternalServerError
import services
diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py
index 38a65b7a90..1d9890199d 100644
--- a/api/controllers/service_api/app/completion.py
+++ b/api/controllers/service_api/app/completion.py
@@ -1,6 +1,6 @@
import logging
-from flask_restful import Resource, reqparse # type: ignore
+from flask_restful import Resource, reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py
index dfc357e1ab..36a7905572 100644
--- a/api/controllers/service_api/app/conversation.py
+++ b/api/controllers/service_api/app/conversation.py
@@ -1,5 +1,5 @@
-from flask_restful import Resource, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import Resource, marshal_with, reqparse
+from flask_restful.inputs import int_range
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
diff --git a/api/controllers/service_api/app/file.py b/api/controllers/service_api/app/file.py
index 27b21b9f50..b0fd8e65ef 100644
--- a/api/controllers/service_api/app/file.py
+++ b/api/controllers/service_api/app/file.py
@@ -1,5 +1,5 @@
from flask import request
-from flask_restful import Resource, marshal_with # type: ignore
+from flask_restful import Resource, marshal_with
import services
from controllers.common.errors import FilenameNotExistsError
diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py
index 95e538f4c7..d90fa2081f 100644
--- a/api/controllers/service_api/app/message.py
+++ b/api/controllers/service_api/app/message.py
@@ -1,8 +1,8 @@
import json
import logging
-from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import Resource, fields, marshal_with, reqparse
+from flask_restful.inputs import int_range
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services
@@ -93,6 +93,18 @@ class MessageFeedbackApi(Resource):
return {"result": "success"}
+class AppGetFeedbacksApi(Resource):
+ @validate_app_token
+ def get(self, app_model: App):
+ """Get All Feedbacks of an app"""
+ parser = reqparse.RequestParser()
+ parser.add_argument("page", type=int, default=1, location="args")
+ parser.add_argument("limit", type=int_range(1, 101), required=False, default=20, location="args")
+ args = parser.parse_args()
+ feedbacks = MessageService.get_all_messages_feedbacks(app_model, page=args["page"], limit=args["limit"])
+ return {"data": feedbacks}
+
+
class MessageSuggestedApi(Resource):
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY, required=True))
def get(self, app_model: App, end_user: EndUser, message_id):
@@ -119,3 +131,4 @@ class MessageSuggestedApi(Resource):
api.add_resource(MessageListApi, "/messages")
api.add_resource(MessageFeedbackApi, "/messages//feedbacks")
api.add_resource(MessageSuggestedApi, "/messages//suggested")
+api.add_resource(AppGetFeedbacksApi, "/app/feedbacks")
diff --git a/api/controllers/service_api/app/site.py b/api/controllers/service_api/app/site.py
new file mode 100644
index 0000000000..e752dfee30
--- /dev/null
+++ b/api/controllers/service_api/app/site.py
@@ -0,0 +1,30 @@
+from flask_restful import Resource, marshal_with
+from werkzeug.exceptions import Forbidden
+
+from controllers.common import fields
+from controllers.service_api import api
+from controllers.service_api.wraps import validate_app_token
+from extensions.ext_database import db
+from models.account import TenantStatus
+from models.model import App, Site
+
+
+class AppSiteApi(Resource):
+ """Resource for app sites."""
+
+ @validate_app_token
+ @marshal_with(fields.site_fields)
+ def get(self, app_model: App):
+ """Retrieve app site info."""
+ site = db.session.query(Site).filter(Site.app_id == app_model.id).first()
+
+ if not site:
+ raise Forbidden()
+
+ if app_model.tenant.status == TenantStatus.ARCHIVE:
+ raise Forbidden()
+
+ return site
+
+
+api.add_resource(AppSiteApi, "/site")
diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py
index ca3e35aab8..e9bb2b046a 100644
--- a/api/controllers/service_api/app/workflow.py
+++ b/api/controllers/service_api/app/workflow.py
@@ -1,8 +1,8 @@
import logging
from dateutil.parser import isoparse
-from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import Resource, fields, marshal_with, reqparse
+from flask_restful.inputs import int_range
from sqlalchemy.orm import Session
from werkzeug.exceptions import InternalServerError
diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py
index e1e6f3168f..394f36c3ff 100644
--- a/api/controllers/service_api/dataset/dataset.py
+++ b/api/controllers/service_api/dataset/dataset.py
@@ -1,5 +1,5 @@
from flask import request
-from flask_restful import marshal, reqparse # type: ignore
+from flask_restful import marshal, reqparse
from werkzeug.exceptions import Forbidden, NotFound
import services.dataset_service
@@ -313,7 +313,7 @@ class DatasetApi(DatasetApiResource):
try:
if DatasetService.delete_dataset(dataset_id_str, current_user):
DatasetPermissionService.clear_partial_member_list(dataset_id_str)
- return {"result": "success"}, 204
+ return 204
else:
raise NotFound("Dataset not found.")
except services.errors.dataset.DatasetInUseError:
diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py
index 9e943e2b2d..44c75f40ef 100644
--- a/api/controllers/service_api/dataset/document.py
+++ b/api/controllers/service_api/dataset/document.py
@@ -1,11 +1,11 @@
import json
from flask import request
-from flask_restful import marshal, reqparse # type: ignore
-from sqlalchemy import desc
+from flask_restful import marshal, reqparse
+from sqlalchemy import desc, select
from werkzeug.exceptions import NotFound
-import services.dataset_service
+import services
from controllers.common.errors import FilenameNotExistsError
from controllers.service_api import api
from controllers.service_api.app.error import (
@@ -323,7 +323,7 @@ class DocumentDeleteApi(DatasetApiResource):
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Cannot delete document during indexing.")
- return {"result": "success"}, 204
+ return 204
class DocumentListApi(DatasetApiResource):
@@ -337,7 +337,7 @@ class DocumentListApi(DatasetApiResource):
if not dataset:
raise NotFound("Dataset not found.")
- query = Document.query.filter_by(dataset_id=str(dataset_id), tenant_id=tenant_id)
+ query = select(Document).filter_by(dataset_id=str(dataset_id), tenant_id=tenant_id)
if search:
search = f"%{search}%"
@@ -345,7 +345,7 @@ class DocumentListApi(DatasetApiResource):
query = query.order_by(desc(Document.created_at), desc(Document.position))
- paginated_documents = query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
+ paginated_documents = db.paginate(select=query, page=page, per_page=limit, max_per_page=100, error_out=False)
documents = paginated_documents.items
response = {
@@ -374,14 +374,20 @@ class DocumentIndexingStatusApi(DatasetApiResource):
raise NotFound("Documents not found.")
documents_status = []
for document in documents:
- completed_segments = DocumentSegment.query.filter(
- DocumentSegment.completed_at.isnot(None),
- DocumentSegment.document_id == str(document.id),
- DocumentSegment.status != "re_segment",
- ).count()
- total_segments = DocumentSegment.query.filter(
- DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment"
- ).count()
+ completed_segments = (
+ db.session.query(DocumentSegment)
+ .filter(
+ DocumentSegment.completed_at.isnot(None),
+ DocumentSegment.document_id == str(document.id),
+ DocumentSegment.status != "re_segment",
+ )
+ .count()
+ )
+ total_segments = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.document_id == str(document.id), DocumentSegment.status != "re_segment")
+ .count()
+ )
document.completed_segments = completed_segments
document.total_segments = total_segments
if document.is_paused:
diff --git a/api/controllers/service_api/dataset/metadata.py b/api/controllers/service_api/dataset/metadata.py
index 35578eae54..35582feea0 100644
--- a/api/controllers/service_api/dataset/metadata.py
+++ b/api/controllers/service_api/dataset/metadata.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore # type: ignore
-from flask_restful import marshal, reqparse # type: ignore
+from flask_login import current_user # type: ignore
+from flask_restful import marshal, reqparse
from werkzeug.exceptions import NotFound
from controllers.service_api import api
diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py
index 95753cfd67..ea4be4e511 100644
--- a/api/controllers/service_api/dataset/segment.py
+++ b/api/controllers/service_api/dataset/segment.py
@@ -1,6 +1,6 @@
from flask import request
-from flask_login import current_user # type: ignore
-from flask_restful import marshal, reqparse # type: ignore
+from flask_login import current_user
+from flask_restful import marshal, reqparse
from werkzeug.exceptions import NotFound
from controllers.service_api import api
@@ -159,7 +159,7 @@ class DatasetSegmentApi(DatasetApiResource):
if not segment:
raise NotFound("Segment not found.")
SegmentService.delete_segment(segment, document, dataset)
- return {"result": "success"}, 204
+ return 204
@cloud_edition_billing_resource_check("vector_space", "dataset")
def post(self, tenant_id, dataset_id, document_id, segment_id):
@@ -344,7 +344,7 @@ class DatasetChildChunkApi(DatasetApiResource):
except ChildChunkDeleteIndexServiceError as e:
raise ChildChunkDeleteIndexError(str(e))
- return {"result": "success"}, 204
+ return 204
@cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
diff --git a/api/controllers/service_api/index.py b/api/controllers/service_api/index.py
index 75d9141a6d..d24c4597e2 100644
--- a/api/controllers/service_api/index.py
+++ b/api/controllers/service_api/index.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from configs import dify_config
from controllers.service_api import api
diff --git a/api/controllers/service_api/workspace/models.py b/api/controllers/service_api/workspace/models.py
index 373f8019f9..3f18474674 100644
--- a/api/controllers/service_api/workspace/models.py
+++ b/api/controllers/service_api/workspace/models.py
@@ -1,5 +1,5 @@
-from flask_login import current_user # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_login import current_user
+from flask_restful import Resource
from controllers.service_api import api
from controllers.service_api.wraps import validate_dataset_token
diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py
index 7facb03358..cd35ceac1d 100644
--- a/api/controllers/service_api/wraps.py
+++ b/api/controllers/service_api/wraps.py
@@ -7,7 +7,7 @@ from typing import Optional
from flask import current_app, request
from flask_login import user_logged_in # type: ignore
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from pydantic import BaseModel
from sqlalchemy import select, update
from sqlalchemy.orm import Session
diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py
index a84b846112..c9a37af5ed 100644
--- a/api/controllers/web/app.py
+++ b/api/controllers/web/app.py
@@ -1,4 +1,4 @@
-from flask_restful import marshal_with # type: ignore
+from flask_restful import marshal_with
from controllers.common import fields
from controllers.web import api
diff --git a/api/controllers/web/audio.py b/api/controllers/web/audio.py
index 97d980d07c..06d9ad7564 100644
--- a/api/controllers/web/audio.py
+++ b/api/controllers/web/audio.py
@@ -65,7 +65,7 @@ class AudioApi(WebApiResource):
class TextApi(WebApiResource):
def post(self, app_model: App, end_user):
- from flask_restful import reqparse # type: ignore
+ from flask_restful import reqparse
try:
parser = reqparse.RequestParser()
diff --git a/api/controllers/web/completion.py b/api/controllers/web/completion.py
index 9677401490..fd3b9aa804 100644
--- a/api/controllers/web/completion.py
+++ b/api/controllers/web/completion.py
@@ -1,6 +1,6 @@
import logging
-from flask_restful import reqparse # type: ignore
+from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
diff --git a/api/controllers/web/conversation.py b/api/controllers/web/conversation.py
index 419247ea14..98cea3974f 100644
--- a/api/controllers/web/conversation.py
+++ b/api/controllers/web/conversation.py
@@ -1,5 +1,5 @@
-from flask_restful import marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import marshal_with, reqparse
+from flask_restful.inputs import int_range
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
diff --git a/api/controllers/web/feature.py b/api/controllers/web/feature.py
index ce841a8814..0563ed2238 100644
--- a/api/controllers/web/feature.py
+++ b/api/controllers/web/feature.py
@@ -1,4 +1,4 @@
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from controllers.web import api
from services.feature_service import FeatureService
diff --git a/api/controllers/web/files.py b/api/controllers/web/files.py
index 1d4474015a..df06a73a85 100644
--- a/api/controllers/web/files.py
+++ b/api/controllers/web/files.py
@@ -1,5 +1,5 @@
from flask import request
-from flask_restful import marshal_with # type: ignore
+from flask_restful import marshal_with
import services
from controllers.common.errors import FilenameNotExistsError
diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py
index 17e9a3990f..f2e1873601 100644
--- a/api/controllers/web/message.py
+++ b/api/controllers/web/message.py
@@ -1,7 +1,7 @@
import logging
-from flask_restful import fields, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import fields, marshal_with, reqparse
+from flask_restful.inputs import int_range
from werkzeug.exceptions import InternalServerError, NotFound
import services
diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py
index e30998c803..267dac223d 100644
--- a/api/controllers/web/passport.py
+++ b/api/controllers/web/passport.py
@@ -1,7 +1,7 @@
import uuid
from flask import request
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from werkzeug.exceptions import NotFound, Unauthorized
from controllers.web import api
diff --git a/api/controllers/web/remote_files.py b/api/controllers/web/remote_files.py
index d559ab8e07..ae68df6bdc 100644
--- a/api/controllers/web/remote_files.py
+++ b/api/controllers/web/remote_files.py
@@ -1,7 +1,7 @@
import urllib.parse
import httpx
-from flask_restful import marshal_with, reqparse # type: ignore
+from flask_restful import marshal_with, reqparse
import services
from controllers.common import helpers
diff --git a/api/controllers/web/saved_message.py b/api/controllers/web/saved_message.py
index ab2d4abcd3..d7188ef0b3 100644
--- a/api/controllers/web/saved_message.py
+++ b/api/controllers/web/saved_message.py
@@ -1,5 +1,5 @@
-from flask_restful import fields, marshal_with, reqparse # type: ignore
-from flask_restful.inputs import int_range # type: ignore
+from flask_restful import fields, marshal_with, reqparse
+from flask_restful.inputs import int_range
from werkzeug.exceptions import NotFound
from controllers.web import api
diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py
index e68dc7aa4a..0564b15ea3 100644
--- a/api/controllers/web/site.py
+++ b/api/controllers/web/site.py
@@ -1,4 +1,4 @@
-from flask_restful import fields, marshal_with # type: ignore
+from flask_restful import fields, marshal_with
from werkzeug.exceptions import Forbidden
from configs import dify_config
diff --git a/api/controllers/web/workflow.py b/api/controllers/web/workflow.py
index d2e183be78..590fd3f2c7 100644
--- a/api/controllers/web/workflow.py
+++ b/api/controllers/web/workflow.py
@@ -1,6 +1,6 @@
import logging
-from flask_restful import reqparse # type: ignore
+from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError
from controllers.web import api
diff --git a/api/controllers/web/wraps.py b/api/controllers/web/wraps.py
index 1b4d263bee..c327c3df18 100644
--- a/api/controllers/web/wraps.py
+++ b/api/controllers/web/wraps.py
@@ -1,7 +1,7 @@
from functools import wraps
from flask import request
-from flask_restful import Resource # type: ignore
+from flask_restful import Resource
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
from controllers.web.error import WebSSOAuthRequiredError
diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py
index e648613605..6998e4d29a 100644
--- a/api/core/agent/base_agent_runner.py
+++ b/api/core/agent/base_agent_runner.py
@@ -91,6 +91,8 @@ class BaseAgentRunner(AppRunner):
return_resource=app_config.additional_features.show_retrieve_source,
invoke_from=application_generate_entity.invoke_from,
hit_callback=hit_callback,
+ user_id=user_id,
+ inputs=cast(dict, application_generate_entity.inputs),
)
# get how many agent thoughts have been created
self.agent_thought_count = (
diff --git a/api/core/agent/cot_agent_runner.py b/api/core/agent/cot_agent_runner.py
index feb8abf6ef..5212d797d8 100644
--- a/api/core/agent/cot_agent_runner.py
+++ b/api/core/agent/cot_agent_runner.py
@@ -80,6 +80,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
llm_usage = final_llm_usage_dict["usage"]
llm_usage.prompt_tokens += usage.prompt_tokens
llm_usage.completion_tokens += usage.completion_tokens
+ llm_usage.total_tokens += usage.total_tokens
llm_usage.prompt_price += usage.prompt_price
llm_usage.completion_price += usage.completion_price
llm_usage.total_price += usage.total_price
diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py
index a1110e7709..611a55b30a 100644
--- a/api/core/agent/fc_agent_runner.py
+++ b/api/core/agent/fc_agent_runner.py
@@ -65,6 +65,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
llm_usage = final_llm_usage_dict["usage"]
llm_usage.prompt_tokens += usage.prompt_tokens
llm_usage.completion_tokens += usage.completion_tokens
+ llm_usage.total_tokens += usage.total_tokens
llm_usage.prompt_price += usage.prompt_price
llm_usage.completion_price += usage.completion_price
llm_usage.total_price += usage.total_price
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/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py
index 6079b51daa..4b0e64130b 100644
--- a/api/core/app/apps/advanced_chat/app_generator.py
+++ b/api/core/app/apps/advanced_chat/app_generator.py
@@ -25,8 +25,8 @@ from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotA
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
from core.prompt.utils.get_thread_messages_length import get_thread_messages_length
-from core.repository import RepositoryFactory
-from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
+from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
+from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db
from factories import file_factory
from models.account import Account
@@ -163,12 +163,10 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
- workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
- params={
- "tenant_id": application_generate_entity.app_config.tenant_id,
- "app_id": application_generate_entity.app_config.app_id,
- "session_factory": session_factory,
- }
+ workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
+ session_factory=session_factory,
+ tenant_id=application_generate_entity.app_config.tenant_id,
+ app_id=application_generate_entity.app_config.app_id,
)
return self._generate(
@@ -231,12 +229,10 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
- workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
- params={
- "tenant_id": application_generate_entity.app_config.tenant_id,
- "app_id": application_generate_entity.app_config.app_id,
- "session_factory": session_factory,
- }
+ workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
+ session_factory=session_factory,
+ tenant_id=application_generate_entity.app_config.tenant_id,
+ app_id=application_generate_entity.app_config.app_id,
)
return self._generate(
@@ -297,12 +293,10 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
- workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
- params={
- "tenant_id": application_generate_entity.app_config.tenant_id,
- "app_id": application_generate_entity.app_config.app_id,
- "session_factory": session_factory,
- }
+ workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
+ session_factory=session_factory,
+ tenant_id=application_generate_entity.app_config.tenant_id,
+ app_id=application_generate_entity.app_config.app_id,
)
return self._generate(
diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py
index 43ccaea9c0..f71c49d112 100644
--- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py
+++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py
@@ -9,7 +9,6 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME
-from core.app.apps.advanced_chat.app_generator_tts_publisher import AppGeneratorTTSPublisher, AudioTrunk
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.entities.app_invoke_entities import (
AdvancedChatAppGenerateEntity,
@@ -58,14 +57,15 @@ from core.app.entities.task_entities import (
)
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.message_cycle_manage import MessageCycleManage
-from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
+from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.ops_trace_manager import TraceQueueManager
-from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes import NodeType
+from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
+from core.workflow.workflow_cycle_manager import WorkflowCycleManager
from events.message_event import message_was_created
from extensions.ext_database import db
from models import Conversation, EndUser, Message, MessageFile
@@ -113,7 +113,7 @@ class AdvancedChatAppGenerateTaskPipeline:
else:
raise NotImplementedError(f"User type not supported: {type(user)}")
- self._workflow_cycle_manager = WorkflowCycleManage(
+ self._workflow_cycle_manager = WorkflowCycleManager(
application_generate_entity=application_generate_entity,
workflow_system_variables={
SystemVariableKey.QUERY: message.query,
diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py
index 6be3a7331d..1d67671974 100644
--- a/api/core/app/apps/workflow/app_generator.py
+++ b/api/core/app/apps/workflow/app_generator.py
@@ -18,13 +18,13 @@ from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
from core.app.apps.workflow.app_queue_manager import WorkflowAppQueueManager
from core.app.apps.workflow.app_runner import WorkflowAppRunner
from core.app.apps.workflow.generate_response_converter import WorkflowAppGenerateResponseConverter
-from core.app.apps.workflow.generate_task_pipeline import WorkflowAppGenerateTaskPipeline
from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity
from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
-from core.repository import RepositoryFactory
-from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
+from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
+from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
+from core.workflow.workflow_app_generate_task_pipeline import WorkflowAppGenerateTaskPipeline
from extensions.ext_database import db
from factories import file_factory
from models import Account, App, EndUser, Workflow
@@ -138,12 +138,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
- workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
- params={
- "tenant_id": application_generate_entity.app_config.tenant_id,
- "app_id": application_generate_entity.app_config.app_id,
- "session_factory": session_factory,
- }
+ workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
+ session_factory=session_factory,
+ tenant_id=application_generate_entity.app_config.tenant_id,
+ app_id=application_generate_entity.app_config.app_id,
)
return self._generate(
@@ -264,12 +262,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
- workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
- params={
- "tenant_id": application_generate_entity.app_config.tenant_id,
- "app_id": application_generate_entity.app_config.app_id,
- "session_factory": session_factory,
- }
+ workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
+ session_factory=session_factory,
+ tenant_id=application_generate_entity.app_config.tenant_id,
+ app_id=application_generate_entity.app_config.app_id,
)
return self._generate(
@@ -329,12 +325,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
# Create workflow node execution repository
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
- workflow_node_execution_repository = RepositoryFactory.create_workflow_node_execution_repository(
- params={
- "tenant_id": application_generate_entity.app_config.tenant_id,
- "app_id": application_generate_entity.app_config.app_id,
- "session_factory": session_factory,
- }
+ workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
+ session_factory=session_factory,
+ tenant_id=application_generate_entity.app_config.tenant_id,
+ app_id=application_generate_entity.app_config.app_id,
)
return self._generate(
diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py
index 8c9c26d36e..a98a42f5df 100644
--- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py
+++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py
@@ -9,7 +9,6 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME
-from core.app.apps.advanced_chat.app_generator_tts_publisher import AppGeneratorTTSPublisher, AudioTrunk
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.entities.app_invoke_entities import (
AgentChatAppGenerateEntity,
@@ -45,6 +44,7 @@ from core.app.entities.task_entities import (
)
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.message_cycle_manage import MessageCycleManage
+from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk
from core.model_manager import ModelInstance
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
from core.model_runtime.entities.message_entities import (
diff --git a/api/core/app/task_pipeline/message_cycle_manage.py b/api/core/app/task_pipeline/message_cycle_manage.py
index fde506639f..a6d826f08b 100644
--- a/api/core/app/task_pipeline/message_cycle_manage.py
+++ b/api/core/app/task_pipeline/message_cycle_manage.py
@@ -24,7 +24,7 @@ from core.app.entities.task_entities import (
WorkflowTaskState,
)
from core.llm_generator.llm_generator import LLMGenerator
-from core.tools.tool_file_manager import ToolFileManager
+from core.tools.signature import sign_tool_file
from extensions.ext_database import db
from models.model import AppMode, Conversation, MessageAnnotation, MessageFile
from services.annotation_service import AppAnnotationService
@@ -154,7 +154,7 @@ class MessageCycleManage:
if message_file.url.startswith("http"):
url = message_file.url
else:
- url = ToolFileManager.sign_file(tool_file_id=tool_file_id, extension=extension)
+ url = sign_tool_file(tool_file_id=tool_file_id, extension=extension)
return MessageFileStreamResponse(
task_id=self._application_generate_entity.task_id,
diff --git a/api/core/base/__init__.py b/api/core/base/__init__.py
new file mode 100644
index 0000000000..3f4bd3b771
--- /dev/null
+++ b/api/core/base/__init__.py
@@ -0,0 +1 @@
+# Core base package
diff --git a/api/core/base/tts/__init__.py b/api/core/base/tts/__init__.py
new file mode 100644
index 0000000000..37b6eeebb0
--- /dev/null
+++ b/api/core/base/tts/__init__.py
@@ -0,0 +1,6 @@
+from core.base.tts.app_generator_tts_publisher import AppGeneratorTTSPublisher, AudioTrunk
+
+__all__ = [
+ "AppGeneratorTTSPublisher",
+ "AudioTrunk",
+]
diff --git a/api/core/app/apps/advanced_chat/app_generator_tts_publisher.py b/api/core/base/tts/app_generator_tts_publisher.py
similarity index 100%
rename from api/core/app/apps/advanced_chat/app_generator_tts_publisher.py
rename to api/core/base/tts/app_generator_tts_publisher.py
diff --git a/api/core/callback_handler/index_tool_callback_handler.py b/api/core/callback_handler/index_tool_callback_handler.py
index 56859df7f4..13c22213c4 100644
--- a/api/core/callback_handler/index_tool_callback_handler.py
+++ b/api/core/callback_handler/index_tool_callback_handler.py
@@ -1,3 +1,5 @@
+import logging
+
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import QueueRetrieverResourcesEvent
@@ -7,6 +9,8 @@ from extensions.ext_database import db
from models.dataset import ChildChunk, DatasetQuery, DocumentSegment
from models.dataset import Document as DatasetDocument
+_logger = logging.getLogger(__name__)
+
class DatasetIndexToolCallbackHandler:
"""Callback handler for dataset tool."""
@@ -42,18 +46,31 @@ class DatasetIndexToolCallbackHandler:
"""Handle tool end."""
for document in documents:
if document.metadata is not None:
- dataset_document = DatasetDocument.query.filter(
- DatasetDocument.id == document.metadata["document_id"]
- ).first()
+ document_id = document.metadata["document_id"]
+ dataset_document = db.session.query(DatasetDocument).filter(DatasetDocument.id == document_id).first()
+ if not dataset_document:
+ _logger.warning(
+ "Expected DatasetDocument record to exist, but none was found, document_id=%s",
+ document_id,
+ )
+ continue
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
- child_chunk = ChildChunk.query.filter(
- ChildChunk.index_node_id == document.metadata["doc_id"],
- ChildChunk.dataset_id == dataset_document.dataset_id,
- ChildChunk.document_id == dataset_document.id,
- ).first()
+ child_chunk = (
+ db.session.query(ChildChunk)
+ .filter(
+ ChildChunk.index_node_id == document.metadata["doc_id"],
+ ChildChunk.dataset_id == dataset_document.dataset_id,
+ ChildChunk.document_id == dataset_document.id,
+ )
+ .first()
+ )
if child_chunk:
- segment = DocumentSegment.query.filter(DocumentSegment.id == child_chunk.segment_id).update(
- {DocumentSegment.hit_count: DocumentSegment.hit_count + 1}, synchronize_session=False
+ segment = (
+ db.session.query(DocumentSegment)
+ .filter(DocumentSegment.id == child_chunk.segment_id)
+ .update(
+ {DocumentSegment.hit_count: DocumentSegment.hit_count + 1}, synchronize_session=False
+ )
)
else:
query = db.session.query(DocumentSegment).filter(
diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py
index 86887c9b4a..81d582fb9a 100644
--- a/api/core/entities/provider_configuration.py
+++ b/api/core/entities/provider_configuration.py
@@ -897,37 +897,36 @@ class ProviderConfiguration(BaseModel):
)
except Exception as ex:
logger.warning(f"get custom model schema failed, {ex}")
+ continue
- if not custom_model_schema:
- continue
+ if not custom_model_schema:
+ continue
- if custom_model_schema.model_type not in model_types:
- continue
+ if custom_model_schema.model_type not in model_types:
+ continue
- status = ModelStatus.ACTIVE
- if (
- custom_model_schema.model_type in model_setting_map
- and custom_model_schema.model in model_setting_map[custom_model_schema.model_type]
- ):
- model_setting = model_setting_map[custom_model_schema.model_type][
- custom_model_schema.model
- ]
- if model_setting.enabled is False:
- status = ModelStatus.DISABLED
+ status = ModelStatus.ACTIVE
+ if (
+ custom_model_schema.model_type in model_setting_map
+ and custom_model_schema.model in model_setting_map[custom_model_schema.model_type]
+ ):
+ model_setting = model_setting_map[custom_model_schema.model_type][custom_model_schema.model]
+ if model_setting.enabled is False:
+ status = ModelStatus.DISABLED
- provider_models.append(
- ModelWithProviderEntity(
- model=custom_model_schema.model,
- label=custom_model_schema.label,
- model_type=custom_model_schema.model_type,
- features=custom_model_schema.features,
- fetch_from=FetchFrom.PREDEFINED_MODEL,
- model_properties=custom_model_schema.model_properties,
- deprecated=custom_model_schema.deprecated,
- provider=SimpleModelProviderEntity(self.provider),
- status=status,
- )
+ provider_models.append(
+ ModelWithProviderEntity(
+ model=custom_model_schema.model,
+ label=custom_model_schema.label,
+ model_type=custom_model_schema.model_type,
+ features=custom_model_schema.features,
+ fetch_from=FetchFrom.PREDEFINED_MODEL,
+ model_properties=custom_model_schema.model_properties,
+ deprecated=custom_model_schema.deprecated,
+ provider=SimpleModelProviderEntity(self.provider),
+ status=status,
)
+ )
# if llm name not in restricted llm list, remove it
restrict_model_names = [rm.model for rm in restrict_models]
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/file/file_manager.py b/api/core/file/file_manager.py
index 9a204e9ff6..ada19ef8ce 100644
--- a/api/core/file/file_manager.py
+++ b/api/core/file/file_manager.py
@@ -10,12 +10,12 @@ from core.model_runtime.entities import (
VideoPromptMessageContent,
)
from core.model_runtime.entities.message_entities import PromptMessageContentUnionTypes
+from core.tools.signature import sign_tool_file
from extensions.ext_storage import storage
from . import helpers
from .enums import FileAttribute
from .models import File, FileTransferMethod, FileType
-from .tool_file_parser import ToolFileParser
def get_attr(*, file: File, attr: FileAttribute):
@@ -130,6 +130,6 @@ def _to_url(f: File, /):
# add sign url
if f.related_id is None or f.extension is None:
raise ValueError("Missing file related_id or extension")
- return ToolFileParser.get_tool_file_manager().sign_file(tool_file_id=f.related_id, extension=f.extension)
+ return sign_tool_file(tool_file_id=f.related_id, extension=f.extension)
else:
raise ValueError(f"Unsupported transfer method: {f.transfer_method}")
diff --git a/api/core/file/models.py b/api/core/file/models.py
index f5db6c2d74..aa3b5f629c 100644
--- a/api/core/file/models.py
+++ b/api/core/file/models.py
@@ -4,11 +4,11 @@ from typing import Any, Optional
from pydantic import BaseModel, Field, model_validator
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
+from core.tools.signature import sign_tool_file
from . import helpers
from .constants import FILE_MODEL_IDENTITY
from .enums import FileTransferMethod, FileType
-from .tool_file_parser import ToolFileParser
class ImageConfig(BaseModel):
@@ -34,13 +34,21 @@ class FileUploadConfig(BaseModel):
class File(BaseModel):
+ # NOTE: dify_model_identity is a special identifier used to distinguish between
+ # new and old data formats during serialization and deserialization.
dify_model_identity: str = FILE_MODEL_IDENTITY
id: Optional[str] = None # message file id
tenant_id: str
type: FileType
transfer_method: FileTransferMethod
+ # If `transfer_method` is `FileTransferMethod.remote_url`, the
+ # `remote_url` attribute must not be `None`.
remote_url: Optional[str] = None # remote url
+ # If `transfer_method` is `FileTransferMethod.local_file` or
+ # `FileTransferMethod.tool_file`, the `related_id` attribute must not be `None`.
+ #
+ # It should be set to `ToolFile.id` when `transfer_method` is `tool_file`.
related_id: Optional[str] = None
filename: Optional[str] = None
extension: Optional[str] = Field(default=None, description="File extension, should contains dot")
@@ -110,9 +118,7 @@ class File(BaseModel):
elif self.transfer_method == FileTransferMethod.TOOL_FILE:
assert self.related_id is not None
assert self.extension is not None
- return ToolFileParser.get_tool_file_manager().sign_file(
- tool_file_id=self.related_id, extension=self.extension
- )
+ return sign_tool_file(tool_file_id=self.related_id, extension=self.extension)
def to_plugin_parameter(self) -> dict[str, Any]:
return {
diff --git a/api/core/file/tool_file_parser.py b/api/core/file/tool_file_parser.py
index 6fa101cf36..656c9d48ed 100644
--- a/api/core/file/tool_file_parser.py
+++ b/api/core/file/tool_file_parser.py
@@ -1,12 +1,19 @@
-from typing import TYPE_CHECKING, Any, cast
+from collections.abc import Callable
+from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.tools.tool_file_manager import ToolFileManager
-tool_file_manager: dict[str, Any] = {"manager": None}
+_tool_file_manager_factory: Callable[[], "ToolFileManager"] | None = None
class ToolFileParser:
@staticmethod
def get_tool_file_manager() -> "ToolFileManager":
- return cast("ToolFileManager", tool_file_manager["manager"])
+ assert _tool_file_manager_factory is not None
+ return _tool_file_manager_factory()
+
+
+def set_tool_file_manager_factory(factory: Callable[[], "ToolFileManager"]) -> None:
+ global _tool_file_manager_factory
+ _tool_file_manager_factory = factory
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/indexing_runner.py b/api/core/indexing_runner.py
index a75a4c22d1..848d897779 100644
--- a/api/core/indexing_runner.py
+++ b/api/core/indexing_runner.py
@@ -9,7 +9,7 @@ import uuid
from typing import Any, Optional, cast
from flask import current_app
-from flask_login import current_user # type: ignore
+from flask_login import current_user
from sqlalchemy.orm.exc import ObjectDeletedError
from configs import dify_config
@@ -51,7 +51,7 @@ class IndexingRunner:
for dataset_document in dataset_documents:
try:
# get dataset
- dataset = Dataset.query.filter_by(id=dataset_document.dataset_id).first()
+ dataset = db.session.query(Dataset).filter_by(id=dataset_document.dataset_id).first()
if not dataset:
raise ValueError("no dataset found")
@@ -103,15 +103,17 @@ class IndexingRunner:
"""Run the indexing process when the index_status is splitting."""
try:
# get dataset
- dataset = Dataset.query.filter_by(id=dataset_document.dataset_id).first()
+ dataset = db.session.query(Dataset).filter_by(id=dataset_document.dataset_id).first()
if not dataset:
raise ValueError("no dataset found")
# get exist document_segment list and delete
- document_segments = DocumentSegment.query.filter_by(
- dataset_id=dataset.id, document_id=dataset_document.id
- ).all()
+ document_segments = (
+ db.session.query(DocumentSegment)
+ .filter_by(dataset_id=dataset.id, document_id=dataset_document.id)
+ .all()
+ )
for document_segment in document_segments:
db.session.delete(document_segment)
@@ -162,15 +164,17 @@ class IndexingRunner:
"""Run the indexing process when the index_status is indexing."""
try:
# get dataset
- dataset = Dataset.query.filter_by(id=dataset_document.dataset_id).first()
+ dataset = db.session.query(Dataset).filter_by(id=dataset_document.dataset_id).first()
if not dataset:
raise ValueError("no dataset found")
# get exist document_segment list and delete
- document_segments = DocumentSegment.query.filter_by(
- dataset_id=dataset.id, document_id=dataset_document.id
- ).all()
+ document_segments = (
+ db.session.query(DocumentSegment)
+ .filter_by(dataset_id=dataset.id, document_id=dataset_document.id)
+ .all()
+ )
documents = []
if document_segments:
@@ -254,7 +258,7 @@ class IndexingRunner:
embedding_model_instance = None
if dataset_id:
- dataset = Dataset.query.filter_by(id=dataset_id).first()
+ dataset = db.session.query(Dataset).filter_by(id=dataset_id).first()
if not dataset:
raise ValueError("Dataset not found.")
if dataset.indexing_technique == "high_quality" or indexing_technique == "high_quality":
@@ -587,7 +591,7 @@ class IndexingRunner:
@staticmethod
def _process_keyword_index(flask_app, dataset_id, document_id, documents):
with flask_app.app_context():
- dataset = Dataset.query.filter_by(id=dataset_id).first()
+ dataset = db.session.query(Dataset).filter_by(id=dataset_id).first()
if not dataset:
raise ValueError("no dataset found")
keyword = Keyword(dataset)
@@ -656,10 +660,10 @@ class IndexingRunner:
"""
Update the document indexing status.
"""
- count = DatasetDocument.query.filter_by(id=document_id, is_paused=True).count()
+ count = db.session.query(DatasetDocument).filter_by(id=document_id, is_paused=True).count()
if count > 0:
raise DocumentIsPausedError()
- document = DatasetDocument.query.filter_by(id=document_id).first()
+ document = db.session.query(DatasetDocument).filter_by(id=document_id).first()
if not document:
raise DocumentIsDeletedPausedError()
@@ -668,7 +672,7 @@ class IndexingRunner:
if extra_update_params:
update_params.update(extra_update_params)
- DatasetDocument.query.filter_by(id=document_id).update(update_params)
+ db.session.query(DatasetDocument).filter_by(id=document_id).update(update_params)
db.session.commit()
@staticmethod
@@ -676,7 +680,7 @@ class IndexingRunner:
"""
Update the document segment by document id.
"""
- DocumentSegment.query.filter_by(document_id=dataset_document_id).update(update_params)
+ db.session.query(DocumentSegment).filter_by(document_id=dataset_document_id).update(update_params)
db.session.commit()
def _transform(
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