mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-19 05:05:53 +08:00
Merge branch 'main' into feat/structured-output
This commit is contained in:
commit
26fc76e705
2
.github/DISCUSSION_TEMPLATE/general.yml
vendored
2
.github/DISCUSSION_TEMPLATE/general.yml
vendored
@ -9,7 +9,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/DISCUSSION_TEMPLATE/help.yml
vendored
2
.github/DISCUSSION_TEMPLATE/help.yml
vendored
@ -9,7 +9,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/DISCUSSION_TEMPLATE/suggestion.yml
vendored
2
.github/DISCUSSION_TEMPLATE/suggestion.yml
vendored
@ -9,7 +9,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -14,7 +14,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/ISSUE_TEMPLATE/document_issue.yml
vendored
2
.github/ISSUE_TEMPLATE/document_issue.yml
vendored
@ -12,7 +12,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -12,7 +12,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/ISSUE_TEMPLATE/tracker.yml
vendored
2
.github/ISSUE_TEMPLATE/tracker.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: "👾 Tracker"
|
name: "👾 Tracker"
|
||||||
description: For inner usages, please donot use this template.
|
description: For inner usages, please do not use this template.
|
||||||
title: "[Tracker] "
|
title: "[Tracker] "
|
||||||
labels:
|
labels:
|
||||||
- tracker
|
- tracker
|
||||||
|
4
.github/ISSUE_TEMPLATE/translation_issue.yml
vendored
4
.github/ISSUE_TEMPLATE/translation_issue.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: "🌐 Localization/Translation issue"
|
name: "🌐 Localization/Translation issue"
|
||||||
description: Report incorrect translations. [please use English :)]
|
description: Report incorrect translations. [please use English :)]
|
||||||
labels:
|
labels:
|
||||||
- translation
|
- translation
|
||||||
body:
|
body:
|
||||||
@ -12,7 +12,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
- label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)).
|
||||||
required: true
|
required: true
|
||||||
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
- label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)"
|
||||||
required: true
|
required: true
|
||||||
- label: "Please do not modify this template :) and fill in all the required fields."
|
- label: "Please do not modify this template :) and fill in all the required fields."
|
||||||
required: true
|
required: true
|
||||||
|
@ -204,7 +204,9 @@ If you'd like to configure a highly-available setup, there are community-contrib
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Using Terraform for Deployment
|
#### Using Terraform for Deployment
|
||||||
|
|
||||||
|
@ -187,7 +187,9 @@ docker compose up -d
|
|||||||
|
|
||||||
- [رسم بياني Helm من قبل @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [رسم بياني Helm من قبل @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [رسم بياني Helm من قبل @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [رسم بياني Helm من قبل @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [رسم بياني Helm من قبل @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [ملف YAML من قبل @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### استخدام Terraform للتوزيع
|
#### استخدام Terraform للتوزيع
|
||||||
|
|
||||||
|
@ -203,7 +203,9 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### টেরাফর্ম ব্যবহার করে ডিপ্লয়
|
#### টেরাফর্ম ব্যবহার করে ডিপ্লয়
|
||||||
|
|
||||||
|
@ -205,7 +205,9 @@ docker compose up -d
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### 使用 Terraform 部署
|
#### 使用 Terraform 部署
|
||||||
|
|
||||||
|
@ -205,7 +205,9 @@ Falls Sie eine hochverfügbare Konfiguration einrichten möchten, gibt es von de
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Terraform für die Bereitstellung verwenden
|
#### Terraform für die Bereitstellung verwenden
|
||||||
|
|
||||||
|
@ -77,9 +77,7 @@ Dify es una plataforma de desarrollo de aplicaciones de LLM de código abierto.
|
|||||||
Amplias capacidades de RAG que cubren todo, desde la ingestión de documentos hasta la recuperación, con soporte listo para usar para la extracción de texto de PDF, PPT y otros formatos de documento comunes.
|
Amplias capacidades de RAG que cubren todo, desde la ingestión de documentos hasta la recuperación, con soporte listo para usar para la extracción de texto de PDF, PPT y otros formatos de documento comunes.
|
||||||
|
|
||||||
**5. Capacidades de agente**:
|
**5. Capacidades de agente**:
|
||||||
Puedes definir agent
|
Puedes definir agentes basados en LLM Function Calling o ReAct, y agregar herramientas preconstruidas o personalizadas para el agente. Dify proporciona más de 50 herramientas integradas para agentes de IA, como Búsqueda de Google, DALL·E, Difusión Estable y WolframAlpha.
|
||||||
|
|
||||||
es basados en LLM Function Calling o ReAct, y agregar herramientas preconstruidas o personalizadas para el agente. Dify proporciona más de 50 herramientas integradas para agentes de IA, como Búsqueda de Google, DALL·E, Difusión Estable y WolframAlpha.
|
|
||||||
|
|
||||||
**6. LLMOps**:
|
**6. LLMOps**:
|
||||||
Supervisa y analiza registros de aplicaciones y rendimiento a lo largo del tiempo. Podrías mejorar continuamente prompts, conjuntos de datos y modelos basados en datos de producción y anotaciones.
|
Supervisa y analiza registros de aplicaciones y rendimiento a lo largo del tiempo. Podrías mejorar continuamente prompts, conjuntos de datos y modelos basados en datos de producción y anotaciones.
|
||||||
@ -207,7 +205,9 @@ Si desea configurar una configuración de alta disponibilidad, la comunidad prop
|
|||||||
|
|
||||||
- [Gráfico Helm por @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Gráfico Helm por @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Gráfico Helm por @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Gráfico Helm por @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Gráfico Helm por @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [Ficheros YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Uso de Terraform para el despliegue
|
#### Uso de Terraform para el despliegue
|
||||||
|
|
||||||
|
@ -203,7 +203,9 @@ Si vous souhaitez configurer une configuration haute disponibilité, la communau
|
|||||||
|
|
||||||
- [Helm Chart par @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart par @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart par @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart par @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart par @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [Fichier YAML par @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Utilisation de Terraform pour le déploiement
|
#### Utilisation de Terraform pour le déploiement
|
||||||
|
|
||||||
|
@ -204,7 +204,9 @@ docker compose up -d
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Terraformを使用したデプロイ
|
#### Terraformを使用したデプロイ
|
||||||
|
|
||||||
|
@ -203,7 +203,9 @@ If you'd like to configure a highly-available setup, there are community-contrib
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Terraform atorlugu pilersitsineq
|
#### Terraform atorlugu pilersitsineq
|
||||||
|
|
||||||
|
@ -197,7 +197,9 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
|
|||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
- [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Terraform을 사용한 배포
|
#### Terraform을 사용한 배포
|
||||||
|
|
||||||
|
@ -203,7 +203,9 @@ Se deseja configurar uma instalação de alta disponibilidade, há [Helm Charts]
|
|||||||
|
|
||||||
- [Helm Chart de @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart de @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart de @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart de @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
- [Arquivo YAML de @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [Helm Chart de @magicsong](https://github.com/magicsong/ai-charts)
|
||||||
|
- [Arquivo YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [Arquivo YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Usando o Terraform para Implantação
|
#### Usando o Terraform para Implantação
|
||||||
|
|
||||||
|
@ -205,6 +205,7 @@ Star Dify on GitHub and be instantly notified of new releases.
|
|||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [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 @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
|
#### Uporaba Terraform za uvajanje
|
||||||
|
|
||||||
|
@ -198,6 +198,7 @@ Yüksek kullanılabilirliğe sahip bir kurulum yapılandırmak isterseniz, Dify'
|
|||||||
- [@LeoQuote tarafından Helm Chart](https://github.com/douban/charts/tree/master/charts/dify)
|
- [@LeoQuote tarafından Helm Chart](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [@BorisPolonsky tarafından Helm Chart](https://github.com/BorisPolonsky/dify-helm)
|
- [@BorisPolonsky tarafından Helm Chart](https://github.com/BorisPolonsky/dify-helm)
|
||||||
- [@Winson-030 tarafından YAML dosyası](https://github.com/Winson-030/dify-kubernetes)
|
- [@Winson-030 tarafından YAML dosyası](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [@wyy-holding tarafından YAML dosyası](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Dağıtım için Terraform Kullanımı
|
#### Dağıtım için Terraform Kullanımı
|
||||||
|
|
||||||
|
@ -204,6 +204,7 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify
|
|||||||
- [由 @LeoQuote 提供的 Helm Chart](https://github.com/douban/charts/tree/master/charts/dify)
|
- [由 @LeoQuote 提供的 Helm Chart](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [由 @BorisPolonsky 提供的 Helm Chart](https://github.com/BorisPolonsky/dify-helm)
|
- [由 @BorisPolonsky 提供的 Helm Chart](https://github.com/BorisPolonsky/dify-helm)
|
||||||
- [由 @Winson-030 提供的 YAML 文件](https://github.com/Winson-030/dify-kubernetes)
|
- [由 @Winson-030 提供的 YAML 文件](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [由 @wyy-holding 提供的 YAML 文件](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
### 使用 Terraform 進行部署
|
### 使用 Terraform 進行部署
|
||||||
|
|
||||||
|
@ -200,6 +200,7 @@ Nếu bạn muốn cấu hình một cài đặt có độ sẵn sàng cao, có
|
|||||||
- [Helm Chart bởi @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart bởi @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart bởi @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart bởi @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
- [Tệp YAML bởi @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
- [Tệp YAML bởi @Winson-030](https://github.com/Winson-030/dify-kubernetes)
|
||||||
|
- [Tệp YAML bởi @wyy-holding](https://github.com/wyy-holding/dify-k8s)
|
||||||
|
|
||||||
#### Sử dụng Terraform để Triển khai
|
#### Sử dụng Terraform để Triển khai
|
||||||
|
|
||||||
|
@ -26,9 +26,6 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60
|
|||||||
# Refresh token expiration time in days
|
# Refresh token expiration time in days
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS=30
|
REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||||
|
|
||||||
# celery configuration
|
|
||||||
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
|
|
||||||
|
|
||||||
# redis configuration
|
# redis configuration
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
@ -50,6 +47,9 @@ REDIS_USE_CLUSTERS=false
|
|||||||
REDIS_CLUSTERS=
|
REDIS_CLUSTERS=
|
||||||
REDIS_CLUSTERS_PASSWORD=
|
REDIS_CLUSTERS_PASSWORD=
|
||||||
|
|
||||||
|
# celery configuration
|
||||||
|
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
|
||||||
|
|
||||||
# PostgreSQL database configuration
|
# PostgreSQL database configuration
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=difyai123456
|
DB_PASSWORD=difyai123456
|
||||||
|
@ -37,6 +37,12 @@ select = [
|
|||||||
"UP", # pyupgrade rules
|
"UP", # pyupgrade rules
|
||||||
"W191", # tab-indentation
|
"W191", # tab-indentation
|
||||||
"W605", # invalid-escape-sequence
|
"W605", # invalid-escape-sequence
|
||||||
|
# security related linting rules
|
||||||
|
# RCE proctection (sort of)
|
||||||
|
"S102", # exec-builtin, disallow use of `exec`
|
||||||
|
"S307", # suspicious-eval-usage, disallow use of `eval` and `ast.literal_eval`
|
||||||
|
"S301", # suspicious-pickle-usage, disallow use of `pickle` and its wrappers.
|
||||||
|
"S302", # suspicious-marshal-usage, disallow use of `marshal` module
|
||||||
]
|
]
|
||||||
|
|
||||||
ignore = [
|
ignore = [
|
||||||
|
105
api/commands.py
105
api/commands.py
@ -12,6 +12,7 @@ from configs import dify_config
|
|||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from core.rag.datasource.vdb.vector_factory import Vector
|
from core.rag.datasource.vdb.vector_factory import Vector
|
||||||
from core.rag.datasource.vdb.vector_type import VectorType
|
from core.rag.datasource.vdb.vector_type import VectorType
|
||||||
|
from core.rag.index_processor.constant.built_in_field import BuiltInField
|
||||||
from core.rag.models.document import Document
|
from core.rag.models.document import Document
|
||||||
from events.app_event import app_was_created
|
from events.app_event import app_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -20,11 +21,12 @@ from libs.helper import email as email_validate
|
|||||||
from libs.password import hash_password, password_pattern, valid_password
|
from libs.password import hash_password, password_pattern, valid_password
|
||||||
from libs.rsa import generate_key_pair
|
from libs.rsa import generate_key_pair
|
||||||
from models import Tenant
|
from models import Tenant
|
||||||
from models.dataset import Dataset, DatasetCollectionBinding, DocumentSegment
|
from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, DatasetMetadataBinding, DocumentSegment
|
||||||
from models.dataset import Document as DatasetDocument
|
from models.dataset import Document as DatasetDocument
|
||||||
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
|
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
|
||||||
from models.provider import Provider, ProviderModel
|
from models.provider import Provider, ProviderModel
|
||||||
from services.account_service import RegisterService, TenantService
|
from services.account_service import RegisterService, TenantService
|
||||||
|
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
|
||||||
from services.plugin.data_migration import PluginDataMigration
|
from services.plugin.data_migration import PluginDataMigration
|
||||||
from services.plugin.plugin_migration import PluginMigration
|
from services.plugin.plugin_migration import PluginMigration
|
||||||
|
|
||||||
@ -483,14 +485,11 @@ def convert_to_agent_apps():
|
|||||||
click.echo(click.style("Conversion complete. Converted {} agent apps.".format(len(proceeded_app_ids)), fg="green"))
|
click.echo(click.style("Conversion complete. Converted {} agent apps.".format(len(proceeded_app_ids)), fg="green"))
|
||||||
|
|
||||||
|
|
||||||
@click.command("add-qdrant-doc-id-index", help="Add Qdrant doc_id index.")
|
@click.command("add-qdrant-index", help="Add Qdrant index.")
|
||||||
@click.option("--field", default="metadata.doc_id", prompt=False, help="Index field , default is metadata.doc_id.")
|
@click.option("--field", default="metadata.doc_id", prompt=False, help="Index field , default is metadata.doc_id.")
|
||||||
def add_qdrant_doc_id_index(field: str):
|
def add_qdrant_index(field: str):
|
||||||
click.echo(click.style("Starting Qdrant doc_id index creation.", fg="green"))
|
click.echo(click.style("Starting Qdrant index creation.", fg="green"))
|
||||||
vector_type = dify_config.VECTOR_STORE
|
|
||||||
if vector_type != "qdrant":
|
|
||||||
click.echo(click.style("This command only supports Qdrant vector store.", fg="red"))
|
|
||||||
return
|
|
||||||
create_count = 0
|
create_count = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -539,6 +538,76 @@ def add_qdrant_doc_id_index(field: str):
|
|||||||
click.echo(click.style(f"Index creation complete. Created {create_count} collection indexes.", fg="green"))
|
click.echo(click.style(f"Index creation complete. Created {create_count} collection indexes.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("old-metadata-migration", help="Old metadata migration.")
|
||||||
|
def old_metadata_migration():
|
||||||
|
"""
|
||||||
|
Old metadata migration.
|
||||||
|
"""
|
||||||
|
click.echo(click.style("Starting old metadata migration.", fg="green"))
|
||||||
|
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
documents = (
|
||||||
|
DatasetDocument.query.filter(DatasetDocument.doc_metadata is not None)
|
||||||
|
.order_by(DatasetDocument.created_at.desc())
|
||||||
|
.paginate(page=page, per_page=50)
|
||||||
|
)
|
||||||
|
except NotFound:
|
||||||
|
break
|
||||||
|
if not documents:
|
||||||
|
break
|
||||||
|
for document in documents:
|
||||||
|
if document.doc_metadata:
|
||||||
|
doc_metadata = document.doc_metadata
|
||||||
|
for key, value in doc_metadata.items():
|
||||||
|
for field in BuiltInField:
|
||||||
|
if field.value == key:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
dataset_metadata = (
|
||||||
|
db.session.query(DatasetMetadata)
|
||||||
|
.filter(DatasetMetadata.dataset_id == document.dataset_id, DatasetMetadata.name == key)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not dataset_metadata:
|
||||||
|
dataset_metadata = DatasetMetadata(
|
||||||
|
tenant_id=document.tenant_id,
|
||||||
|
dataset_id=document.dataset_id,
|
||||||
|
name=key,
|
||||||
|
type="string",
|
||||||
|
created_by=document.created_by,
|
||||||
|
)
|
||||||
|
db.session.add(dataset_metadata)
|
||||||
|
db.session.flush()
|
||||||
|
dataset_metadata_binding = DatasetMetadataBinding(
|
||||||
|
tenant_id=document.tenant_id,
|
||||||
|
dataset_id=document.dataset_id,
|
||||||
|
metadata_id=dataset_metadata.id,
|
||||||
|
document_id=document.id,
|
||||||
|
created_by=document.created_by,
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
if not dataset_metadata_binding:
|
||||||
|
dataset_metadata_binding = DatasetMetadataBinding(
|
||||||
|
tenant_id=document.tenant_id,
|
||||||
|
dataset_id=document.dataset_id,
|
||||||
|
metadata_id=dataset_metadata.id,
|
||||||
|
document_id=document.id,
|
||||||
|
created_by=document.created_by,
|
||||||
|
)
|
||||||
|
db.session.add(dataset_metadata_binding)
|
||||||
|
db.session.commit()
|
||||||
|
page += 1
|
||||||
|
click.echo(click.style("Old metadata migration completed.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
@click.command("create-tenant", help="Create account and tenant.")
|
@click.command("create-tenant", help="Create account and tenant.")
|
||||||
@click.option("--email", prompt=True, help="Tenant account email.")
|
@click.option("--email", prompt=True, help="Tenant account email.")
|
||||||
@click.option("--name", prompt=True, help="Workspace name.")
|
@click.option("--name", prompt=True, help="Workspace name.")
|
||||||
@ -724,3 +793,23 @@ def install_plugins(input_file: str, output_file: str, workers: int):
|
|||||||
PluginMigration.install_plugins(input_file, output_file, workers)
|
PluginMigration.install_plugins(input_file, output_file, workers)
|
||||||
|
|
||||||
click.echo(click.style("Install plugins completed.", fg="green"))
|
click.echo(click.style("Install plugins completed.", fg="green"))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("clear-free-plan-tenant-expired-logs", help="Clear free plan tenant expired logs.")
|
||||||
|
@click.option("--days", prompt=True, help="The days to clear free plan tenant expired logs.", default=30)
|
||||||
|
@click.option("--batch", prompt=True, help="The batch size to clear free plan tenant expired logs.", default=100)
|
||||||
|
@click.option(
|
||||||
|
"--tenant_ids",
|
||||||
|
prompt=True,
|
||||||
|
multiple=True,
|
||||||
|
help="The tenant ids to clear free plan tenant expired logs.",
|
||||||
|
)
|
||||||
|
def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[str]):
|
||||||
|
"""
|
||||||
|
Clear free plan tenant expired logs.
|
||||||
|
"""
|
||||||
|
click.echo(click.style("Starting clear free plan tenant expired logs.", fg="white"))
|
||||||
|
|
||||||
|
ClearFreePlanTenantExpiredLogs.process(days, batch, tenant_ids)
|
||||||
|
|
||||||
|
click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green"))
|
||||||
|
@ -61,6 +61,10 @@ class AppExecutionConfig(BaseSettings):
|
|||||||
description="Maximum number of concurrent active requests per app (0 for unlimited)",
|
description="Maximum number of concurrent active requests per app (0 for unlimited)",
|
||||||
default=0,
|
default=0,
|
||||||
)
|
)
|
||||||
|
APP_DAILY_RATE_LIMIT: NonNegativeInt = Field(
|
||||||
|
description="Maximum number of requests per app per day",
|
||||||
|
default=5000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CodeExecutionSandboxConfig(BaseSettings):
|
class CodeExecutionSandboxConfig(BaseSettings):
|
||||||
|
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
|||||||
|
|
||||||
CURRENT_VERSION: str = Field(
|
CURRENT_VERSION: str = Field(
|
||||||
description="Dify version",
|
description="Dify version",
|
||||||
default="1.1.0",
|
default="1.1.2",
|
||||||
)
|
)
|
||||||
|
|
||||||
COMMIT_SHA: str = Field(
|
COMMIT_SHA: str = Field(
|
||||||
|
@ -50,7 +50,15 @@ class AppListApi(Resource):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"mode",
|
"mode",
|
||||||
type=str,
|
type=str,
|
||||||
choices=["chat", "workflow", "agent-chat", "channel", "all"],
|
choices=[
|
||||||
|
"completion",
|
||||||
|
"chat",
|
||||||
|
"advanced-chat",
|
||||||
|
"workflow",
|
||||||
|
"agent-chat",
|
||||||
|
"channel",
|
||||||
|
"all",
|
||||||
|
],
|
||||||
default="all",
|
default="all",
|
||||||
location="args",
|
location="args",
|
||||||
required=False,
|
required=False,
|
||||||
@ -130,7 +138,6 @@ class AppApi(Resource):
|
|||||||
parser.add_argument("icon_type", type=str, location="json")
|
parser.add_argument("icon_type", type=str, location="json")
|
||||||
parser.add_argument("icon", type=str, location="json")
|
parser.add_argument("icon", type=str, location="json")
|
||||||
parser.add_argument("icon_background", type=str, location="json")
|
parser.add_argument("icon_background", type=str, location="json")
|
||||||
parser.add_argument("max_active_requests", type=int, location="json")
|
|
||||||
parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
|
parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -10,9 +10,14 @@ from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
|||||||
import services
|
import services
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
from controllers.console.app.error import (
|
||||||
|
ConversationCompletedError,
|
||||||
|
DraftWorkflowNotExist,
|
||||||
|
DraftWorkflowNotSync,
|
||||||
|
)
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
|
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -27,6 +32,7 @@ from models.account import Account
|
|||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
from services.errors.app import WorkflowHashNotEqualError
|
from services.errors.app import WorkflowHashNotEqualError
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
|
from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -168,6 +174,8 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
except services.errors.conversation.ConversationCompletedError:
|
except services.errors.conversation.ConversationCompletedError:
|
||||||
raise ConversationCompletedError()
|
raise ConversationCompletedError()
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -344,15 +352,18 @@ class DraftWorkflowRunApi(Resource):
|
|||||||
parser.add_argument("files", type=list, required=False, location="json")
|
parser.add_argument("files", type=list, required=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
response = AppGenerateService.generate(
|
try:
|
||||||
app_model=app_model,
|
response = AppGenerateService.generate(
|
||||||
user=current_user,
|
app_model=app_model,
|
||||||
args=args,
|
user=current_user,
|
||||||
invoke_from=InvokeFrom.DEBUGGER,
|
args=args,
|
||||||
streaming=True,
|
invoke_from=InvokeFrom.DEBUGGER,
|
||||||
)
|
streaming=True,
|
||||||
|
)
|
||||||
|
|
||||||
return helper.compact_generate_response(response)
|
return helper.compact_generate_response(response)
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowTaskStopApi(Resource):
|
class WorkflowTaskStopApi(Resource):
|
||||||
|
@ -79,7 +79,7 @@ class DatasetListApi(Resource):
|
|||||||
data = marshal(datasets, dataset_detail_fields)
|
data = marshal(datasets, dataset_detail_fields)
|
||||||
for item in data:
|
for item in data:
|
||||||
# convert embedding_model_provider to plugin standard format
|
# convert embedding_model_provider to plugin standard format
|
||||||
if item["indexing_technique"] == "high_quality":
|
if item["indexing_technique"] == "high_quality" and item["embedding_model_provider"]:
|
||||||
item["embedding_model_provider"] = str(ModelProviderID(item["embedding_model_provider"]))
|
item["embedding_model_provider"] = str(ModelProviderID(item["embedding_model_provider"]))
|
||||||
item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"
|
item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"
|
||||||
if item_model in model_names:
|
if item_model in model_names:
|
||||||
|
@ -16,6 +16,7 @@ from controllers.console.app.error import (
|
|||||||
)
|
)
|
||||||
from controllers.console.explore.error import NotChatAppError, NotCompletionAppError
|
from controllers.console.explore.error import NotChatAppError, NotCompletionAppError
|
||||||
from controllers.console.explore.wraps import InstalledAppResource
|
from controllers.console.explore.wraps import InstalledAppResource
|
||||||
|
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.errors.error import (
|
from core.errors.error import (
|
||||||
@ -29,6 +30,7 @@ from libs import helper
|
|||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
|
|
||||||
|
|
||||||
# define completion api for user
|
# define completion api for user
|
||||||
@ -75,7 +77,7 @@ class CompletionApi(InstalledAppResource):
|
|||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
@ -133,9 +135,11 @@ class ChatApi(InstalledAppResource):
|
|||||||
raise ProviderModelCurrentlyNotSupportError()
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from controllers.console.app.error import (
|
|||||||
)
|
)
|
||||||
from controllers.console.explore.error import NotWorkflowAppError
|
from controllers.console.explore.error import NotWorkflowAppError
|
||||||
from controllers.console.explore.wraps import InstalledAppResource
|
from controllers.console.explore.wraps import InstalledAppResource
|
||||||
|
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.errors.error import (
|
from core.errors.error import (
|
||||||
@ -23,6 +24,7 @@ from libs import helper
|
|||||||
from libs.login import current_user
|
from libs.login import current_user
|
||||||
from models.model import AppMode, InstalledApp
|
from models.model import AppMode, InstalledApp
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -56,9 +58,11 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
|
|||||||
raise ProviderModelCurrentlyNotSupportError()
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
@ -75,6 +75,7 @@ class FilePreviewApi(Resource):
|
|||||||
if args["as_attachment"]:
|
if args["as_attachment"]:
|
||||||
encoded_filename = quote(upload_file.name)
|
encoded_filename = quote(upload_file.name)
|
||||||
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
|
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
|
||||||
|
response.headers["Content-Type"] = "application/octet-stream"
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -7,4 +7,4 @@ api = ExternalApi(bp)
|
|||||||
|
|
||||||
from . import index
|
from . import index
|
||||||
from .app import app, audio, completion, conversation, file, message, workflow
|
from .app import app, audio, completion, conversation, file, message, workflow
|
||||||
from .dataset import dataset, document, hit_testing, segment, upload_file
|
from .dataset import dataset, document, hit_testing, metadata, segment, upload_file
|
||||||
|
@ -15,6 +15,7 @@ from controllers.service_api.app.error import (
|
|||||||
ProviderQuotaExceededError,
|
ProviderQuotaExceededError,
|
||||||
)
|
)
|
||||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||||
|
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.errors.error import (
|
from core.errors.error import (
|
||||||
@ -27,6 +28,7 @@ from libs import helper
|
|||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
from models.model import App, AppMode, EndUser
|
from models.model import App, AppMode, EndUser
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
|
|
||||||
|
|
||||||
class CompletionApi(Resource):
|
class CompletionApi(Resource):
|
||||||
@ -75,7 +77,7 @@ class CompletionApi(Resource):
|
|||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
@ -130,11 +132,13 @@ class ChatApi(Resource):
|
|||||||
raise ProviderQuotaExceededError()
|
raise ProviderQuotaExceededError()
|
||||||
except ModelCurrentlyNotSupportError:
|
except ModelCurrentlyNotSupportError:
|
||||||
raise ProviderModelCurrentlyNotSupportError()
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ from controllers.service_api.app.error import (
|
|||||||
ProviderQuotaExceededError,
|
ProviderQuotaExceededError,
|
||||||
)
|
)
|
||||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||||
|
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.errors.error import (
|
from core.errors.error import (
|
||||||
@ -29,6 +30,7 @@ from libs import helper
|
|||||||
from models.model import App, AppMode, EndUser
|
from models.model import App, AppMode, EndUser
|
||||||
from models.workflow import WorkflowRun, WorkflowRunStatus
|
from models.workflow import WorkflowRun, WorkflowRunStatus
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
from services.workflow_app_service import WorkflowAppService
|
from services.workflow_app_service import WorkflowAppService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -93,11 +95,13 @@ class WorkflowRunApi(Resource):
|
|||||||
raise ProviderQuotaExceededError()
|
raise ProviderQuotaExceededError()
|
||||||
except ModelCurrentlyNotSupportError:
|
except ModelCurrentlyNotSupportError:
|
||||||
raise ProviderModelCurrentlyNotSupportError()
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from controllers.service_api import api
|
|||||||
from controllers.service_api.dataset.error import DatasetInUseError, DatasetNameDuplicateError
|
from controllers.service_api.dataset.error import DatasetInUseError, DatasetNameDuplicateError
|
||||||
from controllers.service_api.wraps import DatasetApiResource
|
from controllers.service_api.wraps import DatasetApiResource
|
||||||
from core.model_runtime.entities.model_entities import ModelType
|
from core.model_runtime.entities.model_entities import ModelType
|
||||||
|
from core.plugin.entities.plugin import ModelProviderID
|
||||||
from core.provider_manager import ProviderManager
|
from core.provider_manager import ProviderManager
|
||||||
from fields.dataset_fields import dataset_detail_fields
|
from fields.dataset_fields import dataset_detail_fields
|
||||||
from libs.login import current_user
|
from libs.login import current_user
|
||||||
@ -48,7 +49,8 @@ class DatasetListApi(DatasetApiResource):
|
|||||||
|
|
||||||
data = marshal(datasets, dataset_detail_fields)
|
data = marshal(datasets, dataset_detail_fields)
|
||||||
for item in data:
|
for item in data:
|
||||||
if item["indexing_technique"] == "high_quality":
|
if item["indexing_technique"] == "high_quality" and item["embedding_model_provider"]:
|
||||||
|
item["embedding_model_provider"] = str(ModelProviderID(item["embedding_model_provider"]))
|
||||||
item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"
|
item_model = f"{item['embedding_model']}:{item['embedding_model_provider']}"
|
||||||
if item_model in model_names:
|
if item_model in model_names:
|
||||||
item["embedding_available"] = True
|
item["embedding_available"] = True
|
||||||
|
@ -18,7 +18,6 @@ from controllers.service_api.app.error import (
|
|||||||
from controllers.service_api.dataset.error import (
|
from controllers.service_api.dataset.error import (
|
||||||
ArchivedDocumentImmutableError,
|
ArchivedDocumentImmutableError,
|
||||||
DocumentIndexingError,
|
DocumentIndexingError,
|
||||||
InvalidMetadataError,
|
|
||||||
)
|
)
|
||||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
||||||
from core.errors.error import ProviderTokenNotInitError
|
from core.errors.error import ProviderTokenNotInitError
|
||||||
@ -51,8 +50,6 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||||||
"indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json"
|
"indexing_technique", type=str, choices=Dataset.INDEXING_TECHNIQUE_LIST, nullable=False, location="json"
|
||||||
)
|
)
|
||||||
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||||
parser.add_argument("doc_type", type=str, required=False, nullable=True, location="json")
|
|
||||||
parser.add_argument("doc_metadata", type=dict, required=False, nullable=True, location="json")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
@ -65,28 +62,6 @@ class DocumentAddByTextApi(DatasetApiResource):
|
|||||||
if not dataset.indexing_technique and not args["indexing_technique"]:
|
if not dataset.indexing_technique and not args["indexing_technique"]:
|
||||||
raise ValueError("indexing_technique is required.")
|
raise ValueError("indexing_technique is required.")
|
||||||
|
|
||||||
# Validate metadata if provided
|
|
||||||
if args.get("doc_type") or args.get("doc_metadata"):
|
|
||||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
|
||||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
|
||||||
|
|
||||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
|
||||||
raise InvalidMetadataError(
|
|
||||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(args["doc_metadata"], dict):
|
|
||||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
|
||||||
|
|
||||||
# Validate metadata schema based on doc_type
|
|
||||||
if args["doc_type"] != "others":
|
|
||||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
|
||||||
for key, value in args["doc_metadata"].items():
|
|
||||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
|
||||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
|
||||||
# set to MetaDataConfig
|
|
||||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
|
||||||
|
|
||||||
text = args.get("text")
|
text = args.get("text")
|
||||||
name = args.get("name")
|
name = args.get("name")
|
||||||
if text is None or name is None:
|
if text is None or name is None:
|
||||||
@ -133,8 +108,6 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||||||
"doc_language", type=str, default="English", required=False, nullable=False, location="json"
|
"doc_language", type=str, default="English", required=False, nullable=False, location="json"
|
||||||
)
|
)
|
||||||
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
|
||||||
parser.add_argument("doc_type", type=str, required=False, nullable=True, location="json")
|
|
||||||
parser.add_argument("doc_metadata", type=dict, required=False, nullable=True, location="json")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
@ -146,29 +119,6 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
|||||||
# indexing_technique is already set in dataset since this is an update
|
# indexing_technique is already set in dataset since this is an update
|
||||||
args["indexing_technique"] = dataset.indexing_technique
|
args["indexing_technique"] = dataset.indexing_technique
|
||||||
|
|
||||||
# Validate metadata if provided
|
|
||||||
if args.get("doc_type") or args.get("doc_metadata"):
|
|
||||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
|
||||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
|
||||||
|
|
||||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
|
||||||
raise InvalidMetadataError(
|
|
||||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(args["doc_metadata"], dict):
|
|
||||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
|
||||||
|
|
||||||
# Validate metadata schema based on doc_type
|
|
||||||
if args["doc_type"] != "others":
|
|
||||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
|
||||||
for key, value in args["doc_metadata"].items():
|
|
||||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
|
||||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
|
||||||
|
|
||||||
# set to MetaDataConfig
|
|
||||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
|
||||||
|
|
||||||
if args["text"]:
|
if args["text"]:
|
||||||
text = args.get("text")
|
text = args.get("text")
|
||||||
name = args.get("name")
|
name = args.get("name")
|
||||||
@ -216,29 +166,6 @@ class DocumentAddByFileApi(DatasetApiResource):
|
|||||||
if "doc_language" not in args:
|
if "doc_language" not in args:
|
||||||
args["doc_language"] = "English"
|
args["doc_language"] = "English"
|
||||||
|
|
||||||
# Validate metadata if provided
|
|
||||||
if args.get("doc_type") or args.get("doc_metadata"):
|
|
||||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
|
||||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
|
||||||
|
|
||||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
|
||||||
raise InvalidMetadataError(
|
|
||||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(args["doc_metadata"], dict):
|
|
||||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
|
||||||
|
|
||||||
# Validate metadata schema based on doc_type
|
|
||||||
if args["doc_type"] != "others":
|
|
||||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
|
||||||
for key, value in args["doc_metadata"].items():
|
|
||||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
|
||||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
|
||||||
|
|
||||||
# set to MetaDataConfig
|
|
||||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
|
||||||
|
|
||||||
# get dataset info
|
# get dataset info
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
@ -306,29 +233,6 @@ class DocumentUpdateByFileApi(DatasetApiResource):
|
|||||||
if "doc_language" not in args:
|
if "doc_language" not in args:
|
||||||
args["doc_language"] = "English"
|
args["doc_language"] = "English"
|
||||||
|
|
||||||
# Validate metadata if provided
|
|
||||||
if args.get("doc_type") or args.get("doc_metadata"):
|
|
||||||
if not args.get("doc_type") or not args.get("doc_metadata"):
|
|
||||||
raise InvalidMetadataError("Both doc_type and doc_metadata must be provided when adding metadata")
|
|
||||||
|
|
||||||
if args["doc_type"] not in DocumentService.DOCUMENT_METADATA_SCHEMA:
|
|
||||||
raise InvalidMetadataError(
|
|
||||||
"Invalid doc_type. Must be one of: " + ", ".join(DocumentService.DOCUMENT_METADATA_SCHEMA.keys())
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(args["doc_metadata"], dict):
|
|
||||||
raise InvalidMetadataError("doc_metadata must be a dictionary")
|
|
||||||
|
|
||||||
# Validate metadata schema based on doc_type
|
|
||||||
if args["doc_type"] != "others":
|
|
||||||
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[args["doc_type"]]
|
|
||||||
for key, value in args["doc_metadata"].items():
|
|
||||||
if key in metadata_schema and not isinstance(value, metadata_schema[key]):
|
|
||||||
raise InvalidMetadataError(f"Invalid type for metadata field {key}")
|
|
||||||
|
|
||||||
# set to MetaDataConfig
|
|
||||||
args["metadata"] = {"doc_type": args["doc_type"], "doc_metadata": args["doc_metadata"]}
|
|
||||||
|
|
||||||
# get dataset info
|
# get dataset info
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
tenant_id = str(tenant_id)
|
tenant_id = str(tenant_id)
|
||||||
|
126
api/controllers/service_api/dataset/metadata.py
Normal file
126
api/controllers/service_api/dataset/metadata.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from flask_login import current_user # type: ignore # type: ignore
|
||||||
|
from flask_restful import marshal, reqparse # type: ignore
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from controllers.service_api import api
|
||||||
|
from controllers.service_api.wraps import DatasetApiResource
|
||||||
|
from fields.dataset_fields import dataset_metadata_fields
|
||||||
|
from services.dataset_service import DatasetService
|
||||||
|
from services.entities.knowledge_entities.knowledge_entities import (
|
||||||
|
MetadataArgs,
|
||||||
|
MetadataOperationData,
|
||||||
|
)
|
||||||
|
from services.metadata_service import MetadataService
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_name(name):
|
||||||
|
if not name or len(name) < 1 or len(name) > 40:
|
||||||
|
raise ValueError("Name must be between 1 to 40 characters.")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_description_length(description):
|
||||||
|
if len(description) > 400:
|
||||||
|
raise ValueError("Description cannot exceed 400 characters.")
|
||||||
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataCreateServiceApi(DatasetApiResource):
|
||||||
|
def post(self, tenant_id, dataset_id):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("type", type=str, required=True, nullable=True, location="json")
|
||||||
|
parser.add_argument("name", type=str, required=True, nullable=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
metadata_args = MetadataArgs(**args)
|
||||||
|
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
metadata = MetadataService.create_metadata(dataset_id_str, metadata_args)
|
||||||
|
return marshal(metadata, dataset_metadata_fields), 201
|
||||||
|
|
||||||
|
def get(self, tenant_id, dataset_id):
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
return MetadataService.get_dataset_metadatas(dataset), 200
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataServiceApi(DatasetApiResource):
|
||||||
|
def patch(self, tenant_id, dataset_id, metadata_id):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("name", type=str, required=True, nullable=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
metadata_id_str = str(metadata_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name"))
|
||||||
|
return marshal(metadata, dataset_metadata_fields), 200
|
||||||
|
|
||||||
|
def delete(self, tenant_id, dataset_id, metadata_id):
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
metadata_id_str = str(metadata_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
MetadataService.delete_metadata(dataset_id_str, metadata_id_str)
|
||||||
|
return 200
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource):
|
||||||
|
def get(self, tenant_id):
|
||||||
|
built_in_fields = MetadataService.get_built_in_fields()
|
||||||
|
return {"fields": built_in_fields}, 200
|
||||||
|
|
||||||
|
|
||||||
|
class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
|
||||||
|
def post(self, tenant_id, dataset_id, action):
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
if action == "enable":
|
||||||
|
MetadataService.enable_built_in_field(dataset)
|
||||||
|
elif action == "disable":
|
||||||
|
MetadataService.disable_built_in_field(dataset)
|
||||||
|
return 200
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentMetadataEditServiceApi(DatasetApiResource):
|
||||||
|
def post(self, tenant_id, dataset_id):
|
||||||
|
dataset_id_str = str(dataset_id)
|
||||||
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
|
if dataset is None:
|
||||||
|
raise NotFound("Dataset not found.")
|
||||||
|
DatasetService.check_dataset_permission(dataset, current_user)
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("operation_data", type=list, required=True, nullable=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
metadata_args = MetadataOperationData(**args)
|
||||||
|
|
||||||
|
MetadataService.update_documents_metadata(dataset, metadata_args)
|
||||||
|
|
||||||
|
return 200
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(DatasetMetadataCreateServiceApi, "/datasets/<uuid:dataset_id>/metadata")
|
||||||
|
api.add_resource(DatasetMetadataServiceApi, "/datasets/<uuid:dataset_id>/metadata/<uuid:metadata_id>")
|
||||||
|
api.add_resource(DatasetMetadataBuiltInFieldServiceApi, "/datasets/metadata/built-in")
|
||||||
|
api.add_resource(
|
||||||
|
DatasetMetadataBuiltInFieldActionServiceApi, "/datasets/<uuid:dataset_id>/metadata/built-in/<string:action>"
|
||||||
|
)
|
||||||
|
api.add_resource(DocumentMetadataEditServiceApi, "/datasets/<uuid:dataset_id>/documents/metadata")
|
@ -11,6 +11,7 @@ from controllers.web.error import (
|
|||||||
ProviderNotInitializeError,
|
ProviderNotInitializeError,
|
||||||
ProviderQuotaExceededError,
|
ProviderQuotaExceededError,
|
||||||
)
|
)
|
||||||
|
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
@ -23,6 +24,7 @@ from core.model_runtime.errors.invoke import InvokeError
|
|||||||
from libs import helper
|
from libs import helper
|
||||||
from models.model import App, AppMode, EndUser
|
from models.model import App, AppMode, EndUser
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
|
from services.errors.llm import InvokeRateLimitError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -55,9 +57,11 @@ class WorkflowRunApi(WebApiResource):
|
|||||||
raise ProviderModelCurrentlyNotSupportError()
|
raise ProviderModelCurrentlyNotSupportError()
|
||||||
except InvokeError as e:
|
except InvokeError as e:
|
||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
|
except InvokeRateLimitError as ex:
|
||||||
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logging.exception("internal server error.")
|
logging.exception("internal server error.")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
@ -27,6 +27,9 @@ class RateLimit:
|
|||||||
|
|
||||||
def __init__(self, client_id: str, max_active_requests: int):
|
def __init__(self, client_id: str, max_active_requests: int):
|
||||||
self.max_active_requests = max_active_requests
|
self.max_active_requests = max_active_requests
|
||||||
|
# must be called after max_active_requests is set
|
||||||
|
if self.disabled():
|
||||||
|
return
|
||||||
if hasattr(self, "initialized"):
|
if hasattr(self, "initialized"):
|
||||||
return
|
return
|
||||||
self.initialized = True
|
self.initialized = True
|
||||||
@ -37,6 +40,8 @@ class RateLimit:
|
|||||||
self.flush_cache(use_local_value=True)
|
self.flush_cache(use_local_value=True)
|
||||||
|
|
||||||
def flush_cache(self, use_local_value=False):
|
def flush_cache(self, use_local_value=False):
|
||||||
|
if self.disabled():
|
||||||
|
return
|
||||||
self.last_recalculate_time = time.time()
|
self.last_recalculate_time = time.time()
|
||||||
# flush max active requests
|
# flush max active requests
|
||||||
if use_local_value or not redis_client.exists(self.max_active_requests_key):
|
if use_local_value or not redis_client.exists(self.max_active_requests_key):
|
||||||
@ -59,18 +64,18 @@ class RateLimit:
|
|||||||
redis_client.hdel(self.active_requests_key, *timeout_requests)
|
redis_client.hdel(self.active_requests_key, *timeout_requests)
|
||||||
|
|
||||||
def enter(self, request_id: Optional[str] = None) -> str:
|
def enter(self, request_id: Optional[str] = None) -> str:
|
||||||
|
if self.disabled():
|
||||||
|
return RateLimit._UNLIMITED_REQUEST_ID
|
||||||
if time.time() - self.last_recalculate_time > RateLimit._ACTIVE_REQUESTS_COUNT_FLUSH_INTERVAL:
|
if time.time() - self.last_recalculate_time > RateLimit._ACTIVE_REQUESTS_COUNT_FLUSH_INTERVAL:
|
||||||
self.flush_cache()
|
self.flush_cache()
|
||||||
if self.max_active_requests <= 0:
|
|
||||||
return RateLimit._UNLIMITED_REQUEST_ID
|
|
||||||
if not request_id:
|
if not request_id:
|
||||||
request_id = RateLimit.gen_request_key()
|
request_id = RateLimit.gen_request_key()
|
||||||
|
|
||||||
active_requests_count = redis_client.hlen(self.active_requests_key)
|
active_requests_count = redis_client.hlen(self.active_requests_key)
|
||||||
if active_requests_count >= self.max_active_requests:
|
if active_requests_count >= self.max_active_requests:
|
||||||
raise AppInvokeQuotaExceededError(
|
raise AppInvokeQuotaExceededError(
|
||||||
"Too many requests. Please try again later. The current maximum "
|
f"Too many requests. Please try again later. The current maximum concurrent requests allowed "
|
||||||
"concurrent requests allowed is {}.".format(self.max_active_requests)
|
f"for {self.client_id} is {self.max_active_requests}."
|
||||||
)
|
)
|
||||||
redis_client.hset(self.active_requests_key, request_id, str(time.time()))
|
redis_client.hset(self.active_requests_key, request_id, str(time.time()))
|
||||||
return request_id
|
return request_id
|
||||||
@ -80,6 +85,9 @@ class RateLimit:
|
|||||||
return
|
return
|
||||||
redis_client.hdel(self.active_requests_key, request_id)
|
redis_client.hdel(self.active_requests_key, request_id)
|
||||||
|
|
||||||
|
def disabled(self):
|
||||||
|
return self.max_active_requests <= 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gen_request_key() -> str:
|
def gen_request_key() -> str:
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
@ -49,6 +49,7 @@ class FileAttribute(StrEnum):
|
|||||||
TRANSFER_METHOD = "transfer_method"
|
TRANSFER_METHOD = "transfer_method"
|
||||||
URL = "url"
|
URL = "url"
|
||||||
EXTENSION = "extension"
|
EXTENSION = "extension"
|
||||||
|
RELATED_ID = "related_id"
|
||||||
|
|
||||||
|
|
||||||
class ArrayFileAttribute(StrEnum):
|
class ArrayFileAttribute(StrEnum):
|
||||||
|
@ -34,6 +34,8 @@ def get_attr(*, file: File, attr: FileAttribute):
|
|||||||
return file.remote_url
|
return file.remote_url
|
||||||
case FileAttribute.EXTENSION:
|
case FileAttribute.EXTENSION:
|
||||||
return file.extension
|
return file.extension
|
||||||
|
case FileAttribute.RELATED_ID:
|
||||||
|
return file.related_id
|
||||||
|
|
||||||
|
|
||||||
def to_prompt_message_content(
|
def to_prompt_message_content(
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
- 支持 5 种模型类型的能力调用
|
- 支持 5 种模型类型的能力调用
|
||||||
|
|
||||||
- `LLM` - LLM 文本补全、对话,预计算 tokens 能力
|
- `LLM` - LLM 文本补全、对话,预计算 tokens 能力
|
||||||
- `Text Embedidng Model` - 文本 Embedding ,预计算 tokens 能力
|
- `Text Embedding Model` - 文本 Embedding ,预计算 tokens 能力
|
||||||
- `Rerank Model` - 分段 Rerank 能力
|
- `Rerank Model` - 分段 Rerank 能力
|
||||||
- `Speech-to-text Model` - 语音转文本能力
|
- `Speech-to-text Model` - 语音转文本能力
|
||||||
- `Text-to-speech Model` - 文本转语音能力
|
- `Text-to-speech Model` - 文本转语音能力
|
||||||
|
@ -33,7 +33,6 @@ from core.ops.entities.trace_entity import (
|
|||||||
)
|
)
|
||||||
from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace
|
from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace
|
||||||
from core.ops.langsmith_trace.langsmith_trace import LangSmithDataTrace
|
from core.ops.langsmith_trace.langsmith_trace import LangSmithDataTrace
|
||||||
from core.ops.opik_trace.opik_trace import OpikDataTrace
|
|
||||||
from core.ops.utils import get_message_data
|
from core.ops.utils import get_message_data
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from extensions.ext_storage import storage
|
from extensions.ext_storage import storage
|
||||||
@ -41,6 +40,13 @@ from models.model import App, AppModelConfig, Conversation, Message, MessageFile
|
|||||||
from models.workflow import WorkflowAppLog, WorkflowRun
|
from models.workflow import WorkflowAppLog, WorkflowRun
|
||||||
from tasks.ops_trace_task import process_trace_tasks
|
from tasks.ops_trace_task import process_trace_tasks
|
||||||
|
|
||||||
|
|
||||||
|
def build_opik_trace_instance(config: OpikConfig):
|
||||||
|
from core.ops.opik_trace.opik_trace import OpikDataTrace
|
||||||
|
|
||||||
|
return OpikDataTrace(config)
|
||||||
|
|
||||||
|
|
||||||
provider_config_map: dict[str, dict[str, Any]] = {
|
provider_config_map: dict[str, dict[str, Any]] = {
|
||||||
TracingProviderEnum.LANGFUSE.value: {
|
TracingProviderEnum.LANGFUSE.value: {
|
||||||
"config_class": LangfuseConfig,
|
"config_class": LangfuseConfig,
|
||||||
@ -58,7 +64,7 @@ provider_config_map: dict[str, dict[str, Any]] = {
|
|||||||
"config_class": OpikConfig,
|
"config_class": OpikConfig,
|
||||||
"secret_keys": ["api_key"],
|
"secret_keys": ["api_key"],
|
||||||
"other_keys": ["project", "url", "workspace"],
|
"other_keys": ["project", "url", "workspace"],
|
||||||
"trace_instance": OpikDataTrace,
|
"trace_instance": lambda config: build_opik_trace_instance(config),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,8 @@ class ElasticSearchVector(BaseVector):
|
|||||||
Field.METADATA_KEY.value: {
|
Field.METADATA_KEY.value: {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"doc_id": {"type": "keyword"} # Map doc_id to keyword type
|
"doc_id": {"type": "keyword"}, # Map doc_id to keyword type
|
||||||
|
"document_id": {"type": "keyword"}, # Map doc_id to keyword type
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,4 @@ class Field(Enum):
|
|||||||
TEXT_KEY = "text"
|
TEXT_KEY = "text"
|
||||||
PRIMARY_KEY = "id"
|
PRIMARY_KEY = "id"
|
||||||
DOC_ID = "metadata.doc_id"
|
DOC_ID = "metadata.doc_id"
|
||||||
|
DOCUMENT_ID = "metadata.document_id"
|
||||||
|
@ -119,7 +119,7 @@ class QdrantVector(BaseVector):
|
|||||||
max_indexing_threads=0,
|
max_indexing_threads=0,
|
||||||
on_disk=False,
|
on_disk=False,
|
||||||
)
|
)
|
||||||
self._client.recreate_collection(
|
self._client.create_collection(
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
vectors_config=vectors_config,
|
vectors_config=vectors_config,
|
||||||
hnsw_config=hnsw_config,
|
hnsw_config=hnsw_config,
|
||||||
@ -134,6 +134,10 @@ class QdrantVector(BaseVector):
|
|||||||
self._client.create_payload_index(
|
self._client.create_payload_index(
|
||||||
collection_name, Field.DOC_ID.value, field_schema=PayloadSchemaType.KEYWORD
|
collection_name, Field.DOC_ID.value, field_schema=PayloadSchemaType.KEYWORD
|
||||||
)
|
)
|
||||||
|
# create document_id payload index
|
||||||
|
self._client.create_payload_index(
|
||||||
|
collection_name, Field.DOCUMENT_ID.value, field_schema=PayloadSchemaType.KEYWORD
|
||||||
|
)
|
||||||
# create full text index
|
# create full text index
|
||||||
text_index_params = TextIndexParams(
|
text_index_params = TextIndexParams(
|
||||||
type=TextIndexType.TEXT,
|
type=TextIndexType.TEXT,
|
||||||
|
@ -129,7 +129,7 @@ class TidbOnQdrantVector(BaseVector):
|
|||||||
max_indexing_threads=0,
|
max_indexing_threads=0,
|
||||||
on_disk=False,
|
on_disk=False,
|
||||||
)
|
)
|
||||||
self._client.recreate_collection(
|
self._client.create_collection(
|
||||||
collection_name=collection_name,
|
collection_name=collection_name,
|
||||||
vectors_config=vectors_config,
|
vectors_config=vectors_config,
|
||||||
hnsw_config=hnsw_config,
|
hnsw_config=hnsw_config,
|
||||||
@ -144,6 +144,10 @@ class TidbOnQdrantVector(BaseVector):
|
|||||||
self._client.create_payload_index(
|
self._client.create_payload_index(
|
||||||
collection_name, Field.DOC_ID.value, field_schema=PayloadSchemaType.KEYWORD
|
collection_name, Field.DOC_ID.value, field_schema=PayloadSchemaType.KEYWORD
|
||||||
)
|
)
|
||||||
|
# create document_id payload index
|
||||||
|
self._client.create_payload_index(
|
||||||
|
collection_name, Field.DOCUMENT_ID.value, field_schema=PayloadSchemaType.KEYWORD
|
||||||
|
)
|
||||||
# create full text index
|
# create full text index
|
||||||
text_index_params = TextIndexParams(
|
text_index_params = TextIndexParams(
|
||||||
type=TextIndexType.TEXT,
|
type=TextIndexType.TEXT,
|
||||||
@ -318,26 +322,17 @@ class TidbOnQdrantVector(BaseVector):
|
|||||||
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
|
||||||
from qdrant_client.http import models
|
from qdrant_client.http import models
|
||||||
|
|
||||||
filter = models.Filter(
|
filter = None
|
||||||
must=[
|
|
||||||
models.FieldCondition(
|
|
||||||
key="group_id",
|
|
||||||
match=models.MatchValue(value=self._group_id),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
document_ids_filter = kwargs.get("document_ids_filter")
|
document_ids_filter = kwargs.get("document_ids_filter")
|
||||||
if document_ids_filter:
|
if document_ids_filter:
|
||||||
should_conditions = []
|
filter = models.Filter(
|
||||||
for document_id_filter in document_ids_filter:
|
must=[
|
||||||
should_conditions.append(
|
|
||||||
models.FieldCondition(
|
models.FieldCondition(
|
||||||
key="metadata.document_id",
|
key="metadata.document_id",
|
||||||
match=models.MatchValue(value=document_id_filter),
|
match=models.MatchAny(any=document_ids_filter),
|
||||||
)
|
)
|
||||||
)
|
],
|
||||||
if should_conditions:
|
)
|
||||||
filter.should = should_conditions # type: ignore
|
|
||||||
results = self._client.search(
|
results = self._client.search(
|
||||||
collection_name=self._collection_name,
|
collection_name=self._collection_name,
|
||||||
query_vector=query_vector,
|
query_vector=query_vector,
|
||||||
@ -372,26 +367,17 @@ class TidbOnQdrantVector(BaseVector):
|
|||||||
"""
|
"""
|
||||||
from qdrant_client.http import models
|
from qdrant_client.http import models
|
||||||
|
|
||||||
scroll_filter = models.Filter(
|
scroll_filter = None
|
||||||
must=[
|
|
||||||
models.FieldCondition(
|
|
||||||
key="page_content",
|
|
||||||
match=models.MatchText(text=query),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
document_ids_filter = kwargs.get("document_ids_filter")
|
document_ids_filter = kwargs.get("document_ids_filter")
|
||||||
if document_ids_filter:
|
if document_ids_filter:
|
||||||
should_conditions = []
|
scroll_filter = models.Filter(
|
||||||
for document_id_filter in document_ids_filter:
|
must=[
|
||||||
should_conditions.append(
|
|
||||||
models.FieldCondition(
|
models.FieldCondition(
|
||||||
key="metadata.document_id",
|
key="metadata.document_id",
|
||||||
match=models.MatchValue(value=document_id_filter),
|
match=models.MatchAny(any=document_ids_filter),
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
if should_conditions:
|
)
|
||||||
scroll_filter.should = should_conditions # type: ignore
|
|
||||||
response = self._client.scroll(
|
response = self._client.scroll(
|
||||||
collection_name=self._collection_name,
|
collection_name=self._collection_name,
|
||||||
scroll_filter=scroll_filter,
|
scroll_filter=scroll_filter,
|
||||||
|
@ -105,10 +105,12 @@ class TiDBVector(BaseVector):
|
|||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
meta JSON NOT NULL,
|
meta JSON NOT NULL,
|
||||||
doc_id VARCHAR(64) AS (JSON_UNQUOTE(JSON_EXTRACT(meta, '$.doc_id'))) STORED,
|
doc_id VARCHAR(64) AS (JSON_UNQUOTE(JSON_EXTRACT(meta, '$.doc_id'))) STORED,
|
||||||
|
document_id VARCHAR(64) AS (JSON_UNQUOTE(JSON_EXTRACT(meta, '$.document_id'))) STORED,
|
||||||
vector VECTOR<FLOAT>({dimension}) NOT NULL,
|
vector VECTOR<FLOAT>({dimension}) NOT NULL,
|
||||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
KEY (doc_id),
|
KEY (doc_id),
|
||||||
|
KEY (document_id),
|
||||||
VECTOR INDEX idx_vector (({tidb_dist_func}(vector))) USING HNSW
|
VECTOR INDEX idx_vector (({tidb_dist_func}(vector))) USING HNSW
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
@ -189,7 +189,10 @@ class WeaviateVector(BaseVector):
|
|||||||
vector = {"vector": query_vector}
|
vector = {"vector": query_vector}
|
||||||
document_ids_filter = kwargs.get("document_ids_filter")
|
document_ids_filter = kwargs.get("document_ids_filter")
|
||||||
if document_ids_filter:
|
if document_ids_filter:
|
||||||
where_filter = {"operator": "ContainsAny", "path": ["document_id"], "valueTextArray": document_ids_filter}
|
operands = []
|
||||||
|
for document_id_filter in document_ids_filter:
|
||||||
|
operands.append({"path": ["document_id"], "operator": "Equal", "valueText": document_id_filter})
|
||||||
|
where_filter = {"operator": "Or", "operands": operands}
|
||||||
query_obj = query_obj.with_where(where_filter)
|
query_obj = query_obj.with_where(where_filter)
|
||||||
result = (
|
result = (
|
||||||
query_obj.with_near_vector(vector)
|
query_obj.with_near_vector(vector)
|
||||||
@ -237,7 +240,10 @@ class WeaviateVector(BaseVector):
|
|||||||
query_obj = self._client.query.get(collection_name, properties)
|
query_obj = self._client.query.get(collection_name, properties)
|
||||||
document_ids_filter = kwargs.get("document_ids_filter")
|
document_ids_filter = kwargs.get("document_ids_filter")
|
||||||
if document_ids_filter:
|
if document_ids_filter:
|
||||||
where_filter = {"operator": "ContainsAny", "path": ["document_id"], "valueTextArray": document_ids_filter}
|
operands = []
|
||||||
|
for document_id_filter in document_ids_filter:
|
||||||
|
operands.append({"path": ["document_id"], "operator": "Equal", "valueText": document_id_filter})
|
||||||
|
where_filter = {"operator": "Or", "operands": operands}
|
||||||
query_obj = query_obj.with_where(where_filter)
|
query_obj = query_obj.with_where(where_filter)
|
||||||
query_obj = query_obj.with_additional(["vector"])
|
query_obj = query_obj.with_additional(["vector"])
|
||||||
properties = ["text"]
|
properties = ["text"]
|
||||||
|
@ -870,7 +870,7 @@ class DatasetRetrieval:
|
|||||||
for condition in metadata_filtering_conditions.conditions: # type: ignore
|
for condition in metadata_filtering_conditions.conditions: # type: ignore
|
||||||
metadata_name = condition.name
|
metadata_name = condition.name
|
||||||
expected_value = condition.value
|
expected_value = condition.value
|
||||||
if expected_value or condition.comparison_operator in ("empty", "not empty"):
|
if expected_value is not None or condition.comparison_operator in ("empty", "not empty"):
|
||||||
if isinstance(expected_value, str):
|
if isinstance(expected_value, str):
|
||||||
expected_value = self._replace_metadata_filter_value(expected_value, inputs)
|
expected_value = self._replace_metadata_filter_value(expected_value, inputs)
|
||||||
filters = self._process_metadata_filter_func(
|
filters = self._process_metadata_filter_func(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from ast import literal_eval
|
import json
|
||||||
from collections.abc import Generator, Mapping, Sequence
|
from collections.abc import Generator, Mapping, Sequence
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
@ -143,15 +143,23 @@ class AgentNode(ToolNode):
|
|||||||
raise ValueError(f"Variable {agent_input.value} does not exist")
|
raise ValueError(f"Variable {agent_input.value} does not exist")
|
||||||
parameter_value = variable.value
|
parameter_value = variable.value
|
||||||
elif agent_input.type in {"mixed", "constant"}:
|
elif agent_input.type in {"mixed", "constant"}:
|
||||||
segment_group = variable_pool.convert_template(str(agent_input.value))
|
# variable_pool.convert_template expects a string template,
|
||||||
|
# but if passing a dict, convert to JSON string first before rendering
|
||||||
|
try:
|
||||||
|
parameter_value = json.dumps(agent_input.value, ensure_ascii=False)
|
||||||
|
except TypeError:
|
||||||
|
parameter_value = str(agent_input.value)
|
||||||
|
segment_group = variable_pool.convert_template(parameter_value)
|
||||||
parameter_value = segment_group.log if for_log else segment_group.text
|
parameter_value = segment_group.log if for_log else segment_group.text
|
||||||
|
# variable_pool.convert_template returns a string,
|
||||||
|
# so we need to convert it back to a dictionary
|
||||||
|
try:
|
||||||
|
parameter_value = json.loads(parameter_value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
parameter_value = parameter_value
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown agent input type '{agent_input.type}'")
|
raise ValueError(f"Unknown agent input type '{agent_input.type}'")
|
||||||
value = parameter_value.strip()
|
value = parameter_value
|
||||||
if (parameter_value.startswith("{") and parameter_value.endswith("}")) or (
|
|
||||||
parameter_value.startswith("[") and parameter_value.endswith("]")
|
|
||||||
):
|
|
||||||
value = literal_eval(parameter_value) # transform string to python object
|
|
||||||
if parameter.type == "array[tools]":
|
if parameter.type == "array[tools]":
|
||||||
value = cast(list[dict[str, Any]], value)
|
value = cast(list[dict[str, Any]], value)
|
||||||
value = [tool for tool in value if tool.get("enabled", False)]
|
value = [tool for tool in value if tool.get("enabled", False)]
|
||||||
|
@ -65,7 +65,7 @@ class StreamProcessor(ABC):
|
|||||||
# Issues: #13626
|
# Issues: #13626
|
||||||
if (
|
if (
|
||||||
finished_node_id in self.graph.node_parallel_mapping
|
finished_node_id in self.graph.node_parallel_mapping
|
||||||
and edge.target_node_id not in self.graph.parallel_mapping
|
and edge.target_node_id not in self.graph.node_parallel_mapping
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
unreachable_first_node_ids.append(edge.target_node_id)
|
unreachable_first_node_ids.append(edge.target_node_id)
|
||||||
|
@ -356,7 +356,7 @@ class KnowledgeRetrievalNode(LLMNode):
|
|||||||
for condition in node_data.metadata_filtering_conditions.conditions: # type: ignore
|
for condition in node_data.metadata_filtering_conditions.conditions: # type: ignore
|
||||||
metadata_name = condition.name
|
metadata_name = condition.name
|
||||||
expected_value = condition.value
|
expected_value = condition.value
|
||||||
if expected_value or condition.comparison_operator in ("empty", "not empty"):
|
if expected_value is not None or condition.comparison_operator in ("empty", "not empty"):
|
||||||
if isinstance(expected_value, str):
|
if isinstance(expected_value, str):
|
||||||
expected_value = self.graph_runtime_state.variable_pool.convert_template(
|
expected_value = self.graph_runtime_state.variable_pool.convert_template(
|
||||||
expected_value
|
expected_value
|
||||||
|
@ -3,7 +3,8 @@ from dify_app import DifyApp
|
|||||||
|
|
||||||
def init_app(app: DifyApp):
|
def init_app(app: DifyApp):
|
||||||
from commands import (
|
from commands import (
|
||||||
add_qdrant_doc_id_index,
|
add_qdrant_index,
|
||||||
|
clear_free_plan_tenant_expired_logs,
|
||||||
convert_to_agent_apps,
|
convert_to_agent_apps,
|
||||||
create_tenant,
|
create_tenant,
|
||||||
extract_plugins,
|
extract_plugins,
|
||||||
@ -11,6 +12,7 @@ def init_app(app: DifyApp):
|
|||||||
fix_app_site_missing,
|
fix_app_site_missing,
|
||||||
install_plugins,
|
install_plugins,
|
||||||
migrate_data_for_plugin,
|
migrate_data_for_plugin,
|
||||||
|
old_metadata_migration,
|
||||||
reset_email,
|
reset_email,
|
||||||
reset_encrypt_key_pair,
|
reset_encrypt_key_pair,
|
||||||
reset_password,
|
reset_password,
|
||||||
@ -24,7 +26,7 @@ def init_app(app: DifyApp):
|
|||||||
reset_encrypt_key_pair,
|
reset_encrypt_key_pair,
|
||||||
vdb_migrate,
|
vdb_migrate,
|
||||||
convert_to_agent_apps,
|
convert_to_agent_apps,
|
||||||
add_qdrant_doc_id_index,
|
add_qdrant_index,
|
||||||
create_tenant,
|
create_tenant,
|
||||||
upgrade_db,
|
upgrade_db,
|
||||||
fix_app_site_missing,
|
fix_app_site_missing,
|
||||||
@ -32,6 +34,8 @@ def init_app(app: DifyApp):
|
|||||||
extract_plugins,
|
extract_plugins,
|
||||||
extract_unique_plugins,
|
extract_unique_plugins,
|
||||||
install_plugins,
|
install_plugins,
|
||||||
|
old_metadata_migration,
|
||||||
|
clear_free_plan_tenant_expired_logs,
|
||||||
]
|
]
|
||||||
for cmd in cmds_to_register:
|
for cmd in cmds_to_register:
|
||||||
app.cli.add_command(cmd)
|
app.cli.add_command(cmd)
|
||||||
|
@ -24,6 +24,7 @@ vector_setting_fields = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
weighted_score_fields = {
|
weighted_score_fields = {
|
||||||
|
"weight_type": fields.String,
|
||||||
"keyword_setting": fields.Nested(keyword_setting_fields),
|
"keyword_setting": fields.Nested(keyword_setting_fields),
|
||||||
"vector_setting": fields.Nested(vector_setting_fields),
|
"vector_setting": fields.Nested(vector_setting_fields),
|
||||||
}
|
}
|
||||||
|
@ -910,7 +910,7 @@ class Embedding(db.Model): # type: ignore[name-defined]
|
|||||||
self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL)
|
self.embedding = pickle.dumps(embedding_data, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
def get_embedding(self) -> list[float]:
|
def get_embedding(self) -> list[float]:
|
||||||
return cast(list[float], pickle.loads(self.embedding))
|
return cast(list[float], pickle.loads(self.embedding)) # noqa: S301
|
||||||
|
|
||||||
|
|
||||||
class DatasetCollectionBinding(db.Model): # type: ignore[name-defined]
|
class DatasetCollectionBinding(db.Model): # type: ignore[name-defined]
|
||||||
|
@ -838,6 +838,33 @@ class Conversation(db.Model): # type: ignore[name-defined]
|
|||||||
def in_debug_mode(self):
|
def in_debug_mode(self):
|
||||||
return self.override_model_configs is not None
|
return self.override_model_configs is not None
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"app_id": self.app_id,
|
||||||
|
"app_model_config_id": self.app_model_config_id,
|
||||||
|
"model_provider": self.model_provider,
|
||||||
|
"override_model_configs": self.override_model_configs,
|
||||||
|
"model_id": self.model_id,
|
||||||
|
"mode": self.mode,
|
||||||
|
"name": self.name,
|
||||||
|
"summary": self.summary,
|
||||||
|
"inputs": self.inputs,
|
||||||
|
"introduction": self.introduction,
|
||||||
|
"system_instruction": self.system_instruction,
|
||||||
|
"system_instruction_tokens": self.system_instruction_tokens,
|
||||||
|
"status": self.status,
|
||||||
|
"invoke_from": self.invoke_from,
|
||||||
|
"from_source": self.from_source,
|
||||||
|
"from_end_user_id": self.from_end_user_id,
|
||||||
|
"from_account_id": self.from_account_id,
|
||||||
|
"read_at": self.read_at,
|
||||||
|
"read_account_id": self.read_account_id,
|
||||||
|
"dialogue_count": self.dialogue_count,
|
||||||
|
"created_at": self.created_at,
|
||||||
|
"updated_at": self.updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Message(db.Model): # type: ignore[name-defined]
|
class Message(db.Model): # type: ignore[name-defined]
|
||||||
__tablename__ = "messages"
|
__tablename__ = "messages"
|
||||||
|
@ -785,9 +785,11 @@ class TenantService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_member_from_tenant(tenant: Tenant, account: Account, operator: Account) -> None:
|
def remove_member_from_tenant(tenant: Tenant, account: Account, operator: Account) -> None:
|
||||||
"""Remove member from tenant"""
|
"""Remove member from tenant"""
|
||||||
if operator.id == account.id and TenantService.check_member_permission(tenant, operator, account, "remove"):
|
if operator.id == account.id:
|
||||||
raise CannotOperateSelfError("Cannot operate self.")
|
raise CannotOperateSelfError("Cannot operate self.")
|
||||||
|
|
||||||
|
TenantService.check_member_permission(tenant, operator, account, "remove")
|
||||||
|
|
||||||
ta = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=account.id).first()
|
ta = TenantAccountJoin.query.filter_by(tenant_id=tenant.id, account_id=account.id).first()
|
||||||
if not ta:
|
if not ta:
|
||||||
raise MemberNotInTenantError("Member not in tenant.")
|
raise MemberNotInTenantError("Member not in tenant.")
|
||||||
|
@ -11,13 +11,17 @@ from core.app.apps.completion.app_generator import CompletionAppGenerator
|
|||||||
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
|
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.app.features.rate_limiting import RateLimit
|
from core.app.features.rate_limiting import RateLimit
|
||||||
|
from libs.helper import RateLimiter
|
||||||
from models.model import Account, App, AppMode, EndUser
|
from models.model import Account, App, AppMode, EndUser
|
||||||
from models.workflow import Workflow
|
from models.workflow import Workflow
|
||||||
|
from services.billing_service import BillingService
|
||||||
from services.errors.llm import InvokeRateLimitError
|
from services.errors.llm import InvokeRateLimitError
|
||||||
from services.workflow_service import WorkflowService
|
from services.workflow_service import WorkflowService
|
||||||
|
|
||||||
|
|
||||||
class AppGenerateService:
|
class AppGenerateService:
|
||||||
|
system_rate_limiter = RateLimiter("app_daily_rate_limiter", dify_config.APP_DAILY_RATE_LIMIT, 86400)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate(
|
def generate(
|
||||||
cls,
|
cls,
|
||||||
@ -36,6 +40,19 @@ class AppGenerateService:
|
|||||||
:param streaming: streaming
|
:param streaming: streaming
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
# system level rate limiter
|
||||||
|
if dify_config.BILLING_ENABLED:
|
||||||
|
# check if it's free plan
|
||||||
|
limit_info = BillingService.get_info(app_model.tenant_id)
|
||||||
|
if limit_info["subscription"]["plan"] == "sandbox":
|
||||||
|
if cls.system_rate_limiter.is_rate_limited(app_model.tenant_id):
|
||||||
|
raise InvokeRateLimitError(
|
||||||
|
"Rate limit exceeded, please upgrade your plan "
|
||||||
|
f"or your RPD was {dify_config.APP_DAILY_RATE_LIMIT} requests/day"
|
||||||
|
)
|
||||||
|
cls.system_rate_limiter.increment_rate_limit(app_model.tenant_id)
|
||||||
|
|
||||||
|
# app level rate limiter
|
||||||
max_active_request = AppGenerateService._get_max_active_requests(app_model)
|
max_active_request = AppGenerateService._get_max_active_requests(app_model)
|
||||||
rate_limit = RateLimit(app_model.id, max_active_request)
|
rate_limit = RateLimit(app_model.id, max_active_request)
|
||||||
request_id = RateLimit.gen_request_key()
|
request_id = RateLimit.gen_request_key()
|
||||||
|
@ -9,7 +9,6 @@ from flask_sqlalchemy.pagination import Pagination
|
|||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants.model_template import default_app_templates
|
from constants.model_template import default_app_templates
|
||||||
from core.agent.entities import AgentToolEntity
|
from core.agent.entities import AgentToolEntity
|
||||||
from core.app.features.rate_limiting import RateLimit
|
|
||||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||||
from core.model_manager import ModelManager
|
from core.model_manager import ModelManager
|
||||||
from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
|
from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
|
||||||
@ -37,9 +36,13 @@ class AppService:
|
|||||||
filters = [App.tenant_id == tenant_id, App.is_universal == False]
|
filters = [App.tenant_id == tenant_id, App.is_universal == False]
|
||||||
|
|
||||||
if args["mode"] == "workflow":
|
if args["mode"] == "workflow":
|
||||||
filters.append(App.mode.in_([AppMode.WORKFLOW.value, AppMode.COMPLETION.value]))
|
filters.append(App.mode == AppMode.WORKFLOW.value)
|
||||||
|
elif args["mode"] == "completion":
|
||||||
|
filters.append(App.mode == AppMode.COMPLETION.value)
|
||||||
elif args["mode"] == "chat":
|
elif args["mode"] == "chat":
|
||||||
filters.append(App.mode.in_([AppMode.CHAT.value, AppMode.ADVANCED_CHAT.value]))
|
filters.append(App.mode == AppMode.CHAT.value)
|
||||||
|
elif args["mode"] == "advanced-chat":
|
||||||
|
filters.append(App.mode == AppMode.ADVANCED_CHAT.value)
|
||||||
elif args["mode"] == "agent-chat":
|
elif args["mode"] == "agent-chat":
|
||||||
filters.append(App.mode == AppMode.AGENT_CHAT.value)
|
filters.append(App.mode == AppMode.AGENT_CHAT.value)
|
||||||
elif args["mode"] == "channel":
|
elif args["mode"] == "channel":
|
||||||
@ -222,7 +225,6 @@ class AppService:
|
|||||||
"""
|
"""
|
||||||
app.name = args.get("name")
|
app.name = args.get("name")
|
||||||
app.description = args.get("description", "")
|
app.description = args.get("description", "")
|
||||||
app.max_active_requests = args.get("max_active_requests")
|
|
||||||
app.icon_type = args.get("icon_type", "emoji")
|
app.icon_type = args.get("icon_type", "emoji")
|
||||||
app.icon = args.get("icon")
|
app.icon = args.get("icon")
|
||||||
app.icon_background = args.get("icon_background")
|
app.icon_background = args.get("icon_background")
|
||||||
@ -231,9 +233,6 @@ class AppService:
|
|||||||
app.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
app.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if app.max_active_requests is not None:
|
|
||||||
rate_limit = RateLimit(app.id, app.max_active_requests)
|
|
||||||
rate_limit.flush_cache(use_local_value=True)
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
def update_app_name(self, app: App, name: str) -> App:
|
def update_app_name(self, app: App, name: str) -> App:
|
||||||
|
313
api/services/clear_free_plan_tenant_expired_logs.py
Normal file
313
api/services/clear_free_plan_tenant_expired_logs.py
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
import click
|
||||||
|
from flask import Flask, current_app
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from extensions.ext_storage import storage
|
||||||
|
from models.account import Tenant
|
||||||
|
from models.model import App, Conversation, Message
|
||||||
|
from models.workflow import WorkflowNodeExecution, WorkflowRun
|
||||||
|
from services.billing_service import BillingService
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ClearFreePlanTenantExpiredLogs:
|
||||||
|
@classmethod
|
||||||
|
def process_tenant(cls, flask_app: Flask, tenant_id: str, days: int, batch: int):
|
||||||
|
with flask_app.app_context():
|
||||||
|
apps = db.session.query(App).filter(App.tenant_id == tenant_id).all()
|
||||||
|
app_ids = [app.id for app in apps]
|
||||||
|
while True:
|
||||||
|
with Session(db.engine).no_autoflush as session:
|
||||||
|
messages = (
|
||||||
|
session.query(Message)
|
||||||
|
.filter(
|
||||||
|
Message.app_id.in_(app_ids),
|
||||||
|
Message.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
|
||||||
|
)
|
||||||
|
.limit(batch)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
if len(messages) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
storage.save(
|
||||||
|
f"free_plan_tenant_expired_logs/"
|
||||||
|
f"{tenant_id}/messages/{datetime.datetime.now().strftime('%Y-%m-%d')}"
|
||||||
|
f"-{time.time()}.json",
|
||||||
|
json.dumps(
|
||||||
|
jsonable_encoder(
|
||||||
|
[message.to_dict() for message in messages],
|
||||||
|
),
|
||||||
|
).encode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
message_ids = [message.id for message in messages]
|
||||||
|
|
||||||
|
# delete messages
|
||||||
|
session.query(Message).filter(
|
||||||
|
Message.id.in_(message_ids),
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"[{datetime.datetime.now()}] Processed {len(message_ids)} messages for tenant {tenant_id} "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
with Session(db.engine).no_autoflush as session:
|
||||||
|
conversations = (
|
||||||
|
session.query(Conversation)
|
||||||
|
.filter(
|
||||||
|
Conversation.app_id.in_(app_ids),
|
||||||
|
Conversation.updated_at < datetime.datetime.now() - datetime.timedelta(days=days),
|
||||||
|
)
|
||||||
|
.limit(batch)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(conversations) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
storage.save(
|
||||||
|
f"free_plan_tenant_expired_logs/"
|
||||||
|
f"{tenant_id}/conversations/{datetime.datetime.now().strftime('%Y-%m-%d')}"
|
||||||
|
f"-{time.time()}.json",
|
||||||
|
json.dumps(
|
||||||
|
jsonable_encoder(
|
||||||
|
[conversation.to_dict() for conversation in conversations],
|
||||||
|
),
|
||||||
|
).encode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation_ids = [conversation.id for conversation in conversations]
|
||||||
|
session.query(Conversation).filter(
|
||||||
|
Conversation.id.in_(conversation_ids),
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"[{datetime.datetime.now()}] Processed {len(conversation_ids)}"
|
||||||
|
f" conversations for tenant {tenant_id}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
with Session(db.engine).no_autoflush as session:
|
||||||
|
workflow_node_executions = (
|
||||||
|
session.query(WorkflowNodeExecution)
|
||||||
|
.filter(
|
||||||
|
WorkflowNodeExecution.tenant_id == tenant_id,
|
||||||
|
WorkflowNodeExecution.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
|
||||||
|
)
|
||||||
|
.limit(batch)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(workflow_node_executions) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# save workflow node executions
|
||||||
|
storage.save(
|
||||||
|
f"free_plan_tenant_expired_logs/"
|
||||||
|
f"{tenant_id}/workflow_node_executions/{datetime.datetime.now().strftime('%Y-%m-%d')}"
|
||||||
|
f"-{time.time()}.json",
|
||||||
|
json.dumps(
|
||||||
|
jsonable_encoder(workflow_node_executions),
|
||||||
|
).encode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow_node_execution_ids = [
|
||||||
|
workflow_node_execution.id for workflow_node_execution in workflow_node_executions
|
||||||
|
]
|
||||||
|
|
||||||
|
# delete workflow node executions
|
||||||
|
session.query(WorkflowNodeExecution).filter(
|
||||||
|
WorkflowNodeExecution.id.in_(workflow_node_execution_ids),
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"[{datetime.datetime.now()}] Processed {len(workflow_node_execution_ids)}"
|
||||||
|
f" workflow node executions for tenant {tenant_id}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
with Session(db.engine).no_autoflush as session:
|
||||||
|
workflow_runs = (
|
||||||
|
session.query(WorkflowRun)
|
||||||
|
.filter(
|
||||||
|
WorkflowRun.tenant_id == tenant_id,
|
||||||
|
WorkflowRun.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
|
||||||
|
)
|
||||||
|
.limit(batch)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(workflow_runs) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# save workflow runs
|
||||||
|
|
||||||
|
storage.save(
|
||||||
|
f"free_plan_tenant_expired_logs/"
|
||||||
|
f"{tenant_id}/workflow_runs/{datetime.datetime.now().strftime('%Y-%m-%d')}"
|
||||||
|
f"-{time.time()}.json",
|
||||||
|
json.dumps(
|
||||||
|
jsonable_encoder(
|
||||||
|
[workflow_run.to_dict() for workflow_run in workflow_runs],
|
||||||
|
),
|
||||||
|
).encode("utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow_run_ids = [workflow_run.id for workflow_run in workflow_runs]
|
||||||
|
|
||||||
|
# delete workflow runs
|
||||||
|
session.query(WorkflowRun).filter(
|
||||||
|
WorkflowRun.id.in_(workflow_run_ids),
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, days: int, batch: int, tenant_ids: list[str]):
|
||||||
|
"""
|
||||||
|
Clear free plan tenant expired logs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
click.echo(click.style("Clearing free plan tenant expired logs", fg="white"))
|
||||||
|
ended_at = datetime.datetime.now()
|
||||||
|
started_at = datetime.datetime(2023, 4, 3, 8, 59, 24)
|
||||||
|
current_time = started_at
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
total_tenant_count = session.query(Tenant.id).count()
|
||||||
|
|
||||||
|
click.echo(click.style(f"Total tenant count: {total_tenant_count}", fg="white"))
|
||||||
|
|
||||||
|
handled_tenant_count = 0
|
||||||
|
|
||||||
|
thread_pool = ThreadPoolExecutor(max_workers=10)
|
||||||
|
|
||||||
|
def process_tenant(flask_app: Flask, tenant_id: str) -> None:
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
not dify_config.BILLING_ENABLED
|
||||||
|
or BillingService.get_info(tenant_id)["subscription"]["plan"] == "sandbox"
|
||||||
|
):
|
||||||
|
# only process sandbox tenant
|
||||||
|
cls.process_tenant(flask_app, tenant_id, days, batch)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Failed to process tenant {tenant_id}")
|
||||||
|
finally:
|
||||||
|
nonlocal handled_tenant_count
|
||||||
|
handled_tenant_count += 1
|
||||||
|
if handled_tenant_count % 100 == 0:
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f"[{datetime.datetime.now()}] "
|
||||||
|
f"Processed {handled_tenant_count} tenants "
|
||||||
|
f"({(handled_tenant_count / total_tenant_count) * 100:.1f}%), "
|
||||||
|
f"{handled_tenant_count}/{total_tenant_count}",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
futures = []
|
||||||
|
|
||||||
|
if tenant_ids:
|
||||||
|
for tenant_id in tenant_ids:
|
||||||
|
futures.append(
|
||||||
|
thread_pool.submit(
|
||||||
|
process_tenant,
|
||||||
|
current_app._get_current_object(), # type: ignore[attr-defined]
|
||||||
|
tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
while current_time < ended_at:
|
||||||
|
click.echo(
|
||||||
|
click.style(f"Current time: {current_time}, Started at: {datetime.datetime.now()}", fg="white")
|
||||||
|
)
|
||||||
|
# Initial interval of 1 day, will be dynamically adjusted based on tenant count
|
||||||
|
interval = datetime.timedelta(days=1)
|
||||||
|
# Process tenants in this batch
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
# Calculate tenant count in next batch with current interval
|
||||||
|
# Try different intervals until we find one with a reasonable tenant count
|
||||||
|
test_intervals = [
|
||||||
|
datetime.timedelta(days=1),
|
||||||
|
datetime.timedelta(hours=12),
|
||||||
|
datetime.timedelta(hours=6),
|
||||||
|
datetime.timedelta(hours=3),
|
||||||
|
datetime.timedelta(hours=1),
|
||||||
|
]
|
||||||
|
|
||||||
|
for test_interval in test_intervals:
|
||||||
|
tenant_count = (
|
||||||
|
session.query(Tenant.id)
|
||||||
|
.filter(Tenant.created_at.between(current_time, current_time + test_interval))
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
if tenant_count <= 100:
|
||||||
|
interval = test_interval
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# If all intervals have too many tenants, use minimum interval
|
||||||
|
interval = datetime.timedelta(hours=1)
|
||||||
|
|
||||||
|
# Adjust interval to target ~100 tenants per batch
|
||||||
|
if tenant_count > 0:
|
||||||
|
# Scale interval based on ratio to target count
|
||||||
|
interval = min(
|
||||||
|
datetime.timedelta(days=1), # Max 1 day
|
||||||
|
max(
|
||||||
|
datetime.timedelta(hours=1), # Min 1 hour
|
||||||
|
interval * (100 / tenant_count), # Scale to target 100
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_end = min(current_time + interval, ended_at)
|
||||||
|
|
||||||
|
rs = (
|
||||||
|
session.query(Tenant.id)
|
||||||
|
.filter(Tenant.created_at.between(current_time, batch_end))
|
||||||
|
.order_by(Tenant.created_at)
|
||||||
|
)
|
||||||
|
|
||||||
|
tenants = []
|
||||||
|
for row in rs:
|
||||||
|
tenant_id = str(row.id)
|
||||||
|
try:
|
||||||
|
tenants.append(tenant_id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Failed to process tenant {tenant_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
futures.append(
|
||||||
|
thread_pool.submit(
|
||||||
|
process_tenant,
|
||||||
|
current_app._get_current_object(), # type: ignore[attr-defined]
|
||||||
|
tenant_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
current_time = batch_end
|
||||||
|
|
||||||
|
# wait for all threads to finish
|
||||||
|
for future in futures:
|
||||||
|
future.result()
|
@ -46,7 +46,6 @@ from models.source import DataSourceOauthBinding
|
|||||||
from services.entities.knowledge_entities.knowledge_entities import (
|
from services.entities.knowledge_entities.knowledge_entities import (
|
||||||
ChildChunkUpdateArgs,
|
ChildChunkUpdateArgs,
|
||||||
KnowledgeConfig,
|
KnowledgeConfig,
|
||||||
MetaDataConfig,
|
|
||||||
RerankingModel,
|
RerankingModel,
|
||||||
RetrievalModel,
|
RetrievalModel,
|
||||||
SegmentUpdateArgs,
|
SegmentUpdateArgs,
|
||||||
@ -999,9 +998,6 @@ class DocumentService:
|
|||||||
document.data_source_info = json.dumps(data_source_info)
|
document.data_source_info = json.dumps(data_source_info)
|
||||||
document.batch = batch
|
document.batch = batch
|
||||||
document.indexing_status = "waiting"
|
document.indexing_status = "waiting"
|
||||||
if knowledge_config.metadata:
|
|
||||||
document.doc_type = knowledge_config.metadata.doc_type
|
|
||||||
document.metadata = knowledge_config.metadata.doc_metadata
|
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
documents.append(document)
|
documents.append(document)
|
||||||
duplicate_document_ids.append(document.id)
|
duplicate_document_ids.append(document.id)
|
||||||
@ -1018,7 +1014,6 @@ class DocumentService:
|
|||||||
account,
|
account,
|
||||||
file_name,
|
file_name,
|
||||||
batch,
|
batch,
|
||||||
knowledge_config.metadata,
|
|
||||||
)
|
)
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
@ -1076,7 +1071,6 @@ class DocumentService:
|
|||||||
account,
|
account,
|
||||||
truncated_page_name,
|
truncated_page_name,
|
||||||
batch,
|
batch,
|
||||||
knowledge_config.metadata,
|
|
||||||
)
|
)
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
@ -1117,7 +1111,6 @@ class DocumentService:
|
|||||||
account,
|
account,
|
||||||
document_name,
|
document_name,
|
||||||
batch,
|
batch,
|
||||||
knowledge_config.metadata,
|
|
||||||
)
|
)
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
@ -1155,7 +1148,6 @@ class DocumentService:
|
|||||||
account: Account,
|
account: Account,
|
||||||
name: str,
|
name: str,
|
||||||
batch: str,
|
batch: str,
|
||||||
metadata: Optional[MetaDataConfig] = None,
|
|
||||||
):
|
):
|
||||||
document = Document(
|
document = Document(
|
||||||
tenant_id=dataset.tenant_id,
|
tenant_id=dataset.tenant_id,
|
||||||
@ -1180,9 +1172,6 @@ class DocumentService:
|
|||||||
BuiltInField.last_update_date: datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d %H:%M:%S"),
|
BuiltInField.last_update_date: datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
BuiltInField.source: data_source_type,
|
BuiltInField.source: data_source_type,
|
||||||
}
|
}
|
||||||
if metadata is not None:
|
|
||||||
doc_metadata.update(metadata.doc_metadata)
|
|
||||||
document.doc_type = metadata.doc_type
|
|
||||||
if doc_metadata:
|
if doc_metadata:
|
||||||
document.doc_metadata = doc_metadata
|
document.doc_metadata = doc_metadata
|
||||||
return document
|
return document
|
||||||
@ -1297,10 +1286,6 @@ class DocumentService:
|
|||||||
# update document name
|
# update document name
|
||||||
if document_data.name:
|
if document_data.name:
|
||||||
document.name = document_data.name
|
document.name = document_data.name
|
||||||
# update doc_type and doc_metadata if provided
|
|
||||||
if document_data.metadata is not None:
|
|
||||||
document.doc_metadata = document_data.metadata.doc_metadata
|
|
||||||
document.doc_type = document_data.metadata.doc_type
|
|
||||||
# update document to be waiting
|
# update document to be waiting
|
||||||
document.indexing_status = "waiting"
|
document.indexing_status = "waiting"
|
||||||
document.completed_at = None
|
document.completed_at = None
|
||||||
|
@ -84,6 +84,22 @@ class RerankingModel(BaseModel):
|
|||||||
reranking_model_name: Optional[str] = None
|
reranking_model_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class WeightVectorSetting(BaseModel):
|
||||||
|
vector_weight: float
|
||||||
|
embedding_provider_name: str
|
||||||
|
embedding_model_name: str
|
||||||
|
|
||||||
|
|
||||||
|
class WeightKeywordSetting(BaseModel):
|
||||||
|
keyword_weight: float
|
||||||
|
|
||||||
|
|
||||||
|
class WeightModel(BaseModel):
|
||||||
|
weight_type: str
|
||||||
|
vector_setting: Optional[WeightVectorSetting] = None
|
||||||
|
keyword_setting: Optional[WeightKeywordSetting] = None
|
||||||
|
|
||||||
|
|
||||||
class RetrievalModel(BaseModel):
|
class RetrievalModel(BaseModel):
|
||||||
search_method: Literal["hybrid_search", "semantic_search", "full_text_search"]
|
search_method: Literal["hybrid_search", "semantic_search", "full_text_search"]
|
||||||
reranking_enable: bool
|
reranking_enable: bool
|
||||||
@ -92,6 +108,7 @@ class RetrievalModel(BaseModel):
|
|||||||
top_k: int
|
top_k: int
|
||||||
score_threshold_enabled: bool
|
score_threshold_enabled: bool
|
||||||
score_threshold: Optional[float] = None
|
score_threshold: Optional[float] = None
|
||||||
|
weights: Optional[WeightModel] = None
|
||||||
|
|
||||||
|
|
||||||
class MetaDataConfig(BaseModel):
|
class MetaDataConfig(BaseModel):
|
||||||
@ -111,7 +128,6 @@ class KnowledgeConfig(BaseModel):
|
|||||||
embedding_model: Optional[str] = None
|
embedding_model: Optional[str] = None
|
||||||
embedding_model_provider: Optional[str] = None
|
embedding_model_provider: Optional[str] = None
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
metadata: Optional[MetaDataConfig] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentUpdateArgs(BaseModel):
|
class SegmentUpdateArgs(BaseModel):
|
||||||
|
@ -137,7 +137,7 @@ class MetadataService:
|
|||||||
doc_metadata[BuiltInField.source.value] = MetadataDataSource[document.data_source_type].value
|
doc_metadata[BuiltInField.source.value] = MetadataDataSource[document.data_source_type].value
|
||||||
document.doc_metadata = doc_metadata
|
document.doc_metadata = doc_metadata
|
||||||
db.session.add(document)
|
db.session.add(document)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Enable built-in field failed")
|
logging.exception("Enable built-in field failed")
|
||||||
finally:
|
finally:
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult
|
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult
|
||||||
from core.workflow.entities.variable_pool import VariablePool
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
@ -17,12 +20,20 @@ from core.workflow.graph_engine.entities.event import (
|
|||||||
from core.workflow.graph_engine.entities.graph import Graph
|
from core.workflow.graph_engine.entities.graph import Graph
|
||||||
from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeState
|
from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeState
|
||||||
from core.workflow.graph_engine.graph_engine import GraphEngine
|
from core.workflow.graph_engine.graph_engine import GraphEngine
|
||||||
|
from core.workflow.nodes.code.code_node import CodeNode
|
||||||
from core.workflow.nodes.event import RunCompletedEvent, RunStreamChunkEvent
|
from core.workflow.nodes.event import RunCompletedEvent, RunStreamChunkEvent
|
||||||
from core.workflow.nodes.llm.node import LLMNode
|
from core.workflow.nodes.llm.node import LLMNode
|
||||||
|
from core.workflow.nodes.question_classifier.question_classifier_node import QuestionClassifierNode
|
||||||
from models.enums import UserFrom
|
from models.enums import UserFrom
|
||||||
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
|
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
@patch("extensions.ext_database.db.session.remove")
|
@patch("extensions.ext_database.db.session.remove")
|
||||||
@patch("extensions.ext_database.db.session.close")
|
@patch("extensions.ext_database.db.session.close")
|
||||||
def test_run_parallel_in_workflow(mock_close, mock_remove):
|
def test_run_parallel_in_workflow(mock_close, mock_remove):
|
||||||
@ -502,3 +513,361 @@ def test_run_branch(mock_close, mock_remove):
|
|||||||
assert isinstance(items[9], GraphRunSucceededEvent)
|
assert isinstance(items[9], GraphRunSucceededEvent)
|
||||||
|
|
||||||
# print(graph_engine.graph_runtime_state.model_dump_json(indent=2))
|
# print(graph_engine.graph_runtime_state.model_dump_json(indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
@patch("extensions.ext_database.db.session.remove")
|
||||||
|
@patch("extensions.ext_database.db.session.close")
|
||||||
|
def test_condition_parallel_correct_output(mock_close, mock_remove, app):
|
||||||
|
"""issue #16238, workflow got unexpected additional output"""
|
||||||
|
|
||||||
|
graph_config = {
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"isInIteration": False,
|
||||||
|
"isInLoop": False,
|
||||||
|
"sourceType": "question-classifier",
|
||||||
|
"targetType": "question-classifier",
|
||||||
|
},
|
||||||
|
"id": "1742382406742-1-1742382480077-target",
|
||||||
|
"source": "1742382406742",
|
||||||
|
"sourceHandle": "1",
|
||||||
|
"target": "1742382480077",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"isInIteration": False,
|
||||||
|
"isInLoop": False,
|
||||||
|
"sourceType": "question-classifier",
|
||||||
|
"targetType": "answer",
|
||||||
|
},
|
||||||
|
"id": "1742382480077-1-1742382531085-target",
|
||||||
|
"source": "1742382480077",
|
||||||
|
"sourceHandle": "1",
|
||||||
|
"target": "1742382531085",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"isInIteration": False,
|
||||||
|
"isInLoop": False,
|
||||||
|
"sourceType": "question-classifier",
|
||||||
|
"targetType": "answer",
|
||||||
|
},
|
||||||
|
"id": "1742382480077-2-1742382534798-target",
|
||||||
|
"source": "1742382480077",
|
||||||
|
"sourceHandle": "2",
|
||||||
|
"target": "1742382534798",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"isInIteration": False,
|
||||||
|
"isInLoop": False,
|
||||||
|
"sourceType": "question-classifier",
|
||||||
|
"targetType": "answer",
|
||||||
|
},
|
||||||
|
"id": "1742382480077-1742382525856-1742382538517-target",
|
||||||
|
"source": "1742382480077",
|
||||||
|
"sourceHandle": "1742382525856",
|
||||||
|
"target": "1742382538517",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {"isInLoop": False, "sourceType": "start", "targetType": "question-classifier"},
|
||||||
|
"id": "1742382361944-source-1742382406742-target",
|
||||||
|
"source": "1742382361944",
|
||||||
|
"sourceHandle": "source",
|
||||||
|
"target": "1742382406742",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"isInIteration": False,
|
||||||
|
"isInLoop": False,
|
||||||
|
"sourceType": "question-classifier",
|
||||||
|
"targetType": "code",
|
||||||
|
},
|
||||||
|
"id": "1742382406742-1-1742451801533-target",
|
||||||
|
"source": "1742382406742",
|
||||||
|
"sourceHandle": "1",
|
||||||
|
"target": "1742451801533",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {"isInLoop": False, "sourceType": "code", "targetType": "answer"},
|
||||||
|
"id": "1742451801533-source-1742434464898-target",
|
||||||
|
"source": "1742451801533",
|
||||||
|
"sourceHandle": "source",
|
||||||
|
"target": "1742434464898",
|
||||||
|
"targetHandle": "target",
|
||||||
|
"type": "custom",
|
||||||
|
"zIndex": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {"desc": "", "selected": False, "title": "开始", "type": "start", "variables": []},
|
||||||
|
"height": 54,
|
||||||
|
"id": "1742382361944",
|
||||||
|
"position": {"x": 30, "y": 286},
|
||||||
|
"positionAbsolute": {"x": 30, "y": 286},
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"classes": [{"id": "1", "name": "financial"}, {"id": "2", "name": "other"}],
|
||||||
|
"desc": "",
|
||||||
|
"instruction": "",
|
||||||
|
"instructions": "",
|
||||||
|
"model": {
|
||||||
|
"completion_params": {"temperature": 0.7},
|
||||||
|
"mode": "chat",
|
||||||
|
"name": "qwen-max-latest",
|
||||||
|
"provider": "langgenius/tongyi/tongyi",
|
||||||
|
},
|
||||||
|
"query_variable_selector": ["1742382361944", "sys.query"],
|
||||||
|
"selected": False,
|
||||||
|
"title": "qc",
|
||||||
|
"topics": [],
|
||||||
|
"type": "question-classifier",
|
||||||
|
"vision": {"enabled": False},
|
||||||
|
},
|
||||||
|
"height": 172,
|
||||||
|
"id": "1742382406742",
|
||||||
|
"position": {"x": 334, "y": 286},
|
||||||
|
"positionAbsolute": {"x": 334, "y": 286},
|
||||||
|
"selected": False,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"classes": [
|
||||||
|
{"id": "1", "name": "VAT"},
|
||||||
|
{"id": "2", "name": "Stamp Duty"},
|
||||||
|
{"id": "1742382525856", "name": "other"},
|
||||||
|
],
|
||||||
|
"desc": "",
|
||||||
|
"instruction": "",
|
||||||
|
"instructions": "",
|
||||||
|
"model": {
|
||||||
|
"completion_params": {"temperature": 0.7},
|
||||||
|
"mode": "chat",
|
||||||
|
"name": "qwen-max-latest",
|
||||||
|
"provider": "langgenius/tongyi/tongyi",
|
||||||
|
},
|
||||||
|
"query_variable_selector": ["1742382361944", "sys.query"],
|
||||||
|
"selected": False,
|
||||||
|
"title": "qc 2",
|
||||||
|
"topics": [],
|
||||||
|
"type": "question-classifier",
|
||||||
|
"vision": {"enabled": False},
|
||||||
|
},
|
||||||
|
"height": 210,
|
||||||
|
"id": "1742382480077",
|
||||||
|
"position": {"x": 638, "y": 452},
|
||||||
|
"positionAbsolute": {"x": 638, "y": 452},
|
||||||
|
"selected": False,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"answer": "VAT:{{#sys.query#}}\n",
|
||||||
|
"desc": "",
|
||||||
|
"selected": False,
|
||||||
|
"title": "answer 2",
|
||||||
|
"type": "answer",
|
||||||
|
"variables": [],
|
||||||
|
},
|
||||||
|
"height": 105,
|
||||||
|
"id": "1742382531085",
|
||||||
|
"position": {"x": 942, "y": 486.5},
|
||||||
|
"positionAbsolute": {"x": 942, "y": 486.5},
|
||||||
|
"selected": False,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"answer": "Stamp Duty:{{#sys.query#}}\n",
|
||||||
|
"desc": "",
|
||||||
|
"selected": False,
|
||||||
|
"title": "answer 3",
|
||||||
|
"type": "answer",
|
||||||
|
"variables": [],
|
||||||
|
},
|
||||||
|
"height": 105,
|
||||||
|
"id": "1742382534798",
|
||||||
|
"position": {"x": 942, "y": 631.5},
|
||||||
|
"positionAbsolute": {"x": 942, "y": 631.5},
|
||||||
|
"selected": False,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"answer": "other:{{#sys.query#}}\n",
|
||||||
|
"desc": "",
|
||||||
|
"selected": False,
|
||||||
|
"title": "answer 4",
|
||||||
|
"type": "answer",
|
||||||
|
"variables": [],
|
||||||
|
},
|
||||||
|
"height": 105,
|
||||||
|
"id": "1742382538517",
|
||||||
|
"position": {"x": 942, "y": 776.5},
|
||||||
|
"positionAbsolute": {"x": 942, "y": 776.5},
|
||||||
|
"selected": False,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"answer": "{{#1742451801533.result#}}",
|
||||||
|
"desc": "",
|
||||||
|
"selected": False,
|
||||||
|
"title": "Answer 5",
|
||||||
|
"type": "answer",
|
||||||
|
"variables": [],
|
||||||
|
},
|
||||||
|
"height": 105,
|
||||||
|
"id": "1742434464898",
|
||||||
|
"position": {"x": 942, "y": 274.70425695336615},
|
||||||
|
"positionAbsolute": {"x": 942, "y": 274.70425695336615},
|
||||||
|
"selected": True,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"code": '\ndef main(arg1: str, arg2: str) -> dict:\n return {\n "result": arg1 + arg2,\n }\n', # noqa: E501
|
||||||
|
"code_language": "python3",
|
||||||
|
"desc": "",
|
||||||
|
"outputs": {"result": {"children": None, "type": "string"}},
|
||||||
|
"selected": False,
|
||||||
|
"title": "Code",
|
||||||
|
"type": "code",
|
||||||
|
"variables": [
|
||||||
|
{"value_selector": ["sys", "query"], "variable": "arg1"},
|
||||||
|
{"value_selector": ["sys", "query"], "variable": "arg2"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"height": 54,
|
||||||
|
"id": "1742451801533",
|
||||||
|
"position": {"x": 627.8839285786928, "y": 286},
|
||||||
|
"positionAbsolute": {"x": 627.8839285786928, "y": 286},
|
||||||
|
"selected": False,
|
||||||
|
"sourcePosition": "right",
|
||||||
|
"targetPosition": "left",
|
||||||
|
"type": "custom",
|
||||||
|
"width": 244,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
graph = Graph.init(graph_config)
|
||||||
|
|
||||||
|
# construct variable pool
|
||||||
|
pool = VariablePool(
|
||||||
|
system_variables={
|
||||||
|
SystemVariableKey.QUERY: "dify",
|
||||||
|
SystemVariableKey.FILES: [],
|
||||||
|
SystemVariableKey.CONVERSATION_ID: "abababa",
|
||||||
|
SystemVariableKey.USER_ID: "1",
|
||||||
|
},
|
||||||
|
user_inputs={},
|
||||||
|
environment_variables=[],
|
||||||
|
)
|
||||||
|
pool.add(["pe", "list_output"], ["dify-1", "dify-2"])
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={"query": "hi"}
|
||||||
|
)
|
||||||
|
|
||||||
|
graph_engine = GraphEngine(
|
||||||
|
tenant_id="111",
|
||||||
|
app_id="222",
|
||||||
|
workflow_type=WorkflowType.CHAT,
|
||||||
|
workflow_id="333",
|
||||||
|
graph_config=graph_config,
|
||||||
|
user_id="444",
|
||||||
|
user_from=UserFrom.ACCOUNT,
|
||||||
|
invoke_from=InvokeFrom.WEB_APP,
|
||||||
|
call_depth=0,
|
||||||
|
graph=graph,
|
||||||
|
variable_pool=variable_pool,
|
||||||
|
max_execution_steps=500,
|
||||||
|
max_execution_time=1200,
|
||||||
|
)
|
||||||
|
|
||||||
|
def qc_generator(self):
|
||||||
|
yield RunCompletedEvent(
|
||||||
|
run_result=NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
|
inputs={},
|
||||||
|
process_data={},
|
||||||
|
outputs={"class_name": "financial", "class_id": "1"},
|
||||||
|
metadata={
|
||||||
|
NodeRunMetadataKey.TOTAL_TOKENS: 1,
|
||||||
|
NodeRunMetadataKey.TOTAL_PRICE: 1,
|
||||||
|
NodeRunMetadataKey.CURRENCY: "USD",
|
||||||
|
},
|
||||||
|
edge_source_handle="1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def code_generator(self):
|
||||||
|
yield RunCompletedEvent(
|
||||||
|
run_result=NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
|
inputs={},
|
||||||
|
process_data={},
|
||||||
|
outputs={"result": "dify 123"},
|
||||||
|
metadata={
|
||||||
|
NodeRunMetadataKey.TOTAL_TOKENS: 1,
|
||||||
|
NodeRunMetadataKey.TOTAL_PRICE: 1,
|
||||||
|
NodeRunMetadataKey.CURRENCY: "USD",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(QuestionClassifierNode, "_run", new=qc_generator):
|
||||||
|
with app.app_context():
|
||||||
|
with patch.object(CodeNode, "_run", new=code_generator):
|
||||||
|
generator = graph_engine.run()
|
||||||
|
stream_content = ""
|
||||||
|
res_content = "VAT:\ndify 123"
|
||||||
|
for item in generator:
|
||||||
|
if isinstance(item, NodeRunStreamChunkEvent):
|
||||||
|
stream_content += f"{item.chunk_content}\n"
|
||||||
|
if isinstance(item, GraphRunSucceededEvent):
|
||||||
|
assert item.outputs == {"answer": res_content}
|
||||||
|
assert stream_content == res_content + "\n"
|
||||||
|
@ -68,7 +68,7 @@ DEBUG=false
|
|||||||
# which is convenient for debugging.
|
# which is convenient for debugging.
|
||||||
FLASK_DEBUG=false
|
FLASK_DEBUG=false
|
||||||
|
|
||||||
# A secretkey that is used for securely signing the session cookie
|
# A secret key that is used for securely signing the session cookie
|
||||||
# and encrypting sensitive information on the database.
|
# and encrypting sensitive information on the database.
|
||||||
# You can generate a strong key using `openssl rand -base64 42`.
|
# You can generate a strong key using `openssl rand -base64 42`.
|
||||||
SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
|
SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
|
||||||
@ -76,7 +76,7 @@ SECRET_KEY=sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
|
|||||||
# Password for admin user initialization.
|
# Password for admin user initialization.
|
||||||
# If left unset, admin user will not be prompted for a password
|
# If left unset, admin user will not be prompted for a password
|
||||||
# when creating the initial admin account.
|
# when creating the initial admin account.
|
||||||
# The length of the password cannot exceed 30 charactors.
|
# The length of the password cannot exceed 30 characters.
|
||||||
INIT_PASSWORD=
|
INIT_PASSWORD=
|
||||||
|
|
||||||
# Deployment environment.
|
# Deployment environment.
|
||||||
|
@ -27,7 +27,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T
|
|||||||
- Execute `docker compose up` from the `docker` directory to start the services.
|
- Execute `docker compose up` from the `docker` directory to start the services.
|
||||||
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
|
- To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`.
|
||||||
4. **SSL Certificate Setup**:
|
4. **SSL Certificate Setup**:
|
||||||
- Rrefer `docker/certbot/README.md` to set up SSL certificates using Certbot.
|
- Refer `docker/certbot/README.md` to set up SSL certificates using Certbot.
|
||||||
|
|
||||||
### How to Deploy Middleware for Developing Dify
|
### How to Deploy Middleware for Developing Dify
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ For users migrating from the `docker-legacy` setup:
|
|||||||
|
|
||||||
- **Vector Database Services**: Depending on the type of vector database used (`VECTOR_STORE`), users can set specific endpoints, ports, and authentication details.
|
- **Vector Database Services**: Depending on the type of vector database used (`VECTOR_STORE`), users can set specific endpoints, ports, and authentication details.
|
||||||
- **Storage Services**: Depending on the storage type (`STORAGE_TYPE`), users can configure specific settings for S3, Azure Blob, Google Storage, etc.
|
- **Storage Services**: Depending on the storage type (`STORAGE_TYPE`), users can configure specific settings for S3, Azure Blob, Google Storage, etc.
|
||||||
- **API and Web Services**: Users can define URLs and other settings that affect how the API and web frontends operate.
|
- **API and Web Services**: Users can define URLs and other settings that affect how the API and web frontend operate.
|
||||||
|
|
||||||
#### Other notable variables
|
#### Other notable variables
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
services:
|
services:
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
image: langgenius/dify-api:1.1.0
|
image: langgenius/dify-api:1.1.2
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -29,7 +29,7 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing the queue.
|
# The Celery worker for processing the queue.
|
||||||
worker:
|
worker:
|
||||||
image: langgenius/dify-api:1.1.0
|
image: langgenius/dify-api:1.1.2
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -53,7 +53,7 @@ services:
|
|||||||
|
|
||||||
# Frontend web application.
|
# Frontend web application.
|
||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:1.1.0
|
image: langgenius/dify-web:1.1.2
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
@ -133,7 +133,7 @@ services:
|
|||||||
|
|
||||||
# plugin daemon
|
# plugin daemon
|
||||||
plugin_daemon:
|
plugin_daemon:
|
||||||
image: langgenius/dify-plugin-daemon:0.0.5-local
|
image: langgenius/dify-plugin-daemon:0.0.6-local
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
|
@ -66,7 +66,7 @@ services:
|
|||||||
|
|
||||||
# plugin daemon
|
# plugin daemon
|
||||||
plugin_daemon:
|
plugin_daemon:
|
||||||
image: langgenius/dify-plugin-daemon:0.0.5-local
|
image: langgenius/dify-plugin-daemon:0.0.6-local
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
|
@ -433,7 +433,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
services:
|
services:
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
image: langgenius/dify-api:1.1.0
|
image: langgenius/dify-api:1.1.2
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -460,7 +460,7 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing the queue.
|
# The Celery worker for processing the queue.
|
||||||
worker:
|
worker:
|
||||||
image: langgenius/dify-api:1.1.0
|
image: langgenius/dify-api:1.1.2
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -484,7 +484,7 @@ services:
|
|||||||
|
|
||||||
# Frontend web application.
|
# Frontend web application.
|
||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:1.1.0
|
image: langgenius/dify-web:1.1.2
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
@ -564,7 +564,7 @@ services:
|
|||||||
|
|
||||||
# plugin daemon
|
# plugin daemon
|
||||||
plugin_daemon:
|
plugin_daemon:
|
||||||
image: langgenius/dify-plugin-daemon:0.0.5-local
|
image: langgenius/dify-plugin-daemon:0.0.6-local
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
|
@ -3,7 +3,7 @@ import Main from '@/app/components/app/log-annotation'
|
|||||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||||
|
|
||||||
export type IProps = {
|
export type IProps = {
|
||||||
params: { appId: string }
|
params: Promise<{ appId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Logs = async () => {
|
const Logs = async () => {
|
||||||
|
@ -3,12 +3,16 @@ import type { Locale } from '@/i18n'
|
|||||||
import DevelopMain from '@/app/components/develop'
|
import DevelopMain from '@/app/components/develop'
|
||||||
|
|
||||||
export type IDevelopProps = {
|
export type IDevelopProps = {
|
||||||
params: { locale: Locale; appId: string }
|
params: Promise<{ locale: Locale; appId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Develop = async ({
|
const Develop = async (props: IDevelopProps) => {
|
||||||
params: { appId },
|
const params = await props.params
|
||||||
}: IDevelopProps) => {
|
|
||||||
|
const {
|
||||||
|
appId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return <DevelopMain appId={appId} />
|
return <DevelopMain appId={appId} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,177 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useUnmount } from 'ahooks'
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
|
import {
|
||||||
|
RiDashboard2Fill,
|
||||||
|
RiDashboard2Line,
|
||||||
|
RiFileList3Fill,
|
||||||
|
RiFileList3Line,
|
||||||
|
RiTerminalBoxFill,
|
||||||
|
RiTerminalBoxLine,
|
||||||
|
RiTerminalWindowFill,
|
||||||
|
RiTerminalWindowLine,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import { useContextSelector } from 'use-context-selector'
|
||||||
|
import s from './style.module.css'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { useStore } from '@/app/components/app/store'
|
||||||
|
import AppSideBar from '@/app/components/app-sidebar'
|
||||||
|
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||||
|
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
||||||
|
import AppContext, { useAppContext } from '@/context/app-context'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
|
import type { App } from '@/types/app'
|
||||||
|
|
||||||
|
export type IAppDetailLayoutProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
appId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
appId, // get appId in path
|
||||||
|
} = props
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const media = useBreakpoints()
|
||||||
|
const isMobile = media === MediaType.mobile
|
||||||
|
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
|
||||||
|
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
|
||||||
|
appDetail: state.appDetail,
|
||||||
|
setAppDetail: state.setAppDetail,
|
||||||
|
setAppSiderbarExpand: state.setAppSiderbarExpand,
|
||||||
|
})))
|
||||||
|
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
|
||||||
|
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
|
||||||
|
const [navigation, setNavigation] = useState<Array<{
|
||||||
|
name: string
|
||||||
|
href: string
|
||||||
|
icon: NavIcon
|
||||||
|
selectedIcon: NavIcon
|
||||||
|
}>>([])
|
||||||
|
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
||||||
|
|
||||||
|
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
|
||||||
|
const navs = [
|
||||||
|
...(isCurrentWorkspaceEditor
|
||||||
|
? [{
|
||||||
|
name: t('common.appMenus.promptEng'),
|
||||||
|
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
|
||||||
|
icon: RiTerminalWindowLine,
|
||||||
|
selectedIcon: RiTerminalWindowFill,
|
||||||
|
}]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: t('common.appMenus.apiAccess'),
|
||||||
|
href: `/app/${appId}/develop`,
|
||||||
|
icon: RiTerminalBoxLine,
|
||||||
|
selectedIcon: RiTerminalBoxFill,
|
||||||
|
},
|
||||||
|
...(isCurrentWorkspaceEditor
|
||||||
|
? [{
|
||||||
|
name: mode !== 'workflow'
|
||||||
|
? t('common.appMenus.logAndAnn')
|
||||||
|
: t('common.appMenus.logs'),
|
||||||
|
href: `/app/${appId}/logs`,
|
||||||
|
icon: RiFileList3Line,
|
||||||
|
selectedIcon: RiFileList3Fill,
|
||||||
|
}]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: t('common.appMenus.overview'),
|
||||||
|
href: `/app/${appId}/overview`,
|
||||||
|
icon: RiDashboard2Line,
|
||||||
|
selectedIcon: RiDashboard2Fill,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return navs
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appDetail) {
|
||||||
|
document.title = `${(appDetail.name || 'App')} - Dify`
|
||||||
|
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||||
|
const mode = isMobile ? 'collapse' : 'expand'
|
||||||
|
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
||||||
|
// TODO: consider screen size and mode
|
||||||
|
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
||||||
|
// setAppSiderbarExpand('collapse')
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appDetail, isMobile])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAppDetail()
|
||||||
|
setIsLoadingAppDetail(true)
|
||||||
|
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
|
||||||
|
setAppDetailRes(res)
|
||||||
|
}).catch((e: any) => {
|
||||||
|
if (e.status === 404)
|
||||||
|
router.replace('/apps')
|
||||||
|
}).finally(() => {
|
||||||
|
setIsLoadingAppDetail(false)
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appId, pathname])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!appDetailRes || isLoadingCurrentWorkspace || isLoadingAppDetail)
|
||||||
|
return
|
||||||
|
const res = appDetailRes
|
||||||
|
// redirection
|
||||||
|
const canIEditApp = isCurrentWorkspaceEditor
|
||||||
|
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
|
||||||
|
router.replace(`/app/${appId}/overview`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
|
||||||
|
router.replace(`/app/${appId}/workflow`)
|
||||||
|
}
|
||||||
|
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
|
||||||
|
router.replace(`/app/${appId}/configuration`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setAppDetail({ ...res, enable_sso: false })
|
||||||
|
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
|
||||||
|
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
|
||||||
|
fetchAppSSO({ appId }).then((ssoRes) => {
|
||||||
|
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, systemFeatures.enable_web_sso_switch_component])
|
||||||
|
|
||||||
|
useUnmount(() => {
|
||||||
|
setAppDetail()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!appDetail) {
|
||||||
|
return (
|
||||||
|
<div className='flex h-full items-center justify-center bg-background-body'>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(s.app, 'relative flex', 'overflow-hidden')}>
|
||||||
|
{appDetail && (
|
||||||
|
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
|
||||||
|
)}
|
||||||
|
<div className="grow overflow-hidden bg-components-panel-bg">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(AppDetailLayout)
|
@ -1,177 +1,14 @@
|
|||||||
'use client'
|
import Main from './layout-main'
|
||||||
import type { FC } from 'react'
|
|
||||||
import { useUnmount } from 'ahooks'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { usePathname, useRouter } from 'next/navigation'
|
|
||||||
import {
|
|
||||||
RiDashboard2Fill,
|
|
||||||
RiDashboard2Line,
|
|
||||||
RiFileList3Fill,
|
|
||||||
RiFileList3Line,
|
|
||||||
RiTerminalBoxFill,
|
|
||||||
RiTerminalBoxLine,
|
|
||||||
RiTerminalWindowFill,
|
|
||||||
RiTerminalWindowLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
|
||||||
import { useContextSelector } from 'use-context-selector'
|
|
||||||
import s from './style.module.css'
|
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
import { useStore } from '@/app/components/app/store'
|
|
||||||
import AppSideBar from '@/app/components/app-sidebar'
|
|
||||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
|
||||||
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
|
||||||
import AppContext, { useAppContext } from '@/context/app-context'
|
|
||||||
import Loading from '@/app/components/base/loading'
|
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
|
||||||
import type { App } from '@/types/app'
|
|
||||||
|
|
||||||
export type IAppDetailLayoutProps = {
|
const AppDetailLayout = async (props: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
params: { appId: string }
|
params: Promise<{ appId: string }>
|
||||||
}
|
}) => {
|
||||||
|
|
||||||
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
params: { appId }, // get appId in path
|
params,
|
||||||
} = props
|
} = props
|
||||||
const { t } = useTranslation()
|
|
||||||
const router = useRouter()
|
|
||||||
const pathname = usePathname()
|
|
||||||
const media = useBreakpoints()
|
|
||||||
const isMobile = media === MediaType.mobile
|
|
||||||
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
|
|
||||||
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
|
|
||||||
appDetail: state.appDetail,
|
|
||||||
setAppDetail: state.setAppDetail,
|
|
||||||
setAppSiderbarExpand: state.setAppSiderbarExpand,
|
|
||||||
})))
|
|
||||||
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
|
|
||||||
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
|
|
||||||
const [navigation, setNavigation] = useState<Array<{
|
|
||||||
name: string
|
|
||||||
href: string
|
|
||||||
icon: NavIcon
|
|
||||||
selectedIcon: NavIcon
|
|
||||||
}>>([])
|
|
||||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
|
||||||
|
|
||||||
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
|
return <Main appId={(await params).appId}>{children}</Main>
|
||||||
const navs = [
|
|
||||||
...(isCurrentWorkspaceEditor
|
|
||||||
? [{
|
|
||||||
name: t('common.appMenus.promptEng'),
|
|
||||||
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
|
|
||||||
icon: RiTerminalWindowLine,
|
|
||||||
selectedIcon: RiTerminalWindowFill,
|
|
||||||
}]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
{
|
|
||||||
name: t('common.appMenus.apiAccess'),
|
|
||||||
href: `/app/${appId}/develop`,
|
|
||||||
icon: RiTerminalBoxLine,
|
|
||||||
selectedIcon: RiTerminalBoxFill,
|
|
||||||
},
|
|
||||||
...(isCurrentWorkspaceEditor
|
|
||||||
? [{
|
|
||||||
name: mode !== 'workflow'
|
|
||||||
? t('common.appMenus.logAndAnn')
|
|
||||||
: t('common.appMenus.logs'),
|
|
||||||
href: `/app/${appId}/logs`,
|
|
||||||
icon: RiFileList3Line,
|
|
||||||
selectedIcon: RiFileList3Fill,
|
|
||||||
}]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
{
|
|
||||||
name: t('common.appMenus.overview'),
|
|
||||||
href: `/app/${appId}/overview`,
|
|
||||||
icon: RiDashboard2Line,
|
|
||||||
selectedIcon: RiDashboard2Fill,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return navs
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (appDetail) {
|
|
||||||
document.title = `${(appDetail.name || 'App')} - Dify`
|
|
||||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
|
||||||
const mode = isMobile ? 'collapse' : 'expand'
|
|
||||||
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
|
||||||
// TODO: consider screen size and mode
|
|
||||||
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
|
||||||
// setAppSiderbarExpand('collapse')
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [appDetail, isMobile])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAppDetail()
|
|
||||||
setIsLoadingAppDetail(true)
|
|
||||||
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
|
|
||||||
setAppDetailRes(res)
|
|
||||||
}).catch((e: any) => {
|
|
||||||
if (e.status === 404)
|
|
||||||
router.replace('/apps')
|
|
||||||
}).finally(() => {
|
|
||||||
setIsLoadingAppDetail(false)
|
|
||||||
})
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [appId, pathname])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!appDetailRes || isLoadingCurrentWorkspace || isLoadingAppDetail)
|
|
||||||
return
|
|
||||||
const res = appDetailRes
|
|
||||||
// redirection
|
|
||||||
const canIEditApp = isCurrentWorkspaceEditor
|
|
||||||
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
|
|
||||||
router.replace(`/app/${appId}/overview`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
|
|
||||||
router.replace(`/app/${appId}/workflow`)
|
|
||||||
}
|
|
||||||
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
|
|
||||||
router.replace(`/app/${appId}/configuration`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setAppDetail({ ...res, enable_sso: false })
|
|
||||||
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
|
|
||||||
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
|
|
||||||
fetchAppSSO({ appId }).then((ssoRes) => {
|
|
||||||
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, systemFeatures.enable_web_sso_switch_component])
|
|
||||||
|
|
||||||
useUnmount(() => {
|
|
||||||
setAppDetail()
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!appDetail) {
|
|
||||||
return (
|
|
||||||
<div className='flex h-full items-center justify-center bg-background-body'>
|
|
||||||
<Loading />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn(s.app, 'flex relative', 'overflow-hidden')}>
|
|
||||||
{appDetail && (
|
|
||||||
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
|
|
||||||
)}
|
|
||||||
<div className="bg-components-panel-bg grow overflow-hidden">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
export default React.memo(AppDetailLayout)
|
export default AppDetailLayout
|
||||||
|
@ -122,7 +122,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||||||
return <Loading />
|
return <Loading />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className || 'grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'}>
|
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
|
||||||
<AppCard
|
<AppCard
|
||||||
appInfo={appDetail}
|
appInfo={appDetail}
|
||||||
cardType="webapp"
|
cardType="webapp"
|
||||||
|
@ -46,7 +46,7 @@ export default function ChartView({ appId }: IChartViewProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='flex flex-row items-center mt-8 mb-4 system-xl-semibold text-text-primary'>
|
<div className='system-xl-semibold mb-4 mt-8 flex flex-row items-center text-text-primary'>
|
||||||
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
|
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
|
||||||
<SimpleSelect
|
<SimpleSelect
|
||||||
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
|
items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))}
|
||||||
@ -61,13 +61,13 @@ export default function ChartView({ appId }: IChartViewProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isWorkflow && (
|
{!isWorkflow && (
|
||||||
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
|
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
<ConversationsChart period={period} id={appId} />
|
<ConversationsChart period={period} id={appId} />
|
||||||
<EndUsersChart period={period} id={appId} />
|
<EndUsersChart period={period} id={appId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isWorkflow && (
|
{!isWorkflow && (
|
||||||
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
|
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
{isChatApp
|
{isChatApp
|
||||||
? (
|
? (
|
||||||
<AvgSessionInteractions period={period} id={appId} />
|
<AvgSessionInteractions period={period} id={appId} />
|
||||||
@ -79,24 +79,24 @@ export default function ChartView({ appId }: IChartViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isWorkflow && (
|
{!isWorkflow && (
|
||||||
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
|
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
<UserSatisfactionRate period={period} id={appId} />
|
<UserSatisfactionRate period={period} id={appId} />
|
||||||
<CostChart period={period} id={appId} />
|
<CostChart period={period} id={appId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isWorkflow && isChatApp && (
|
{!isWorkflow && isChatApp && (
|
||||||
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
|
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
<MessagesChart period={period} id={appId} />
|
<MessagesChart period={period} id={appId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isWorkflow && (
|
{isWorkflow && (
|
||||||
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
|
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
<WorkflowMessagesChart period={period} id={appId} />
|
<WorkflowMessagesChart period={period} id={appId} />
|
||||||
<WorkflowDailyTerminalsChart period={period} id={appId} />
|
<WorkflowDailyTerminalsChart period={period} id={appId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isWorkflow && (
|
{isWorkflow && (
|
||||||
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
|
<div className='mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'>
|
||||||
<WorkflowCostChart period={period} id={appId} />
|
<WorkflowCostChart period={period} id={appId} />
|
||||||
<AvgUserInteractions period={period} id={appId} />
|
<AvgUserInteractions period={period} id={appId} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,14 +5,18 @@ import TracingPanel from './tracing/panel'
|
|||||||
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
|
import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
|
||||||
|
|
||||||
export type IDevelopProps = {
|
export type IDevelopProps = {
|
||||||
params: { appId: string }
|
params: Promise<{ appId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Overview = async ({
|
const Overview = async (props: IDevelopProps) => {
|
||||||
params: { appId },
|
const params = await props.params
|
||||||
}: IDevelopProps) => {
|
|
||||||
|
const {
|
||||||
|
appId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full px-4 sm:px-12 py-6 overflow-scroll bg-chatbot-bg">
|
<div className="h-full overflow-scroll bg-chatbot-bg px-4 py-6 sm:px-12">
|
||||||
<ApikeyInfoPanel />
|
<ApikeyInfoPanel />
|
||||||
<TracingPanel />
|
<TracingPanel />
|
||||||
<CardView appId={appId} />
|
<CardView appId={appId} />
|
||||||
|
@ -58,8 +58,8 @@ const ConfigBtn: FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||||
<div className={cn(className, 'p-1 rounded-md')}>
|
<div className={cn(className, 'rounded-md p-1')}>
|
||||||
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
|
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemTrigger>
|
</PortalToFollowElemTrigger>
|
||||||
<PortalToFollowElemContent className='z-[11]'>
|
<PortalToFollowElemContent className='z-[11]'>
|
||||||
|
@ -162,15 +162,15 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-[420px] p-4 rounded-2xl bg-components-panel-bg border-[0.5px] border-components-panel-border shadow-xl'>
|
<div className='w-[420px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-xl'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex items-center justify-between'>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<TracingIcon size='md' className='mr-2' />
|
<TracingIcon size='md' className='mr-2' />
|
||||||
<div className='text-text-primary title-2xl-semi-bold'>{t(`${I18N_PREFIX}.tracing`)}</div>
|
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.tracing`)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||||
<div className={cn('ml-1 system-xs-semibold-uppercase text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
|
<div className={cn('system-xs-semibold-uppercase ml-1 text-text-tertiary', enabled && 'text-util-colors-green-green-600')}>
|
||||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||||
</div>
|
</div>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
@ -189,7 +189,7 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-2 system-xs-regular text-text-tertiary'>
|
<div className='system-xs-regular mt-2 text-text-tertiary'>
|
||||||
{t(`${I18N_PREFIX}.tracingDescription`)}
|
{t(`${I18N_PREFIX}.tracingDescription`)}
|
||||||
</div>
|
</div>
|
||||||
<Divider className='my-3' />
|
<Divider className='my-3' />
|
||||||
@ -211,7 +211,7 @@ const ConfigPopup: FC<PopupProps> = ({
|
|||||||
<div className='mt-2 space-y-2'>
|
<div className='mt-2 space-y-2'>
|
||||||
{configuredProviderPanel()}
|
{configuredProviderPanel()}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 system-xs-medium-uppercase text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
|
<div className='system-xs-medium-uppercase mt-3 text-text-tertiary'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
|
||||||
<div className='mt-2 space-y-2'>
|
<div className='mt-2 space-y-2'>
|
||||||
{moreProviderPanel()}
|
{moreProviderPanel()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +26,7 @@ const Field: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={cn(className)}>
|
<div className={cn(className)}>
|
||||||
<div className='flex py-[7px]'>
|
<div className='flex py-[7px]'>
|
||||||
<div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-text-primary')}>{label} </div>
|
<div className={cn(labelClassName, 'flex h-[18px] items-center text-[13px] font-medium text-text-primary')}>{label} </div>
|
||||||
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
|
{isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
|
@ -31,7 +31,7 @@ const Title = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center system-xl-semibold text-text-primary', className)}>
|
<div className={cn('system-xl-semibold flex items-center text-text-primary', className)}>
|
||||||
{t('common.appMenus.overview')}
|
{t('common.appMenus.overview')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -143,7 +143,7 @@ const Panel: FC = () => {
|
|||||||
}, [setControlShowPopup])
|
}, [setControlShowPopup])
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-between mb-3'>
|
<div className='mb-3 flex items-center justify-between'>
|
||||||
<Title className='h-[41px]' />
|
<Title className='h-[41px]' />
|
||||||
<div className='w-[200px]'>
|
<div className='w-[200px]'>
|
||||||
<Loading />
|
<Loading />
|
||||||
@ -153,19 +153,19 @@ const Panel: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('mb-3 flex justify-between items-center')}>
|
<div className={cn('mb-3 flex items-center justify-between')}>
|
||||||
<Title className='h-[41px]' />
|
<Title className='h-[41px]' />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center p-2 rounded-xl bg-background-default-dodge border-t border-l-[0.5px] border-effects-highlight shadow-xs cursor-pointer hover:bg-background-default-lighter hover:border-effects-highlight-lightmode-off',
|
'flex cursor-pointer items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
|
||||||
controlShowPopup && 'bg-background-default-lighter border-effects-highlight-lightmode-off',
|
controlShowPopup && 'border-effects-highlight-lightmode-off bg-background-default-lighter',
|
||||||
)}
|
)}
|
||||||
onClick={showPopup}
|
onClick={showPopup}
|
||||||
>
|
>
|
||||||
{!inUseTracingProvider && (
|
{!inUseTracingProvider && (
|
||||||
<>
|
<>
|
||||||
<TracingIcon size='md' />
|
<TracingIcon size='md' />
|
||||||
<div className='mx-2 system-sm-semibold text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
|
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
|
||||||
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
||||||
<ConfigButton
|
<ConfigButton
|
||||||
appId={appId}
|
appId={appId}
|
||||||
@ -184,8 +184,8 @@ const Panel: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Divider type='vertical' className='h-3.5' />
|
<Divider type='vertical' className='h-3.5' />
|
||||||
<div className='p-1 rounded-md'>
|
<div className='rounded-md p-1'>
|
||||||
<RiArrowDownDoubleLine className='w-4 h-4 text-text-tertiary' />
|
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -193,7 +193,7 @@ const Panel: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className='ml-4 mr-1 flex items-center'>
|
<div className='ml-4 mr-1 flex items-center'>
|
||||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||||
<div className='ml-1.5 system-xs-semibold-uppercase text-text-tertiary'>
|
<div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'>
|
||||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -167,11 +167,11 @@ const ProviderConfigModal: FC<Props> = ({
|
|||||||
{!isShowRemoveConfirm
|
{!isShowRemoveConfirm
|
||||||
? (
|
? (
|
||||||
<PortalToFollowElem open>
|
<PortalToFollowElem open>
|
||||||
<PortalToFollowElemContent className='w-full h-full z-[60]'>
|
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||||
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
|
<div className='fixed inset-0 flex items-center justify-center bg-background-overlay'>
|
||||||
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-components-panel-bg shadow-xl rounded-2xl overflow-y-auto'>
|
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-components-panel-bg shadow-xl'>
|
||||||
<div className='px-8 pt-8'>
|
<div className='px-8 pt-8'>
|
||||||
<div className='flex justify-between items-center mb-4'>
|
<div className='mb-4 flex items-center justify-between'>
|
||||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
|
<div className='title-2xl-semi-bold text-text-primary'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -265,14 +265,14 @@ const ProviderConfigModal: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className='my-8 flex justify-between items-center h-8'>
|
<div className='my-8 flex h-8 items-center justify-between'>
|
||||||
<a
|
<a
|
||||||
className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]'
|
className='flex items-center space-x-1 text-xs font-normal leading-[18px] text-[#155EEF]'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
href={docURL[type]}
|
href={docURL[type]}
|
||||||
>
|
>
|
||||||
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
|
<span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
|
||||||
<LinkExternal02 className='w-3 h-3' />
|
<LinkExternal02 className='h-3 w-3' />
|
||||||
</a>
|
</a>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
{isEdit && (
|
{isEdit && (
|
||||||
@ -305,11 +305,11 @@ const ProviderConfigModal: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='border-t-[0.5px] border-divider-regular'>
|
<div className='border-t-[0.5px] border-divider-regular'>
|
||||||
<div className='flex justify-center items-center py-3 bg-background-section-burn text-xs text-text-tertiary'>
|
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||||
<Lock01 className='mr-1 w-3 h-3 text-text-tertiary' />
|
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||||
{t('common.modelProvider.encrypted.front')}
|
{t('common.modelProvider.encrypted.front')}
|
||||||
<a
|
<a
|
||||||
className='text-primary-600 mx-1'
|
className='mx-1 text-primary-600'
|
||||||
target='_blank' rel='noopener noreferrer'
|
target='_blank' rel='noopener noreferrer'
|
||||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||||
>
|
>
|
||||||
|
@ -65,36 +65,36 @@ const ProviderPanel: FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-4 py-3 rounded-xl border-[1.5px] bg-background-section-burn',
|
'rounded-xl border-[1.5px] bg-background-section-burn px-4 py-3',
|
||||||
isChosen ? 'bg-background-section border-components-option-card-option-selected-border' : 'border-transparent',
|
isChosen ? 'border-components-option-card-option-selected-border bg-background-section' : 'border-transparent',
|
||||||
!isChosen && hasConfigured && !readOnly && 'cursor-pointer',
|
!isChosen && hasConfigured && !readOnly && 'cursor-pointer',
|
||||||
)}
|
)}
|
||||||
onClick={handleChosen}
|
onClick={handleChosen}
|
||||||
>
|
>
|
||||||
<div className={'flex justify-between items-center space-x-1'}>
|
<div className={'flex items-center justify-between space-x-1'}>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<Icon className='h-6' />
|
<Icon className='h-6' />
|
||||||
{isChosen && <div className='ml-1 flex items-center h-4 px-1 rounded-[4px] border border-text-accent-secondary system-2xs-medium-uppercase text-text-accent-secondary'>{t(`${I18N_PREFIX}.inUse`)}</div>}
|
{isChosen && <div className='system-2xs-medium-uppercase ml-1 flex h-4 items-center rounded-[4px] border border-text-accent-secondary px-1 text-text-accent-secondary'>{t(`${I18N_PREFIX}.inUse`)}</div>}
|
||||||
</div>
|
</div>
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<div className={'flex justify-between items-center space-x-1'}>
|
<div className={'flex items-center justify-between space-x-1'}>
|
||||||
{hasConfigured && (
|
{hasConfigured && (
|
||||||
<div className='flex px-2 items-center h-6 bg-components-button-secondary-bg rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs cursor-pointer text-text-secondary space-x-1' onClick={viewBtnClick} >
|
<div className='flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs' onClick={viewBtnClick} >
|
||||||
<View className='w-3 h-3' />
|
<View className='h-3 w-3' />
|
||||||
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
|
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.view`)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className='flex px-2 items-center h-6 bg-components-button-secondary-bg rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs cursor-pointer text-text-secondary space-x-1'
|
className='flex h-6 cursor-pointer items-center space-x-1 rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-text-secondary shadow-xs'
|
||||||
onClick={handleConfigBtnClick}
|
onClick={handleConfigBtnClick}
|
||||||
>
|
>
|
||||||
<RiEqualizer2Line className='w-3 h-3' />
|
<RiEqualizer2Line className='h-3 w-3' />
|
||||||
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
|
<div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2 system-xs-regular text-text-tertiary'>
|
<div className='system-xs-regular mt-2 text-text-tertiary'>
|
||||||
{t(`${I18N_PREFIX}.${type}.description`)}
|
{t(`${I18N_PREFIX}.${type}.description`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@ const TracingIcon: FC<Props> = ({
|
|||||||
const sizeClass = sizeClassMap[size]
|
const sizeClass = sizeClassMap[size]
|
||||||
return (
|
return (
|
||||||
<div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
|
<div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
|
||||||
<Icon className='w-full h-full' />
|
<Icon className='h-full w-full' />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import Workflow from '@/app/components/workflow'
|
|||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full overflow-x-auto'>
|
<div className='h-full w-full overflow-x-auto'>
|
||||||
<Workflow />
|
<Workflow />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -226,37 +226,37 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
||||||
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickSettings}>
|
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickSettings}>
|
||||||
<span className='text-text-secondary system-sm-regular'>{t('app.editApp')}</span>
|
<span className='system-sm-regular text-text-secondary'>{t('app.editApp')}</span>
|
||||||
</button>
|
</button>
|
||||||
<Divider className="!my-1" />
|
<Divider className="!my-1" />
|
||||||
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickDuplicate}>
|
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickDuplicate}>
|
||||||
<span className='text-text-secondary system-sm-regular'>{t('app.duplicate')}</span>
|
<span className='system-sm-regular text-text-secondary'>{t('app.duplicate')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickExport}>
|
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickExport}>
|
||||||
<span className='text-text-secondary system-sm-regular'>{t('app.export')}</span>
|
<span className='system-sm-regular text-text-secondary'>{t('app.export')}</span>
|
||||||
</button>
|
</button>
|
||||||
{(app.mode === 'completion' || app.mode === 'chat') && (
|
{(app.mode === 'completion' || app.mode === 'chat') && (
|
||||||
<>
|
<>
|
||||||
<Divider className="!my-1" />
|
<Divider className="!my-1" />
|
||||||
<div
|
<div
|
||||||
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-state-base-hover rounded-lg cursor-pointer'
|
className='mx-1 flex h-9 cursor-pointer items-center rounded-lg px-3 py-2 hover:bg-state-base-hover'
|
||||||
onClick={onClickSwitch}
|
onClick={onClickSwitch}
|
||||||
>
|
>
|
||||||
<span className='text-text-secondary text-sm leading-5'>{t('app.switch')}</span>
|
<span className='text-sm leading-5 text-text-secondary'>{t('app.switch')}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider className="!my-1" />
|
<Divider className="!my-1" />
|
||||||
<button className='h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-base-hover rounded-lg cursor-pointer' onClick={onClickInstalledApp}>
|
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickInstalledApp}>
|
||||||
<span className='text-text-secondary system-sm-regular'>{t('app.openInExplore')}</span>
|
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
|
||||||
</button>
|
</button>
|
||||||
<Divider className="!my-1" />
|
<Divider className="!my-1" />
|
||||||
<div
|
<div
|
||||||
className='group h-8 w-[calc(100%_-_8px)] py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-state-destructive-hover rounded-lg cursor-pointer'
|
className='group mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
|
||||||
onClick={onClickDelete}
|
onClick={onClickDelete}
|
||||||
>
|
>
|
||||||
<span className='text-text-secondary system-sm-regular group-hover:text-text-destructive'>
|
<span className='system-sm-regular text-text-secondary group-hover:text-text-destructive'>
|
||||||
{t('common.operation.delete')}
|
{t('common.operation.delete')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -276,9 +276,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||||
}}
|
}}
|
||||||
className='relative h-[160px] group col-span-1 bg-components-card-bg border-[1px] border-solid border-components-card-border rounded-xl shadow-sm inline-flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
|
className='group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border-[1px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
|
||||||
>
|
>
|
||||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
|
||||||
<div className='relative shrink-0'>
|
<div className='relative shrink-0'>
|
||||||
<AppIcon
|
<AppIcon
|
||||||
size="large"
|
size="large"
|
||||||
@ -287,13 +287,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
background={app.icon_background}
|
background={app.icon_background}
|
||||||
imageUrl={app.icon_url}
|
imageUrl={app.icon_url}
|
||||||
/>
|
/>
|
||||||
<AppTypeIcon type={app.mode} wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm' className='w-3 h-3' />
|
<AppTypeIcon type={app.mode} wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm' className='h-3 w-3' />
|
||||||
</div>
|
</div>
|
||||||
<div className='grow w-0 py-[1px]'>
|
<div className='w-0 grow py-[1px]'>
|
||||||
<div className='flex items-center text-sm leading-5 font-semibold text-text-secondary'>
|
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
|
||||||
<div className='truncate' title={app.name}>{app.name}</div>
|
<div className='truncate' title={app.name}>{app.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center text-[10px] leading-[18px] text-text-tertiary font-medium'>
|
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'>
|
||||||
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
|
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
|
||||||
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
|
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
|
||||||
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
|
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
|
||||||
@ -311,17 +311,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'absolute bottom-1 left-0 right-0 items-center shrink-0 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
|
'absolute bottom-1 left-0 right-0 h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1',
|
||||||
tags.length ? 'flex' : '!hidden group-hover:!flex',
|
tags.length ? 'flex' : '!hidden group-hover:!flex',
|
||||||
)}>
|
)}>
|
||||||
{isCurrentWorkspaceEditor && (
|
{isCurrentWorkspaceEditor && (
|
||||||
<>
|
<>
|
||||||
<div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
|
<div className={cn('flex w-0 grow items-center gap-1')} onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}}>
|
}}>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
|
'mr-[41px] w-full grow group-hover:!mr-0 group-hover:!block',
|
||||||
tags.length ? '!block' : '!hidden',
|
tags.length ? '!block' : '!hidden',
|
||||||
)}>
|
)}>
|
||||||
<TagSelector
|
<TagSelector
|
||||||
@ -335,23 +335,23 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px]' />
|
<div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 group-hover:!flex' />
|
||||||
<div className='!hidden group-hover:!flex shrink-0'>
|
<div className='!hidden shrink-0 group-hover:!flex'>
|
||||||
<CustomPopover
|
<CustomPopover
|
||||||
htmlContent={<Operations />}
|
htmlContent={<Operations />}
|
||||||
position="br"
|
position="br"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
btnElement={
|
btnElement={
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-center w-8 h-8 cursor-pointer rounded-md'
|
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
|
||||||
>
|
>
|
||||||
<RiMoreFill className='w-4 h-4 text-text-tertiary' />
|
<RiMoreFill className='h-4 w-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
btnClassName={open =>
|
btnClassName={open =>
|
||||||
cn(
|
cn(
|
||||||
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
|
||||||
'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5',
|
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
popupClassName={
|
popupClassName={
|
||||||
@ -359,7 +359,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
? '!w-[256px] translate-x-[-224px]'
|
? '!w-[256px] translate-x-[-224px]'
|
||||||
: '!w-[160px] translate-x-[-128px]'
|
: '!w-[160px] translate-x-[-128px]'
|
||||||
}
|
}
|
||||||
className={'h-fit !z-20'}
|
className={'!z-20 h-fit'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -8,6 +8,7 @@ import { useDebounceFn } from 'ahooks'
|
|||||||
import {
|
import {
|
||||||
RiApps2Line,
|
RiApps2Line,
|
||||||
RiExchange2Line,
|
RiExchange2Line,
|
||||||
|
RiFile4Line,
|
||||||
RiMessage3Line,
|
RiMessage3Line,
|
||||||
RiRobot3Line,
|
RiRobot3Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
@ -78,10 +79,12 @@ const Apps = () => {
|
|||||||
|
|
||||||
const anchorRef = useRef<HTMLDivElement>(null)
|
const anchorRef = useRef<HTMLDivElement>(null)
|
||||||
const options = [
|
const options = [
|
||||||
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='w-[14px] h-[14px] mr-1' /> },
|
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||||
{ value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='w-[14px] h-[14px] mr-1' /> },
|
{ value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||||
{ value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='w-[14px] h-[14px] mr-1' /> },
|
{ value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||||
{ value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='w-[14px] h-[14px] mr-1' /> },
|
{ value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||||
|
{ value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||||
|
{ value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -134,7 +137,7 @@ const Apps = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
|
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
|
||||||
<TabSliderNew
|
<TabSliderNew
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
@ -159,14 +162,14 @@ const Apps = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(data && data[0].total > 0)
|
{(data && data[0].total > 0)
|
||||||
? <div className='grid content-start grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6 gap-4 px-12 pt-2 grow relative'>
|
? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
|
||||||
{isCurrentWorkspaceEditor
|
{isCurrentWorkspaceEditor
|
||||||
&& <NewAppCard onSuccess={mutate} />}
|
&& <NewAppCard onSuccess={mutate} />}
|
||||||
{data.map(({ data: apps }) => apps.map(app => (
|
{data.map(({ data: apps }) => apps.map(app => (
|
||||||
<AppCard key={app.id} app={app} onRefresh={mutate} />
|
<AppCard key={app.id} app={app} onRefresh={mutate} />
|
||||||
)))}
|
)))}
|
||||||
</div>
|
</div>
|
||||||
: <div className='grid content-start grid-cols-1 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6 gap-4 px-12 pt-2 grow relative overflow-hidden'>
|
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
|
||||||
{isCurrentWorkspaceEditor
|
{isCurrentWorkspaceEditor
|
||||||
&& <NewAppCard className='z-10' onSuccess={mutate} />}
|
&& <NewAppCard className='z-10' onSuccess={mutate} />}
|
||||||
<NoAppsFound />
|
<NoAppsFound />
|
||||||
@ -186,14 +189,14 @@ function NoAppsFound() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
function renderDefaultCard() {
|
function renderDefaultCard() {
|
||||||
const defaultCards = Array.from({ length: 36 }, (_, index) => (
|
const defaultCards = Array.from({ length: 36 }, (_, index) => (
|
||||||
<div key={index} className='h-[160px] inline-flex rounded-xl bg-background-default-lighter'></div>
|
<div key={index} className='inline-flex h-[160px] rounded-xl bg-background-default-lighter'></div>
|
||||||
))
|
))
|
||||||
return defaultCards
|
return defaultCards
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderDefaultCard()}
|
{renderDefaultCard()}
|
||||||
<div className='absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
|
<div className='absolute bottom-0 left-0 right-0 top-0 flex items-center justify-center bg-gradient-to-t from-background-body to-transparent'>
|
||||||
<span className='system-md-medium text-text-tertiary'>{t('app.newApp.noAppsFound')}</span>
|
<span className='system-md-medium text-text-tertiary'>{t('app.newApp.noAppsFound')}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { forwardRef, useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
useRouter,
|
useRouter,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
@ -18,7 +18,15 @@ export type CreateAppCardProps = {
|
|||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateAppCard = forwardRef<HTMLDivElement, CreateAppCardProps>(({ className, onSuccess }, ref) => {
|
const CreateAppCard = (
|
||||||
|
{
|
||||||
|
ref,
|
||||||
|
className,
|
||||||
|
onSuccess,
|
||||||
|
}: CreateAppCardProps & {
|
||||||
|
ref: React.RefObject<HTMLDivElement>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { onPlanInfoChanged } = useProviderContext()
|
const { onPlanInfoChanged } = useProviderContext()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
@ -39,22 +47,22 @@ const CreateAppCard = forwardRef<HTMLDivElement, CreateAppCardProps>(({ classNam
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('relative col-span-1 inline-flex flex-col justify-between h-[160px] bg-components-card-bg rounded-xl border-[0.5px] border-components-card-border', className)}
|
className={cn('relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg', className)}
|
||||||
>
|
>
|
||||||
<div className='grow p-2 rounded-t-xl'>
|
<div className='grow rounded-t-xl p-2'>
|
||||||
<div className='px-6 pt-2 pb-1 text-xs font-medium leading-[18px] text-text-tertiary'>{t('app.createApp')}</div>
|
<div className='px-6 pb-1 pt-2 text-xs font-medium leading-[18px] text-text-tertiary'>{t('app.createApp')}</div>
|
||||||
<button className='w-full flex items-center mb-1 px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-text-tertiary cursor-pointer hover:text-text-secondary hover:bg-state-base-hover' onClick={() => setShowNewAppModal(true)}>
|
<button className='mb-1 flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' onClick={() => setShowNewAppModal(true)}>
|
||||||
<FilePlus01 className='shrink-0 mr-2 w-4 h-4' />
|
<FilePlus01 className='mr-2 h-4 w-4 shrink-0' />
|
||||||
{t('app.newApp.startFromBlank')}
|
{t('app.newApp.startFromBlank')}
|
||||||
</button>
|
</button>
|
||||||
<button className='w-full flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-text-tertiary cursor-pointer hover:text-text-secondary hover:bg-state-base-hover' onClick={() => setShowNewAppTemplateDialog(true)}>
|
<button className='flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' onClick={() => setShowNewAppTemplateDialog(true)}>
|
||||||
<FilePlus02 className='shrink-0 mr-2 w-4 h-4' />
|
<FilePlus02 className='mr-2 h-4 w-4 shrink-0' />
|
||||||
{t('app.newApp.startFromTemplate')}
|
{t('app.newApp.startFromTemplate')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateFromDSLModal(true)}
|
onClick={() => setShowCreateFromDSLModal(true)}
|
||||||
className='w-full flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-text-tertiary cursor-pointer hover:text-text-secondary hover:bg-state-base-hover'>
|
className='flex w-full cursor-pointer items-center rounded-lg px-6 py-[7px] text-[13px] font-medium leading-[18px] text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'>
|
||||||
<FileArrow01 className='shrink-0 mr-2 w-4 h-4' />
|
<FileArrow01 className='mr-2 h-4 w-4 shrink-0' />
|
||||||
{t('app.importDSL')}
|
{t('app.importDSL')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -103,7 +111,7 @@ const CreateAppCard = forwardRef<HTMLDivElement, CreateAppCardProps>(({ classNam
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
CreateAppCard.displayName = 'CreateAppCard'
|
CreateAppCard.displayName = 'CreateAppCard'
|
||||||
export default CreateAppCard
|
export default CreateAppCard
|
||||||
|
@ -13,17 +13,17 @@ const AppList = () => {
|
|||||||
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
|
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
|
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
||||||
<Apps />
|
<Apps />
|
||||||
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='px-12 py-6 grow-0 shrink-0'>
|
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='shrink-0 grow-0 px-12 py-6'>
|
||||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3>
|
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3>
|
||||||
<p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
|
<p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p>
|
||||||
<div className='flex items-center gap-2 mt-3'>
|
<div className='mt-3 flex items-center gap-2'>
|
||||||
<Link className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://github.com/langgenius/dify'>
|
<Link className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://github.com/langgenius/dify'>
|
||||||
<RiGithubFill className='w-5 h-5 text-text-tertiary' />
|
<RiGithubFill className='h-5 w-5 text-text-tertiary' />
|
||||||
</Link>
|
</Link>
|
||||||
<Link className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://discord.gg/FngNHpbcY7'>
|
<Link className={style.socialMediaLink} target='_blank' rel='noopener noreferrer' href='https://discord.gg/FngNHpbcY7'>
|
||||||
<RiDiscordFill className='w-5 h-5 text-text-tertiary' />
|
<RiDiscordFill className='h-5 w-5 text-text-tertiary' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</footer>}
|
</footer>}
|
||||||
|
@ -2,12 +2,17 @@ import React from 'react'
|
|||||||
import MainDetail from '@/app/components/datasets/documents/detail'
|
import MainDetail from '@/app/components/datasets/documents/detail'
|
||||||
|
|
||||||
export type IDocumentDetailProps = {
|
export type IDocumentDetailProps = {
|
||||||
params: { datasetId: string; documentId: string }
|
params: Promise<{ datasetId: string; documentId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentDetail = async ({
|
const DocumentDetail = async (props: IDocumentDetailProps) => {
|
||||||
params: { datasetId, documentId },
|
const params = await props.params
|
||||||
}: IDocumentDetailProps) => {
|
|
||||||
|
const {
|
||||||
|
datasetId,
|
||||||
|
documentId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainDetail datasetId={datasetId} documentId={documentId} />
|
<MainDetail datasetId={datasetId} documentId={documentId} />
|
||||||
)
|
)
|
||||||
|
@ -2,12 +2,17 @@ import React from 'react'
|
|||||||
import Settings from '@/app/components/datasets/documents/detail/settings'
|
import Settings from '@/app/components/datasets/documents/detail/settings'
|
||||||
|
|
||||||
export type IProps = {
|
export type IProps = {
|
||||||
params: { datasetId: string; documentId: string }
|
params: Promise<{ datasetId: string; documentId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentSettings = async ({
|
const DocumentSettings = async (props: IProps) => {
|
||||||
params: { datasetId, documentId },
|
const params = await props.params
|
||||||
}: IProps) => {
|
|
||||||
|
const {
|
||||||
|
datasetId,
|
||||||
|
documentId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Settings datasetId={datasetId} documentId={documentId} />
|
<Settings datasetId={datasetId} documentId={documentId} />
|
||||||
)
|
)
|
||||||
|
@ -2,12 +2,16 @@ import React from 'react'
|
|||||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||||
|
|
||||||
export type IProps = {
|
export type IProps = {
|
||||||
params: { datasetId: string }
|
params: Promise<{ datasetId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Create = async ({
|
const Create = async (props: IProps) => {
|
||||||
params: { datasetId },
|
const params = await props.params
|
||||||
}: IProps) => {
|
|
||||||
|
const {
|
||||||
|
datasetId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DatasetUpdateForm datasetId={datasetId} />
|
<DatasetUpdateForm datasetId={datasetId} />
|
||||||
)
|
)
|
||||||
|
@ -2,12 +2,16 @@ import React from 'react'
|
|||||||
import Main from '@/app/components/datasets/documents'
|
import Main from '@/app/components/datasets/documents'
|
||||||
|
|
||||||
export type IProps = {
|
export type IProps = {
|
||||||
params: { datasetId: string }
|
params: Promise<{ datasetId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Documents = async ({
|
const Documents = async (props: IProps) => {
|
||||||
params: { datasetId },
|
const params = await props.params
|
||||||
}: IProps) => {
|
|
||||||
|
const {
|
||||||
|
datasetId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Main datasetId={datasetId} />
|
<Main datasetId={datasetId} />
|
||||||
)
|
)
|
||||||
|
@ -2,12 +2,16 @@ import React from 'react'
|
|||||||
import Main from '@/app/components/datasets/hit-testing'
|
import Main from '@/app/components/datasets/hit-testing'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { datasetId: string }
|
params: Promise<{ datasetId: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const HitTesting = ({
|
const HitTesting = async (props: Props) => {
|
||||||
params: { datasetId },
|
const params = await props.params
|
||||||
}: Props) => {
|
|
||||||
|
const {
|
||||||
|
datasetId,
|
||||||
|
} = params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Main datasetId={datasetId} />
|
<Main datasetId={datasetId} />
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,221 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC, SVGProps } from 'react'
|
||||||
|
import React, { useEffect, useMemo } from 'react'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useBoolean } from 'ahooks'
|
||||||
|
import {
|
||||||
|
Cog8ToothIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
PaperClipIcon,
|
||||||
|
} from '@heroicons/react/24/outline'
|
||||||
|
import {
|
||||||
|
Cog8ToothIcon as Cog8ToothSolidIcon,
|
||||||
|
// CommandLineIcon as CommandLineSolidIcon,
|
||||||
|
DocumentTextIcon as DocumentTextSolidIcon,
|
||||||
|
} from '@heroicons/react/24/solid'
|
||||||
|
import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
|
||||||
|
import s from './style.module.css'
|
||||||
|
import classNames from '@/utils/classnames'
|
||||||
|
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
||||||
|
import type { RelatedAppResponse } from '@/models/datasets'
|
||||||
|
import AppSideBar from '@/app/components/app-sidebar'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import DatasetDetailContext from '@/context/dataset-detail'
|
||||||
|
import { DataSourceType } from '@/models/datasets'
|
||||||
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
|
import { LanguagesSupported } from '@/i18n/language'
|
||||||
|
import { useStore } from '@/app/components/app/store'
|
||||||
|
import { getLocaleOnClient } from '@/i18n'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
|
||||||
|
|
||||||
|
export type IAppDetailLayoutProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
params: { datasetId: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||||
|
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||||
|
<g clipPath="url(#clip0_4610_6951)">
|
||||||
|
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_4610_6951">
|
||||||
|
<rect width="16" height="16" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||||
|
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
|
||||||
|
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
|
||||||
|
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
|
type IExtraInfoProps = {
|
||||||
|
isMobile: boolean
|
||||||
|
relatedApps?: RelatedAppResponse
|
||||||
|
expand: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
|
||||||
|
const locale = getLocaleOnClient()
|
||||||
|
const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
|
||||||
|
const relatedAppsTotal = relatedApps?.data?.length || 0
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowTips(!isMobile)
|
||||||
|
}, [isMobile, setShowTips])
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
{hasRelatedApps && (
|
||||||
|
<>
|
||||||
|
{!isMobile && (
|
||||||
|
<Tooltip
|
||||||
|
position='right'
|
||||||
|
noDecoration
|
||||||
|
needsDelay
|
||||||
|
popupContent={
|
||||||
|
<LinkedAppsPanel
|
||||||
|
relatedApps={relatedApps.data}
|
||||||
|
isMobile={isMobile}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='system-xs-medium-uppercase inline-flex cursor-pointer items-center space-x-1 text-text-secondary'>
|
||||||
|
<span>{relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')}</span>
|
||||||
|
<RiInformation2Line className='h-4 w-4' />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
|
||||||
|
{relatedAppsTotal || '--'}
|
||||||
|
<PaperClipIcon className='h-4 w-4 text-gray-700' />
|
||||||
|
</div>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!hasRelatedApps && !expand && (
|
||||||
|
<Tooltip
|
||||||
|
position='right'
|
||||||
|
noDecoration
|
||||||
|
needsDelay
|
||||||
|
popupContent={
|
||||||
|
<div className='w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-4'>
|
||||||
|
<div className='inline-flex rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle p-2'>
|
||||||
|
<RiApps2AddLine className='h-4 w-4 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
<div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div>
|
||||||
|
<a
|
||||||
|
className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent'
|
||||||
|
href={
|
||||||
|
locale === LanguagesSupported[1]
|
||||||
|
? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
|
||||||
|
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
|
||||||
|
}
|
||||||
|
target='_blank' rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<RiBookOpenLine className='mr-1 text-text-accent' />
|
||||||
|
{t('common.datasetMenus.viewDoc')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='system-xs-medium-uppercase inline-flex cursor-pointer items-center space-x-1 text-text-secondary'>
|
||||||
|
<span>{t('common.datasetMenus.noRelatedApp')}</span>
|
||||||
|
<RiInformation2Line className='h-4 w-4' />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
params: { datasetId },
|
||||||
|
} = props
|
||||||
|
const pathname = usePathname()
|
||||||
|
const hideSideBar = /documents\/create$/.test(pathname)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||||
|
|
||||||
|
const media = useBreakpoints()
|
||||||
|
const isMobile = media === MediaType.mobile
|
||||||
|
|
||||||
|
const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
|
||||||
|
url: 'fetchDatasetDetail',
|
||||||
|
datasetId,
|
||||||
|
}, apiParams => fetchDatasetDetail(apiParams.datasetId))
|
||||||
|
|
||||||
|
const { data: relatedApps } = useSWR({
|
||||||
|
action: 'fetchDatasetRelatedApps',
|
||||||
|
datasetId,
|
||||||
|
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
|
||||||
|
|
||||||
|
const navigation = useMemo(() => {
|
||||||
|
const baseNavigation = [
|
||||||
|
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
|
||||||
|
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||||
|
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||||
|
]
|
||||||
|
|
||||||
|
if (datasetRes?.provider !== 'external') {
|
||||||
|
baseNavigation.unshift({
|
||||||
|
name: t('common.datasetMenus.documents'),
|
||||||
|
href: `/datasets/${datasetId}/documents`,
|
||||||
|
icon: DocumentTextIcon,
|
||||||
|
selectedIcon: DocumentTextSolidIcon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return baseNavigation
|
||||||
|
}, [datasetRes?.provider, datasetId, t])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (datasetRes)
|
||||||
|
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
||||||
|
}, [datasetRes])
|
||||||
|
|
||||||
|
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||||
|
const mode = isMobile ? 'collapse' : 'expand'
|
||||||
|
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
||||||
|
}, [isMobile, setAppSiderbarExpand])
|
||||||
|
|
||||||
|
if (!datasetRes && !error)
|
||||||
|
return <Loading type='app' />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex grow overflow-hidden'>
|
||||||
|
{!hideSideBar && <AppSideBar
|
||||||
|
title={datasetRes?.name || '--'}
|
||||||
|
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
|
||||||
|
icon_background={datasetRes?.icon_background || '#F5F5F5'}
|
||||||
|
desc={datasetRes?.description || '--'}
|
||||||
|
isExternal={datasetRes?.provider === 'external'}
|
||||||
|
navigation={navigation}
|
||||||
|
extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} expand={mode === 'collapse'} /> : undefined}
|
||||||
|
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
|
||||||
|
/>}
|
||||||
|
<DatasetDetailContext.Provider value={{
|
||||||
|
indexingTechnique: datasetRes?.indexing_technique,
|
||||||
|
dataset: datasetRes,
|
||||||
|
mutateDatasetRes: () => mutateDatasetRes(),
|
||||||
|
}}>
|
||||||
|
<div className="grow overflow-hidden bg-background-default-subtle">{children}</div>
|
||||||
|
</DatasetDetailContext.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(DatasetDetailLayout)
|
@ -1,221 +1,17 @@
|
|||||||
'use client'
|
import Main from './layout-main'
|
||||||
import type { FC, SVGProps } from 'react'
|
|
||||||
import React, { useEffect, useMemo } from 'react'
|
|
||||||
import { usePathname } from 'next/navigation'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useBoolean } from 'ahooks'
|
|
||||||
import {
|
|
||||||
Cog8ToothIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
PaperClipIcon,
|
|
||||||
} from '@heroicons/react/24/outline'
|
|
||||||
import {
|
|
||||||
Cog8ToothIcon as Cog8ToothSolidIcon,
|
|
||||||
// CommandLineIcon as CommandLineSolidIcon,
|
|
||||||
DocumentTextIcon as DocumentTextSolidIcon,
|
|
||||||
} from '@heroicons/react/24/solid'
|
|
||||||
import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
|
|
||||||
import s from './style.module.css'
|
|
||||||
import classNames from '@/utils/classnames'
|
|
||||||
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
|
||||||
import type { RelatedAppResponse } from '@/models/datasets'
|
|
||||||
import AppSideBar from '@/app/components/app-sidebar'
|
|
||||||
import Loading from '@/app/components/base/loading'
|
|
||||||
import DatasetDetailContext from '@/context/dataset-detail'
|
|
||||||
import { DataSourceType } from '@/models/datasets'
|
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
|
||||||
import { LanguagesSupported } from '@/i18n/language'
|
|
||||||
import { useStore } from '@/app/components/app/store'
|
|
||||||
import { getLocaleOnClient } from '@/i18n'
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
|
||||||
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
|
|
||||||
|
|
||||||
export type IAppDetailLayoutProps = {
|
const DatasetDetailLayout = async (
|
||||||
children: React.ReactNode
|
props: {
|
||||||
params: { datasetId: string }
|
children: React.ReactNode
|
||||||
}
|
params: Promise<{ datasetId: string }>
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const params = await props.params
|
||||||
|
|
||||||
const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
|
|
||||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
|
||||||
<g clipPath="url(#clip0_4610_6951)">
|
|
||||||
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_4610_6951">
|
|
||||||
<rect width="16" height="16" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
|
|
||||||
const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
|
|
||||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
|
|
||||||
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
|
|
||||||
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
|
|
||||||
type IExtraInfoProps = {
|
|
||||||
isMobile: boolean
|
|
||||||
relatedApps?: RelatedAppResponse
|
|
||||||
expand: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
|
|
||||||
const locale = getLocaleOnClient()
|
|
||||||
const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
|
|
||||||
const relatedAppsTotal = relatedApps?.data?.length || 0
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setShowTips(!isMobile)
|
|
||||||
}, [isMobile, setShowTips])
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
{hasRelatedApps && (
|
|
||||||
<>
|
|
||||||
{!isMobile && (
|
|
||||||
<Tooltip
|
|
||||||
position='right'
|
|
||||||
noDecoration
|
|
||||||
needsDelay
|
|
||||||
popupContent={
|
|
||||||
<LinkedAppsPanel
|
|
||||||
relatedApps={relatedApps.data}
|
|
||||||
isMobile={isMobile}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className='inline-flex items-center system-xs-medium-uppercase text-text-secondary space-x-1 cursor-pointer'>
|
|
||||||
<span>{relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')}</span>
|
|
||||||
<RiInformation2Line className='w-4 h-4' />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
|
|
||||||
{relatedAppsTotal || '--'}
|
|
||||||
<PaperClipIcon className='h-4 w-4 text-gray-700' />
|
|
||||||
</div>}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!hasRelatedApps && !expand && (
|
|
||||||
<Tooltip
|
|
||||||
position='right'
|
|
||||||
noDecoration
|
|
||||||
needsDelay
|
|
||||||
popupContent={
|
|
||||||
<div className='p-4 w-[240px] bg-components-panel-bg-blur border-[0.5px] border-components-panel-border rounded-xl'>
|
|
||||||
<div className='inline-flex p-2 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle'>
|
|
||||||
<RiApps2AddLine className='h-4 w-4 text-text-tertiary' />
|
|
||||||
</div>
|
|
||||||
<div className='text-xs text-text-tertiary my-2'>{t('common.datasetMenus.emptyTip')}</div>
|
|
||||||
<a
|
|
||||||
className='inline-flex items-center text-xs text-text-accent mt-2 cursor-pointer'
|
|
||||||
href={
|
|
||||||
locale === LanguagesSupported[1]
|
|
||||||
? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
|
|
||||||
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
|
|
||||||
}
|
|
||||||
target='_blank' rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
<RiBookOpenLine className='mr-1 text-text-accent' />
|
|
||||||
{t('common.datasetMenus.viewDoc')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className='inline-flex items-center system-xs-medium-uppercase text-text-secondary space-x-1 cursor-pointer'>
|
|
||||||
<span>{t('common.datasetMenus.noRelatedApp')}</span>
|
|
||||||
<RiInformation2Line className='w-4 h-4' />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
params: { datasetId },
|
|
||||||
} = props
|
} = props
|
||||||
const pathname = usePathname()
|
|
||||||
const hideSideBar = /documents\/create$/.test(pathname)
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
|
||||||
|
|
||||||
const media = useBreakpoints()
|
return <Main params={(await params)}>{children}</Main>
|
||||||
const isMobile = media === MediaType.mobile
|
|
||||||
|
|
||||||
const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
|
|
||||||
url: 'fetchDatasetDetail',
|
|
||||||
datasetId,
|
|
||||||
}, apiParams => fetchDatasetDetail(apiParams.datasetId))
|
|
||||||
|
|
||||||
const { data: relatedApps } = useSWR({
|
|
||||||
action: 'fetchDatasetRelatedApps',
|
|
||||||
datasetId,
|
|
||||||
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
|
|
||||||
|
|
||||||
const navigation = useMemo(() => {
|
|
||||||
const baseNavigation = [
|
|
||||||
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
|
|
||||||
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
|
||||||
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
|
||||||
]
|
|
||||||
|
|
||||||
if (datasetRes?.provider !== 'external') {
|
|
||||||
baseNavigation.unshift({
|
|
||||||
name: t('common.datasetMenus.documents'),
|
|
||||||
href: `/datasets/${datasetId}/documents`,
|
|
||||||
icon: DocumentTextIcon,
|
|
||||||
selectedIcon: DocumentTextSolidIcon,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return baseNavigation
|
|
||||||
}, [datasetRes?.provider, datasetId, t])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (datasetRes)
|
|
||||||
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
|
||||||
}, [datasetRes])
|
|
||||||
|
|
||||||
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
|
||||||
const mode = isMobile ? 'collapse' : 'expand'
|
|
||||||
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
|
||||||
}, [isMobile, setAppSiderbarExpand])
|
|
||||||
|
|
||||||
if (!datasetRes && !error)
|
|
||||||
return <Loading type='app' />
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='grow flex overflow-hidden'>
|
|
||||||
{!hideSideBar && <AppSideBar
|
|
||||||
title={datasetRes?.name || '--'}
|
|
||||||
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
|
|
||||||
icon_background={datasetRes?.icon_background || '#F5F5F5'}
|
|
||||||
desc={datasetRes?.description || '--'}
|
|
||||||
isExternal={datasetRes?.provider === 'external'}
|
|
||||||
navigation={navigation}
|
|
||||||
extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} expand={mode === 'collapse'} /> : undefined}
|
|
||||||
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
|
|
||||||
/>}
|
|
||||||
<DatasetDetailContext.Provider value={{
|
|
||||||
indexingTechnique: datasetRes?.indexing_technique,
|
|
||||||
dataset: datasetRes,
|
|
||||||
mutateDatasetRes: () => mutateDatasetRes(),
|
|
||||||
}}>
|
|
||||||
<div className="bg-background-default-subtle grow overflow-hidden">{children}</div>
|
|
||||||
</DatasetDetailContext.Provider>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
export default React.memo(DatasetDetailLayout)
|
export default DatasetDetailLayout
|
||||||
|
@ -3,13 +3,13 @@ import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
|
|||||||
import Form from '@/app/components/datasets/settings/form'
|
import Form from '@/app/components/datasets/settings/form'
|
||||||
|
|
||||||
const Settings = async () => {
|
const Settings = async () => {
|
||||||
const locale = getLocaleOnServer()
|
const locale = await getLocaleOnServer()
|
||||||
const { t } = await translate(locale, 'dataset-settings')
|
const { t } = await translate(locale, 'dataset-settings')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full overflow-y-auto'>
|
<div className='h-full overflow-y-auto'>
|
||||||
<div className='px-6 py-3'>
|
<div className='px-6 py-3'>
|
||||||
<div className='mb-1 system-xl-semibold text-text-primary'>{t('title')}</div>
|
<div className='system-xl-semibold mb-1 text-text-primary'>{t('title')}</div>
|
||||||
<div className='system-sm-regular text-text-tertiary'>{t('desc')}</div>
|
<div className='system-sm-regular text-text-tertiary'>{t('desc')}</div>
|
||||||
</div>
|
</div>
|
||||||
<Form />
|
<Form />
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user