Upload 14 files
Browse files- api/.dockerignore +14 -0
- api/.env.example +433 -0
- api/.ruff.toml +97 -0
- api/Dockerfile +84 -0
- api/README.md +89 -0
- api/app.py +41 -0
- api/app_factory.py +101 -0
- api/commands.py +651 -0
- api/dify_app.py +5 -0
- api/mypy.ini +10 -0
- api/poetry.lock +0 -0
- api/poetry.toml +4 -0
- api/pyproject.toml +209 -0
- api/pytest.ini +31 -0
api/.dockerignore
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.env
|
2 |
+
*.env.*
|
3 |
+
|
4 |
+
storage/privkeys/*
|
5 |
+
|
6 |
+
# Logs
|
7 |
+
logs
|
8 |
+
*.log*
|
9 |
+
|
10 |
+
# jetbrains
|
11 |
+
.idea
|
12 |
+
|
13 |
+
# venv
|
14 |
+
.venv
|
api/.env.example
ADDED
@@ -0,0 +1,433 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Your App secret key will be used for securely signing the session cookie
|
2 |
+
# Make sure you are changing this key for your deployment with a strong key.
|
3 |
+
# You can generate a strong key using `openssl rand -base64 42`.
|
4 |
+
# Alternatively you can set it with `SECRET_KEY` environment variable.
|
5 |
+
SECRET_KEY=
|
6 |
+
|
7 |
+
# Console API base URL
|
8 |
+
CONSOLE_API_URL=http://127.0.0.1:5001
|
9 |
+
CONSOLE_WEB_URL=http://127.0.0.1:3000
|
10 |
+
|
11 |
+
# Service API base URL
|
12 |
+
SERVICE_API_URL=http://127.0.0.1:5001
|
13 |
+
|
14 |
+
# Web APP base URL
|
15 |
+
APP_WEB_URL=http://127.0.0.1:3000
|
16 |
+
|
17 |
+
# Files URL
|
18 |
+
FILES_URL=http://127.0.0.1:5001
|
19 |
+
|
20 |
+
# The time in seconds after the signature is rejected
|
21 |
+
FILES_ACCESS_TIMEOUT=300
|
22 |
+
|
23 |
+
# Access token expiration time in minutes
|
24 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
25 |
+
|
26 |
+
# Refresh token expiration time in days
|
27 |
+
REFRESH_TOKEN_EXPIRE_DAYS=30
|
28 |
+
|
29 |
+
# celery configuration
|
30 |
+
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
|
31 |
+
|
32 |
+
# redis configuration
|
33 |
+
REDIS_HOST=localhost
|
34 |
+
REDIS_PORT=6379
|
35 |
+
REDIS_USERNAME=
|
36 |
+
REDIS_PASSWORD=difyai123456
|
37 |
+
REDIS_USE_SSL=false
|
38 |
+
REDIS_DB=0
|
39 |
+
|
40 |
+
# redis Sentinel configuration.
|
41 |
+
REDIS_USE_SENTINEL=false
|
42 |
+
REDIS_SENTINELS=
|
43 |
+
REDIS_SENTINEL_SERVICE_NAME=
|
44 |
+
REDIS_SENTINEL_USERNAME=
|
45 |
+
REDIS_SENTINEL_PASSWORD=
|
46 |
+
REDIS_SENTINEL_SOCKET_TIMEOUT=0.1
|
47 |
+
|
48 |
+
# redis Cluster configuration.
|
49 |
+
REDIS_USE_CLUSTERS=false
|
50 |
+
REDIS_CLUSTERS=
|
51 |
+
REDIS_CLUSTERS_PASSWORD=
|
52 |
+
|
53 |
+
# PostgreSQL database configuration
|
54 |
+
DB_USERNAME=postgres
|
55 |
+
DB_PASSWORD=difyai123456
|
56 |
+
DB_HOST=localhost
|
57 |
+
DB_PORT=5432
|
58 |
+
DB_DATABASE=dify
|
59 |
+
|
60 |
+
# Storage configuration
|
61 |
+
# use for store upload files, private keys...
|
62 |
+
# storage type: opendal, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
|
63 |
+
STORAGE_TYPE=opendal
|
64 |
+
|
65 |
+
# Apache OpenDAL storage configuration, refer to https://github.com/apache/opendal
|
66 |
+
OPENDAL_SCHEME=fs
|
67 |
+
OPENDAL_FS_ROOT=storage
|
68 |
+
|
69 |
+
# S3 Storage configuration
|
70 |
+
S3_USE_AWS_MANAGED_IAM=false
|
71 |
+
S3_ENDPOINT=https://your-bucket-name.storage.s3.cloudflare.com
|
72 |
+
S3_BUCKET_NAME=your-bucket-name
|
73 |
+
S3_ACCESS_KEY=your-access-key
|
74 |
+
S3_SECRET_KEY=your-secret-key
|
75 |
+
S3_REGION=your-region
|
76 |
+
|
77 |
+
# Azure Blob Storage configuration
|
78 |
+
AZURE_BLOB_ACCOUNT_NAME=your-account-name
|
79 |
+
AZURE_BLOB_ACCOUNT_KEY=your-account-key
|
80 |
+
AZURE_BLOB_CONTAINER_NAME=your-container-name
|
81 |
+
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
|
82 |
+
|
83 |
+
# Aliyun oss Storage configuration
|
84 |
+
ALIYUN_OSS_BUCKET_NAME=your-bucket-name
|
85 |
+
ALIYUN_OSS_ACCESS_KEY=your-access-key
|
86 |
+
ALIYUN_OSS_SECRET_KEY=your-secret-key
|
87 |
+
ALIYUN_OSS_ENDPOINT=your-endpoint
|
88 |
+
ALIYUN_OSS_AUTH_VERSION=v1
|
89 |
+
ALIYUN_OSS_REGION=your-region
|
90 |
+
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
91 |
+
ALIYUN_OSS_PATH=your-path
|
92 |
+
|
93 |
+
# Google Storage configuration
|
94 |
+
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
95 |
+
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string
|
96 |
+
|
97 |
+
# Tencent COS Storage configuration
|
98 |
+
TENCENT_COS_BUCKET_NAME=your-bucket-name
|
99 |
+
TENCENT_COS_SECRET_KEY=your-secret-key
|
100 |
+
TENCENT_COS_SECRET_ID=your-secret-id
|
101 |
+
TENCENT_COS_REGION=your-region
|
102 |
+
TENCENT_COS_SCHEME=your-scheme
|
103 |
+
|
104 |
+
# Huawei OBS Storage Configuration
|
105 |
+
HUAWEI_OBS_BUCKET_NAME=your-bucket-name
|
106 |
+
HUAWEI_OBS_SECRET_KEY=your-secret-key
|
107 |
+
HUAWEI_OBS_ACCESS_KEY=your-access-key
|
108 |
+
HUAWEI_OBS_SERVER=your-server-url
|
109 |
+
|
110 |
+
# Baidu OBS Storage Configuration
|
111 |
+
BAIDU_OBS_BUCKET_NAME=your-bucket-name
|
112 |
+
BAIDU_OBS_SECRET_KEY=your-secret-key
|
113 |
+
BAIDU_OBS_ACCESS_KEY=your-access-key
|
114 |
+
BAIDU_OBS_ENDPOINT=your-server-url
|
115 |
+
|
116 |
+
# OCI Storage configuration
|
117 |
+
OCI_ENDPOINT=your-endpoint
|
118 |
+
OCI_BUCKET_NAME=your-bucket-name
|
119 |
+
OCI_ACCESS_KEY=your-access-key
|
120 |
+
OCI_SECRET_KEY=your-secret-key
|
121 |
+
OCI_REGION=your-region
|
122 |
+
|
123 |
+
# Volcengine tos Storage configuration
|
124 |
+
VOLCENGINE_TOS_ENDPOINT=your-endpoint
|
125 |
+
VOLCENGINE_TOS_BUCKET_NAME=your-bucket-name
|
126 |
+
VOLCENGINE_TOS_ACCESS_KEY=your-access-key
|
127 |
+
VOLCENGINE_TOS_SECRET_KEY=your-secret-key
|
128 |
+
VOLCENGINE_TOS_REGION=your-region
|
129 |
+
|
130 |
+
# Supabase Storage Configuration
|
131 |
+
SUPABASE_BUCKET_NAME=your-bucket-name
|
132 |
+
SUPABASE_API_KEY=your-access-key
|
133 |
+
SUPABASE_URL=your-server-url
|
134 |
+
|
135 |
+
# CORS configuration
|
136 |
+
WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
137 |
+
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
|
138 |
+
|
139 |
+
# Vector database configuration
|
140 |
+
# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase
|
141 |
+
VECTOR_STORE=weaviate
|
142 |
+
|
143 |
+
# Weaviate configuration
|
144 |
+
WEAVIATE_ENDPOINT=http://localhost:8080
|
145 |
+
WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
|
146 |
+
WEAVIATE_GRPC_ENABLED=false
|
147 |
+
WEAVIATE_BATCH_SIZE=100
|
148 |
+
|
149 |
+
# Qdrant configuration, use `http://localhost:6333` for local mode or `https://your-qdrant-cluster-url.qdrant.io` for remote mode
|
150 |
+
QDRANT_URL=http://localhost:6333
|
151 |
+
QDRANT_API_KEY=difyai123456
|
152 |
+
QDRANT_CLIENT_TIMEOUT=20
|
153 |
+
QDRANT_GRPC_ENABLED=false
|
154 |
+
QDRANT_GRPC_PORT=6334
|
155 |
+
|
156 |
+
#Couchbase configuration
|
157 |
+
COUCHBASE_CONNECTION_STRING=127.0.0.1
|
158 |
+
COUCHBASE_USER=Administrator
|
159 |
+
COUCHBASE_PASSWORD=password
|
160 |
+
COUCHBASE_BUCKET_NAME=Embeddings
|
161 |
+
COUCHBASE_SCOPE_NAME=_default
|
162 |
+
|
163 |
+
# Milvus configuration
|
164 |
+
MILVUS_URI=http://127.0.0.1:19530
|
165 |
+
MILVUS_TOKEN=
|
166 |
+
MILVUS_USER=root
|
167 |
+
MILVUS_PASSWORD=Milvus
|
168 |
+
|
169 |
+
# MyScale configuration
|
170 |
+
MYSCALE_HOST=127.0.0.1
|
171 |
+
MYSCALE_PORT=8123
|
172 |
+
MYSCALE_USER=default
|
173 |
+
MYSCALE_PASSWORD=
|
174 |
+
MYSCALE_DATABASE=default
|
175 |
+
MYSCALE_FTS_PARAMS=
|
176 |
+
|
177 |
+
# Relyt configuration
|
178 |
+
RELYT_HOST=127.0.0.1
|
179 |
+
RELYT_PORT=5432
|
180 |
+
RELYT_USER=postgres
|
181 |
+
RELYT_PASSWORD=postgres
|
182 |
+
RELYT_DATABASE=postgres
|
183 |
+
|
184 |
+
# Tencent configuration
|
185 |
+
TENCENT_VECTOR_DB_URL=http://127.0.0.1
|
186 |
+
TENCENT_VECTOR_DB_API_KEY=dify
|
187 |
+
TENCENT_VECTOR_DB_TIMEOUT=30
|
188 |
+
TENCENT_VECTOR_DB_USERNAME=dify
|
189 |
+
TENCENT_VECTOR_DB_DATABASE=dify
|
190 |
+
TENCENT_VECTOR_DB_SHARD=1
|
191 |
+
TENCENT_VECTOR_DB_REPLICAS=2
|
192 |
+
|
193 |
+
# ElasticSearch configuration
|
194 |
+
ELASTICSEARCH_HOST=127.0.0.1
|
195 |
+
ELASTICSEARCH_PORT=9200
|
196 |
+
ELASTICSEARCH_USERNAME=elastic
|
197 |
+
ELASTICSEARCH_PASSWORD=elastic
|
198 |
+
|
199 |
+
# PGVECTO_RS configuration
|
200 |
+
PGVECTO_RS_HOST=localhost
|
201 |
+
PGVECTO_RS_PORT=5431
|
202 |
+
PGVECTO_RS_USER=postgres
|
203 |
+
PGVECTO_RS_PASSWORD=difyai123456
|
204 |
+
PGVECTO_RS_DATABASE=postgres
|
205 |
+
|
206 |
+
# PGVector configuration
|
207 |
+
PGVECTOR_HOST=127.0.0.1
|
208 |
+
PGVECTOR_PORT=5433
|
209 |
+
PGVECTOR_USER=postgres
|
210 |
+
PGVECTOR_PASSWORD=postgres
|
211 |
+
PGVECTOR_DATABASE=postgres
|
212 |
+
PGVECTOR_MIN_CONNECTION=1
|
213 |
+
PGVECTOR_MAX_CONNECTION=5
|
214 |
+
|
215 |
+
# Tidb Vector configuration
|
216 |
+
TIDB_VECTOR_HOST=xxx.eu-central-1.xxx.aws.tidbcloud.com
|
217 |
+
TIDB_VECTOR_PORT=4000
|
218 |
+
TIDB_VECTOR_USER=xxx.root
|
219 |
+
TIDB_VECTOR_PASSWORD=xxxxxx
|
220 |
+
TIDB_VECTOR_DATABASE=dify
|
221 |
+
|
222 |
+
# Tidb on qdrant configuration
|
223 |
+
TIDB_ON_QDRANT_URL=http://127.0.0.1
|
224 |
+
TIDB_ON_QDRANT_API_KEY=dify
|
225 |
+
TIDB_ON_QDRANT_CLIENT_TIMEOUT=20
|
226 |
+
TIDB_ON_QDRANT_GRPC_ENABLED=false
|
227 |
+
TIDB_ON_QDRANT_GRPC_PORT=6334
|
228 |
+
TIDB_PUBLIC_KEY=dify
|
229 |
+
TIDB_PRIVATE_KEY=dify
|
230 |
+
TIDB_API_URL=http://127.0.0.1
|
231 |
+
TIDB_IAM_API_URL=http://127.0.0.1
|
232 |
+
TIDB_REGION=regions/aws-us-east-1
|
233 |
+
TIDB_PROJECT_ID=dify
|
234 |
+
TIDB_SPEND_LIMIT=100
|
235 |
+
|
236 |
+
# Chroma configuration
|
237 |
+
CHROMA_HOST=127.0.0.1
|
238 |
+
CHROMA_PORT=8000
|
239 |
+
CHROMA_TENANT=default_tenant
|
240 |
+
CHROMA_DATABASE=default_database
|
241 |
+
CHROMA_AUTH_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider
|
242 |
+
CHROMA_AUTH_CREDENTIALS=difyai123456
|
243 |
+
|
244 |
+
# AnalyticDB configuration
|
245 |
+
ANALYTICDB_KEY_ID=your-ak
|
246 |
+
ANALYTICDB_KEY_SECRET=your-sk
|
247 |
+
ANALYTICDB_REGION_ID=cn-hangzhou
|
248 |
+
ANALYTICDB_INSTANCE_ID=gp-ab123456
|
249 |
+
ANALYTICDB_ACCOUNT=testaccount
|
250 |
+
ANALYTICDB_PASSWORD=testpassword
|
251 |
+
ANALYTICDB_NAMESPACE=dify
|
252 |
+
ANALYTICDB_NAMESPACE_PASSWORD=difypassword
|
253 |
+
ANALYTICDB_HOST=gp-test.aliyuncs.com
|
254 |
+
ANALYTICDB_PORT=5432
|
255 |
+
ANALYTICDB_MIN_CONNECTION=1
|
256 |
+
ANALYTICDB_MAX_CONNECTION=5
|
257 |
+
|
258 |
+
# OpenSearch configuration
|
259 |
+
OPENSEARCH_HOST=127.0.0.1
|
260 |
+
OPENSEARCH_PORT=9200
|
261 |
+
OPENSEARCH_USER=admin
|
262 |
+
OPENSEARCH_PASSWORD=admin
|
263 |
+
OPENSEARCH_SECURE=true
|
264 |
+
|
265 |
+
# Baidu configuration
|
266 |
+
BAIDU_VECTOR_DB_ENDPOINT=http://127.0.0.1:5287
|
267 |
+
BAIDU_VECTOR_DB_CONNECTION_TIMEOUT_MS=30000
|
268 |
+
BAIDU_VECTOR_DB_ACCOUNT=root
|
269 |
+
BAIDU_VECTOR_DB_API_KEY=dify
|
270 |
+
BAIDU_VECTOR_DB_DATABASE=dify
|
271 |
+
BAIDU_VECTOR_DB_SHARD=1
|
272 |
+
BAIDU_VECTOR_DB_REPLICAS=3
|
273 |
+
|
274 |
+
# Upstash configuration
|
275 |
+
UPSTASH_VECTOR_URL=your-server-url
|
276 |
+
UPSTASH_VECTOR_TOKEN=your-access-token
|
277 |
+
|
278 |
+
# ViKingDB configuration
|
279 |
+
VIKINGDB_ACCESS_KEY=your-ak
|
280 |
+
VIKINGDB_SECRET_KEY=your-sk
|
281 |
+
VIKINGDB_REGION=cn-shanghai
|
282 |
+
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
283 |
+
VIKINGDB_SCHEMA=http
|
284 |
+
VIKINGDB_CONNECTION_TIMEOUT=30
|
285 |
+
VIKINGDB_SOCKET_TIMEOUT=30
|
286 |
+
|
287 |
+
# Lindorm configuration
|
288 |
+
LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070
|
289 |
+
LINDORM_USERNAME=admin
|
290 |
+
LINDORM_PASSWORD=admin
|
291 |
+
USING_UGC_INDEX=False
|
292 |
+
|
293 |
+
# OceanBase Vector configuration
|
294 |
+
OCEANBASE_VECTOR_HOST=127.0.0.1
|
295 |
+
OCEANBASE_VECTOR_PORT=2881
|
296 |
+
OCEANBASE_VECTOR_USER=root@test
|
297 |
+
OCEANBASE_VECTOR_PASSWORD=difyai123456
|
298 |
+
OCEANBASE_VECTOR_DATABASE=test
|
299 |
+
OCEANBASE_MEMORY_LIMIT=6G
|
300 |
+
|
301 |
+
|
302 |
+
# Upload configuration
|
303 |
+
UPLOAD_FILE_SIZE_LIMIT=15
|
304 |
+
UPLOAD_FILE_BATCH_LIMIT=5
|
305 |
+
UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
|
306 |
+
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
307 |
+
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
|
308 |
+
|
309 |
+
# Model configuration
|
310 |
+
MULTIMODAL_SEND_FORMAT=base64
|
311 |
+
PROMPT_GENERATION_MAX_TOKENS=512
|
312 |
+
CODE_GENERATION_MAX_TOKENS=1024
|
313 |
+
|
314 |
+
# Mail configuration, support: resend, smtp
|
315 |
+
MAIL_TYPE=
|
316 |
+
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
|
317 |
+
RESEND_API_KEY=
|
318 |
+
RESEND_API_URL=https://api.resend.com
|
319 |
+
# smtp configuration
|
320 |
+
SMTP_SERVER=smtp.gmail.com
|
321 |
+
SMTP_PORT=465
|
322 |
+
SMTP_USERNAME=123
|
323 |
+
SMTP_PASSWORD=abc
|
324 |
+
SMTP_USE_TLS=true
|
325 |
+
SMTP_OPPORTUNISTIC_TLS=false
|
326 |
+
|
327 |
+
# Sentry configuration
|
328 |
+
SENTRY_DSN=
|
329 |
+
|
330 |
+
# DEBUG
|
331 |
+
DEBUG=false
|
332 |
+
SQLALCHEMY_ECHO=false
|
333 |
+
|
334 |
+
# Notion import configuration, support public and internal
|
335 |
+
NOTION_INTEGRATION_TYPE=public
|
336 |
+
NOTION_CLIENT_SECRET=you-client-secret
|
337 |
+
NOTION_CLIENT_ID=you-client-id
|
338 |
+
NOTION_INTERNAL_SECRET=you-internal-secret
|
339 |
+
|
340 |
+
ETL_TYPE=dify
|
341 |
+
UNSTRUCTURED_API_URL=
|
342 |
+
UNSTRUCTURED_API_KEY=
|
343 |
+
SCARF_NO_ANALYTICS=true
|
344 |
+
|
345 |
+
#ssrf
|
346 |
+
SSRF_PROXY_HTTP_URL=
|
347 |
+
SSRF_PROXY_HTTPS_URL=
|
348 |
+
SSRF_DEFAULT_MAX_RETRIES=3
|
349 |
+
SSRF_DEFAULT_TIME_OUT=5
|
350 |
+
SSRF_DEFAULT_CONNECT_TIME_OUT=5
|
351 |
+
SSRF_DEFAULT_READ_TIME_OUT=5
|
352 |
+
SSRF_DEFAULT_WRITE_TIME_OUT=5
|
353 |
+
|
354 |
+
BATCH_UPLOAD_LIMIT=10
|
355 |
+
KEYWORD_DATA_SOURCE_TYPE=database
|
356 |
+
|
357 |
+
# Workflow file upload limit
|
358 |
+
WORKFLOW_FILE_UPLOAD_LIMIT=10
|
359 |
+
|
360 |
+
# CODE EXECUTION CONFIGURATION
|
361 |
+
CODE_EXECUTION_ENDPOINT=http://127.0.0.1:8194
|
362 |
+
CODE_EXECUTION_API_KEY=dify-sandbox
|
363 |
+
CODE_MAX_NUMBER=9223372036854775807
|
364 |
+
CODE_MIN_NUMBER=-9223372036854775808
|
365 |
+
CODE_MAX_STRING_LENGTH=80000
|
366 |
+
TEMPLATE_TRANSFORM_MAX_LENGTH=80000
|
367 |
+
CODE_MAX_STRING_ARRAY_LENGTH=30
|
368 |
+
CODE_MAX_OBJECT_ARRAY_LENGTH=30
|
369 |
+
CODE_MAX_NUMBER_ARRAY_LENGTH=1000
|
370 |
+
|
371 |
+
# API Tool configuration
|
372 |
+
API_TOOL_DEFAULT_CONNECT_TIMEOUT=10
|
373 |
+
API_TOOL_DEFAULT_READ_TIMEOUT=60
|
374 |
+
|
375 |
+
# HTTP Node configuration
|
376 |
+
HTTP_REQUEST_MAX_CONNECT_TIMEOUT=300
|
377 |
+
HTTP_REQUEST_MAX_READ_TIMEOUT=600
|
378 |
+
HTTP_REQUEST_MAX_WRITE_TIMEOUT=600
|
379 |
+
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
380 |
+
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
381 |
+
|
382 |
+
# Respect X-* headers to redirect clients
|
383 |
+
RESPECT_XFORWARD_HEADERS_ENABLED=false
|
384 |
+
|
385 |
+
# Log file path
|
386 |
+
LOG_FILE=
|
387 |
+
# Log file max size, the unit is MB
|
388 |
+
LOG_FILE_MAX_SIZE=20
|
389 |
+
# Log file max backup count
|
390 |
+
LOG_FILE_BACKUP_COUNT=5
|
391 |
+
# Log dateformat
|
392 |
+
LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S
|
393 |
+
# Log Timezone
|
394 |
+
LOG_TZ=UTC
|
395 |
+
# Log format
|
396 |
+
LOG_FORMAT=%(asctime)s,%(msecs)d %(levelname)-2s [%(filename)s:%(lineno)d] %(req_id)s %(message)s
|
397 |
+
|
398 |
+
# Indexing configuration
|
399 |
+
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
400 |
+
|
401 |
+
# Workflow runtime configuration
|
402 |
+
WORKFLOW_MAX_EXECUTION_STEPS=500
|
403 |
+
WORKFLOW_MAX_EXECUTION_TIME=1200
|
404 |
+
WORKFLOW_CALL_MAX_DEPTH=5
|
405 |
+
WORKFLOW_PARALLEL_DEPTH_LIMIT=3
|
406 |
+
MAX_VARIABLE_SIZE=204800
|
407 |
+
|
408 |
+
# App configuration
|
409 |
+
APP_MAX_EXECUTION_TIME=1200
|
410 |
+
APP_MAX_ACTIVE_REQUESTS=0
|
411 |
+
|
412 |
+
|
413 |
+
# Celery beat configuration
|
414 |
+
CELERY_BEAT_SCHEDULER_TIME=1
|
415 |
+
|
416 |
+
# Position configuration
|
417 |
+
POSITION_TOOL_PINS=
|
418 |
+
POSITION_TOOL_INCLUDES=
|
419 |
+
POSITION_TOOL_EXCLUDES=
|
420 |
+
|
421 |
+
POSITION_PROVIDER_PINS=
|
422 |
+
POSITION_PROVIDER_INCLUDES=
|
423 |
+
POSITION_PROVIDER_EXCLUDES=
|
424 |
+
|
425 |
+
# Reset password token expiry minutes
|
426 |
+
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
427 |
+
|
428 |
+
CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
429 |
+
|
430 |
+
# Maximum number of submitted thread count in a ThreadPool for parallel node execution
|
431 |
+
MAX_SUBMIT_COUNT=100
|
432 |
+
# Lockout duration in seconds
|
433 |
+
LOGIN_LOCKOUT_DURATION=86400
|
api/.ruff.toml
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
exclude = [
|
2 |
+
"migrations/*",
|
3 |
+
]
|
4 |
+
line-length = 120
|
5 |
+
|
6 |
+
[format]
|
7 |
+
quote-style = "double"
|
8 |
+
|
9 |
+
[lint]
|
10 |
+
preview = true
|
11 |
+
select = [
|
12 |
+
"B", # flake8-bugbear rules
|
13 |
+
"C4", # flake8-comprehensions
|
14 |
+
"E", # pycodestyle E rules
|
15 |
+
"F", # pyflakes rules
|
16 |
+
"FURB", # refurb rules
|
17 |
+
"I", # isort rules
|
18 |
+
"N", # pep8-naming
|
19 |
+
"PT", # flake8-pytest-style rules
|
20 |
+
"PLC0208", # iteration-over-set
|
21 |
+
"PLC2801", # unnecessary-dunder-call
|
22 |
+
"PLC0414", # useless-import-alias
|
23 |
+
"PLE0604", # invalid-all-object
|
24 |
+
"PLE0605", # invalid-all-format
|
25 |
+
"PLR0402", # manual-from-import
|
26 |
+
"PLR1711", # useless-return
|
27 |
+
"PLR1714", # repeated-equality-comparison
|
28 |
+
"RUF013", # implicit-optional
|
29 |
+
"RUF019", # unnecessary-key-check
|
30 |
+
"RUF100", # unused-noqa
|
31 |
+
"RUF101", # redirected-noqa
|
32 |
+
"RUF200", # invalid-pyproject-toml
|
33 |
+
"RUF022", # unsorted-dunder-all
|
34 |
+
"S506", # unsafe-yaml-load
|
35 |
+
"SIM", # flake8-simplify rules
|
36 |
+
"TRY400", # error-instead-of-exception
|
37 |
+
"TRY401", # verbose-log-message
|
38 |
+
"UP", # pyupgrade rules
|
39 |
+
"W191", # tab-indentation
|
40 |
+
"W605", # invalid-escape-sequence
|
41 |
+
]
|
42 |
+
|
43 |
+
ignore = [
|
44 |
+
"E402", # module-import-not-at-top-of-file
|
45 |
+
"E711", # none-comparison
|
46 |
+
"E712", # true-false-comparison
|
47 |
+
"E721", # type-comparison
|
48 |
+
"E722", # bare-except
|
49 |
+
"E731", # lambda-assignment
|
50 |
+
"F821", # undefined-name
|
51 |
+
"F841", # unused-variable
|
52 |
+
"FURB113", # repeated-append
|
53 |
+
"FURB152", # math-constant
|
54 |
+
"UP007", # non-pep604-annotation
|
55 |
+
"UP032", # f-string
|
56 |
+
"UP045", # non-pep604-annotation-optional
|
57 |
+
"B005", # strip-with-multi-characters
|
58 |
+
"B006", # mutable-argument-default
|
59 |
+
"B007", # unused-loop-control-variable
|
60 |
+
"B026", # star-arg-unpacking-after-keyword-arg
|
61 |
+
"B903", # class-as-data-structure
|
62 |
+
"B904", # raise-without-from-inside-except
|
63 |
+
"B905", # zip-without-explicit-strict
|
64 |
+
"N806", # non-lowercase-variable-in-function
|
65 |
+
"N815", # mixed-case-variable-in-class-scope
|
66 |
+
"PT011", # pytest-raises-too-broad
|
67 |
+
"SIM102", # collapsible-if
|
68 |
+
"SIM103", # needless-bool
|
69 |
+
"SIM105", # suppressible-exception
|
70 |
+
"SIM107", # return-in-try-except-finally
|
71 |
+
"SIM108", # if-else-block-instead-of-if-exp
|
72 |
+
"SIM113", # enumerate-for-loop
|
73 |
+
"SIM117", # multiple-with-statements
|
74 |
+
"SIM210", # if-expr-with-true-false
|
75 |
+
]
|
76 |
+
|
77 |
+
[lint.per-file-ignores]
|
78 |
+
"__init__.py" = [
|
79 |
+
"F401", # unused-import
|
80 |
+
"F811", # redefined-while-unused
|
81 |
+
]
|
82 |
+
"configs/*" = [
|
83 |
+
"N802", # invalid-function-name
|
84 |
+
]
|
85 |
+
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
86 |
+
"N803", # invalid-argument-name
|
87 |
+
]
|
88 |
+
"tests/*" = [
|
89 |
+
"F811", # redefined-while-unused
|
90 |
+
]
|
91 |
+
|
92 |
+
[lint.pyflakes]
|
93 |
+
allowed-unused-imports = [
|
94 |
+
"_pytest.monkeypatch",
|
95 |
+
"tests.integration_tests",
|
96 |
+
"tests.unit_tests",
|
97 |
+
]
|
api/Dockerfile
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# base image
|
2 |
+
FROM python:3.12-slim-bookworm AS base
|
3 |
+
|
4 |
+
WORKDIR /app/api
|
5 |
+
|
6 |
+
# Install Poetry
|
7 |
+
ENV POETRY_VERSION=2.0.1
|
8 |
+
|
9 |
+
# if you located in China, you can use aliyun mirror to speed up
|
10 |
+
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
|
11 |
+
|
12 |
+
RUN pip install --no-cache-dir poetry==${POETRY_VERSION}
|
13 |
+
|
14 |
+
# Configure Poetry
|
15 |
+
ENV POETRY_CACHE_DIR=/tmp/poetry_cache
|
16 |
+
ENV POETRY_NO_INTERACTION=1
|
17 |
+
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
18 |
+
ENV POETRY_VIRTUALENVS_CREATE=true
|
19 |
+
ENV POETRY_REQUESTS_TIMEOUT=15
|
20 |
+
|
21 |
+
FROM base AS packages
|
22 |
+
|
23 |
+
# if you located in China, you can use aliyun mirror to speed up
|
24 |
+
# RUN sed -i 's@deb.debian.org@mirrors.aliyun.com@g' /etc/apt/sources.list.d/debian.sources
|
25 |
+
|
26 |
+
RUN apt-get update \
|
27 |
+
&& apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev
|
28 |
+
|
29 |
+
# Install Python dependencies
|
30 |
+
COPY pyproject.toml poetry.lock ./
|
31 |
+
RUN poetry install --sync --no-cache --no-root
|
32 |
+
|
33 |
+
# production stage
|
34 |
+
FROM base AS production
|
35 |
+
|
36 |
+
ENV FLASK_APP=app.py
|
37 |
+
ENV EDITION=SELF_HOSTED
|
38 |
+
ENV DEPLOY_ENV=PRODUCTION
|
39 |
+
ENV CONSOLE_API_URL=http://127.0.0.1:5001
|
40 |
+
ENV CONSOLE_WEB_URL=http://127.0.0.1:3000
|
41 |
+
ENV SERVICE_API_URL=http://127.0.0.1:5001
|
42 |
+
ENV APP_WEB_URL=http://127.0.0.1:3000
|
43 |
+
|
44 |
+
EXPOSE 5001
|
45 |
+
|
46 |
+
# set timezone
|
47 |
+
ENV TZ=UTC
|
48 |
+
|
49 |
+
WORKDIR /app/api
|
50 |
+
|
51 |
+
RUN \
|
52 |
+
apt-get update \
|
53 |
+
# Install dependencies
|
54 |
+
&& apt-get install -y --no-install-recommends \
|
55 |
+
# basic environment
|
56 |
+
curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
|
57 |
+
# For Security
|
58 |
+
expat libldap-2.5-0 perl libsqlite3-0 zlib1g \
|
59 |
+
# install a chinese font to support the use of tools like matplotlib
|
60 |
+
fonts-noto-cjk \
|
61 |
+
# install libmagic to support the use of python-magic guess MIMETYPE
|
62 |
+
libmagic1 \
|
63 |
+
&& apt-get autoremove -y \
|
64 |
+
&& rm -rf /var/lib/apt/lists/*
|
65 |
+
|
66 |
+
# Copy Python environment and packages
|
67 |
+
ENV VIRTUAL_ENV=/app/api/.venv
|
68 |
+
COPY --from=packages ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
69 |
+
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
70 |
+
|
71 |
+
# Download nltk data
|
72 |
+
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger')"
|
73 |
+
|
74 |
+
# Copy source code
|
75 |
+
COPY . /app/api/
|
76 |
+
|
77 |
+
# Copy entrypoint
|
78 |
+
COPY docker/entrypoint.sh /entrypoint.sh
|
79 |
+
RUN chmod +x /entrypoint.sh
|
80 |
+
|
81 |
+
ARG COMMIT_SHA
|
82 |
+
ENV COMMIT_SHA=${COMMIT_SHA}
|
83 |
+
|
84 |
+
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
|
api/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Dify Backend API
|
2 |
+
|
3 |
+
## Usage
|
4 |
+
|
5 |
+
> [!IMPORTANT]
|
6 |
+
> In the v0.6.12 release, we deprecated `pip` as the package management tool for Dify API Backend service and replaced it with `poetry`.
|
7 |
+
|
8 |
+
1. Start the docker-compose stack
|
9 |
+
|
10 |
+
The backend require some middleware, including PostgreSQL, Redis, and Weaviate, which can be started together using `docker-compose`.
|
11 |
+
|
12 |
+
```bash
|
13 |
+
cd ../docker
|
14 |
+
cp middleware.env.example middleware.env
|
15 |
+
# change the profile to other vector database if you are not using weaviate
|
16 |
+
docker compose -f docker-compose.middleware.yaml --profile weaviate -p dify up -d
|
17 |
+
cd ../api
|
18 |
+
```
|
19 |
+
|
20 |
+
2. Copy `.env.example` to `.env`
|
21 |
+
|
22 |
+
```cli
|
23 |
+
cp .env.example .env
|
24 |
+
```
|
25 |
+
3. Generate a `SECRET_KEY` in the `.env` file.
|
26 |
+
|
27 |
+
bash for Linux
|
28 |
+
```bash for Linux
|
29 |
+
sed -i "/^SECRET_KEY=/c\SECRET_KEY=$(openssl rand -base64 42)" .env
|
30 |
+
```
|
31 |
+
bash for Mac
|
32 |
+
```bash for Mac
|
33 |
+
secret_key=$(openssl rand -base64 42)
|
34 |
+
sed -i '' "/^SECRET_KEY=/c\\
|
35 |
+
SECRET_KEY=${secret_key}" .env
|
36 |
+
```
|
37 |
+
|
38 |
+
4. Create environment.
|
39 |
+
|
40 |
+
Dify API service uses [Poetry](https://python-poetry.org/docs/) to manage dependencies. First, you need to add the poetry shell plugin, if you don't have it already, in order to run in a virtual environment. [Note: Poetry shell is no longer a native command so you need to install the poetry plugin beforehand]
|
41 |
+
|
42 |
+
```bash
|
43 |
+
poetry self add poetry-plugin-shell
|
44 |
+
```
|
45 |
+
|
46 |
+
Then, You can execute `poetry shell` to activate the environment.
|
47 |
+
|
48 |
+
5. Install dependencies
|
49 |
+
|
50 |
+
```bash
|
51 |
+
poetry env use 3.12
|
52 |
+
poetry install
|
53 |
+
```
|
54 |
+
|
55 |
+
6. Run migrate
|
56 |
+
|
57 |
+
Before the first launch, migrate the database to the latest version.
|
58 |
+
|
59 |
+
```bash
|
60 |
+
poetry run python -m flask db upgrade
|
61 |
+
```
|
62 |
+
|
63 |
+
7. Start backend
|
64 |
+
|
65 |
+
```bash
|
66 |
+
poetry run python -m flask run --host 0.0.0.0 --port=5001 --debug
|
67 |
+
```
|
68 |
+
|
69 |
+
8. Start Dify [web](../web) service.
|
70 |
+
9. Setup your application by visiting `http://localhost:3000`...
|
71 |
+
10. If you need to handle and debug the async tasks (e.g. dataset importing and documents indexing), please start the worker service.
|
72 |
+
|
73 |
+
```bash
|
74 |
+
poetry run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion
|
75 |
+
```
|
76 |
+
|
77 |
+
## Testing
|
78 |
+
|
79 |
+
1. Install dependencies for both the backend and the test environment
|
80 |
+
|
81 |
+
```bash
|
82 |
+
poetry install -C api --with dev
|
83 |
+
```
|
84 |
+
|
85 |
+
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
|
86 |
+
|
87 |
+
```bash
|
88 |
+
poetry run -P api bash dev/pytest/pytest_all_tests.sh
|
89 |
+
```
|
api/app.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
|
4 |
+
|
5 |
+
def is_db_command():
|
6 |
+
if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db":
|
7 |
+
return True
|
8 |
+
return False
|
9 |
+
|
10 |
+
|
11 |
+
# create app
|
12 |
+
if is_db_command():
|
13 |
+
from app_factory import create_migrations_app
|
14 |
+
|
15 |
+
app = create_migrations_app()
|
16 |
+
else:
|
17 |
+
# It seems that JetBrains Python debugger does not work well with gevent,
|
18 |
+
# so we need to disable gevent in debug mode.
|
19 |
+
# If you are using debugpy and set GEVENT_SUPPORT=True, you can debug with gevent.
|
20 |
+
if (flask_debug := os.environ.get("FLASK_DEBUG", "0")) and flask_debug.lower() in {"false", "0", "no"}:
|
21 |
+
from gevent import monkey # type: ignore
|
22 |
+
|
23 |
+
# gevent
|
24 |
+
monkey.patch_all()
|
25 |
+
|
26 |
+
from grpc.experimental import gevent as grpc_gevent # type: ignore
|
27 |
+
|
28 |
+
# grpc gevent
|
29 |
+
grpc_gevent.init_gevent()
|
30 |
+
|
31 |
+
import psycogreen.gevent # type: ignore
|
32 |
+
|
33 |
+
psycogreen.gevent.patch_psycopg()
|
34 |
+
|
35 |
+
from app_factory import create_app
|
36 |
+
|
37 |
+
app = create_app()
|
38 |
+
celery = app.extensions["celery"]
|
39 |
+
|
40 |
+
if __name__ == "__main__":
|
41 |
+
app.run(host="0.0.0.0", port=5001)
|
api/app_factory.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import time
|
3 |
+
|
4 |
+
from configs import dify_config
|
5 |
+
from dify_app import DifyApp
|
6 |
+
|
7 |
+
|
8 |
+
# ----------------------------
|
9 |
+
# Application Factory Function
|
10 |
+
# ----------------------------
|
11 |
+
def create_flask_app_with_configs() -> DifyApp:
|
12 |
+
"""
|
13 |
+
create a raw flask app
|
14 |
+
with configs loaded from .env file
|
15 |
+
"""
|
16 |
+
dify_app = DifyApp(__name__)
|
17 |
+
dify_app.config.from_mapping(dify_config.model_dump())
|
18 |
+
|
19 |
+
return dify_app
|
20 |
+
|
21 |
+
|
22 |
+
def create_app() -> DifyApp:
|
23 |
+
start_time = time.perf_counter()
|
24 |
+
app = create_flask_app_with_configs()
|
25 |
+
initialize_extensions(app)
|
26 |
+
end_time = time.perf_counter()
|
27 |
+
if dify_config.DEBUG:
|
28 |
+
logging.info(f"Finished create_app ({round((end_time - start_time) * 1000, 2)} ms)")
|
29 |
+
return app
|
30 |
+
|
31 |
+
|
32 |
+
def initialize_extensions(app: DifyApp):
|
33 |
+
from extensions import (
|
34 |
+
ext_app_metrics,
|
35 |
+
ext_blueprints,
|
36 |
+
ext_celery,
|
37 |
+
ext_code_based_extension,
|
38 |
+
ext_commands,
|
39 |
+
ext_compress,
|
40 |
+
ext_database,
|
41 |
+
ext_hosting_provider,
|
42 |
+
ext_import_modules,
|
43 |
+
ext_logging,
|
44 |
+
ext_login,
|
45 |
+
ext_mail,
|
46 |
+
ext_migrate,
|
47 |
+
ext_proxy_fix,
|
48 |
+
ext_redis,
|
49 |
+
ext_sentry,
|
50 |
+
ext_set_secretkey,
|
51 |
+
ext_storage,
|
52 |
+
ext_timezone,
|
53 |
+
ext_warnings,
|
54 |
+
)
|
55 |
+
|
56 |
+
extensions = [
|
57 |
+
ext_timezone,
|
58 |
+
ext_logging,
|
59 |
+
ext_warnings,
|
60 |
+
ext_import_modules,
|
61 |
+
ext_set_secretkey,
|
62 |
+
ext_compress,
|
63 |
+
ext_code_based_extension,
|
64 |
+
ext_database,
|
65 |
+
ext_app_metrics,
|
66 |
+
ext_migrate,
|
67 |
+
ext_redis,
|
68 |
+
ext_storage,
|
69 |
+
ext_celery,
|
70 |
+
ext_login,
|
71 |
+
ext_mail,
|
72 |
+
ext_hosting_provider,
|
73 |
+
ext_sentry,
|
74 |
+
ext_proxy_fix,
|
75 |
+
ext_blueprints,
|
76 |
+
ext_commands,
|
77 |
+
]
|
78 |
+
for ext in extensions:
|
79 |
+
short_name = ext.__name__.split(".")[-1]
|
80 |
+
is_enabled = ext.is_enabled() if hasattr(ext, "is_enabled") else True
|
81 |
+
if not is_enabled:
|
82 |
+
if dify_config.DEBUG:
|
83 |
+
logging.info(f"Skipped {short_name}")
|
84 |
+
continue
|
85 |
+
|
86 |
+
start_time = time.perf_counter()
|
87 |
+
ext.init_app(app)
|
88 |
+
end_time = time.perf_counter()
|
89 |
+
if dify_config.DEBUG:
|
90 |
+
logging.info(f"Loaded {short_name} ({round((end_time - start_time) * 1000, 2)} ms)")
|
91 |
+
|
92 |
+
|
93 |
+
def create_migrations_app():
|
94 |
+
app = create_flask_app_with_configs()
|
95 |
+
from extensions import ext_database, ext_migrate
|
96 |
+
|
97 |
+
# Initialize only required extensions
|
98 |
+
ext_database.init_app(app)
|
99 |
+
ext_migrate.init_app(app)
|
100 |
+
|
101 |
+
return app
|
api/commands.py
ADDED
@@ -0,0 +1,651 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import base64
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
import secrets
|
5 |
+
from typing import Optional
|
6 |
+
|
7 |
+
import click
|
8 |
+
from flask import current_app
|
9 |
+
from werkzeug.exceptions import NotFound
|
10 |
+
|
11 |
+
from configs import dify_config
|
12 |
+
from constants.languages import languages
|
13 |
+
from core.rag.datasource.vdb.vector_factory import Vector
|
14 |
+
from core.rag.datasource.vdb.vector_type import VectorType
|
15 |
+
from core.rag.models.document import Document
|
16 |
+
from events.app_event import app_was_created
|
17 |
+
from extensions.ext_database import db
|
18 |
+
from extensions.ext_redis import redis_client
|
19 |
+
from libs.helper import email as email_validate
|
20 |
+
from libs.password import hash_password, password_pattern, valid_password
|
21 |
+
from libs.rsa import generate_key_pair
|
22 |
+
from models import Tenant
|
23 |
+
from models.dataset import Dataset, DatasetCollectionBinding, DocumentSegment
|
24 |
+
from models.dataset import Document as DatasetDocument
|
25 |
+
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
|
26 |
+
from models.provider import Provider, ProviderModel
|
27 |
+
from services.account_service import RegisterService, TenantService
|
28 |
+
|
29 |
+
|
30 |
+
@click.command("reset-password", help="Reset the account password.")
|
31 |
+
@click.option("--email", prompt=True, help="Account email to reset password for")
|
32 |
+
@click.option("--new-password", prompt=True, help="New password")
|
33 |
+
@click.option("--password-confirm", prompt=True, help="Confirm new password")
|
34 |
+
def reset_password(email, new_password, password_confirm):
|
35 |
+
"""
|
36 |
+
Reset password of owner account
|
37 |
+
Only available in SELF_HOSTED mode
|
38 |
+
"""
|
39 |
+
if str(new_password).strip() != str(password_confirm).strip():
|
40 |
+
click.echo(click.style("Passwords do not match.", fg="red"))
|
41 |
+
return
|
42 |
+
|
43 |
+
account = db.session.query(Account).filter(Account.email == email).one_or_none()
|
44 |
+
|
45 |
+
if not account:
|
46 |
+
click.echo(click.style("Account not found for email: {}".format(email), fg="red"))
|
47 |
+
return
|
48 |
+
|
49 |
+
try:
|
50 |
+
valid_password(new_password)
|
51 |
+
except:
|
52 |
+
click.echo(click.style("Invalid password. Must match {}".format(password_pattern), fg="red"))
|
53 |
+
return
|
54 |
+
|
55 |
+
# generate password salt
|
56 |
+
salt = secrets.token_bytes(16)
|
57 |
+
base64_salt = base64.b64encode(salt).decode()
|
58 |
+
|
59 |
+
# encrypt password with salt
|
60 |
+
password_hashed = hash_password(new_password, salt)
|
61 |
+
base64_password_hashed = base64.b64encode(password_hashed).decode()
|
62 |
+
account.password = base64_password_hashed
|
63 |
+
account.password_salt = base64_salt
|
64 |
+
db.session.commit()
|
65 |
+
click.echo(click.style("Password reset successfully.", fg="green"))
|
66 |
+
|
67 |
+
|
68 |
+
@click.command("reset-email", help="Reset the account email.")
|
69 |
+
@click.option("--email", prompt=True, help="Current account email")
|
70 |
+
@click.option("--new-email", prompt=True, help="New email")
|
71 |
+
@click.option("--email-confirm", prompt=True, help="Confirm new email")
|
72 |
+
def reset_email(email, new_email, email_confirm):
|
73 |
+
"""
|
74 |
+
Replace account email
|
75 |
+
:return:
|
76 |
+
"""
|
77 |
+
if str(new_email).strip() != str(email_confirm).strip():
|
78 |
+
click.echo(click.style("New emails do not match.", fg="red"))
|
79 |
+
return
|
80 |
+
|
81 |
+
account = db.session.query(Account).filter(Account.email == email).one_or_none()
|
82 |
+
|
83 |
+
if not account:
|
84 |
+
click.echo(click.style("Account not found for email: {}".format(email), fg="red"))
|
85 |
+
return
|
86 |
+
|
87 |
+
try:
|
88 |
+
email_validate(new_email)
|
89 |
+
except:
|
90 |
+
click.echo(click.style("Invalid email: {}".format(new_email), fg="red"))
|
91 |
+
return
|
92 |
+
|
93 |
+
account.email = new_email
|
94 |
+
db.session.commit()
|
95 |
+
click.echo(click.style("Email updated successfully.", fg="green"))
|
96 |
+
|
97 |
+
|
98 |
+
@click.command(
|
99 |
+
"reset-encrypt-key-pair",
|
100 |
+
help="Reset the asymmetric key pair of workspace for encrypt LLM credentials. "
|
101 |
+
"After the reset, all LLM credentials will become invalid, "
|
102 |
+
"requiring re-entry."
|
103 |
+
"Only support SELF_HOSTED mode.",
|
104 |
+
)
|
105 |
+
@click.confirmation_option(
|
106 |
+
prompt=click.style(
|
107 |
+
"Are you sure you want to reset encrypt key pair? This operation cannot be rolled back!", fg="red"
|
108 |
+
)
|
109 |
+
)
|
110 |
+
def reset_encrypt_key_pair():
|
111 |
+
"""
|
112 |
+
Reset the encrypted key pair of workspace for encrypt LLM credentials.
|
113 |
+
After the reset, all LLM credentials will become invalid, requiring re-entry.
|
114 |
+
Only support SELF_HOSTED mode.
|
115 |
+
"""
|
116 |
+
if dify_config.EDITION != "SELF_HOSTED":
|
117 |
+
click.echo(click.style("This command is only for SELF_HOSTED installations.", fg="red"))
|
118 |
+
return
|
119 |
+
|
120 |
+
tenants = db.session.query(Tenant).all()
|
121 |
+
for tenant in tenants:
|
122 |
+
if not tenant:
|
123 |
+
click.echo(click.style("No workspaces found. Run /install first.", fg="red"))
|
124 |
+
return
|
125 |
+
|
126 |
+
tenant.encrypt_public_key = generate_key_pair(tenant.id)
|
127 |
+
|
128 |
+
db.session.query(Provider).filter(Provider.provider_type == "custom", Provider.tenant_id == tenant.id).delete()
|
129 |
+
db.session.query(ProviderModel).filter(ProviderModel.tenant_id == tenant.id).delete()
|
130 |
+
db.session.commit()
|
131 |
+
|
132 |
+
click.echo(
|
133 |
+
click.style(
|
134 |
+
"Congratulations! The asymmetric key pair of workspace {} has been reset.".format(tenant.id),
|
135 |
+
fg="green",
|
136 |
+
)
|
137 |
+
)
|
138 |
+
|
139 |
+
|
140 |
+
@click.command("vdb-migrate", help="Migrate vector db.")
|
141 |
+
@click.option("--scope", default="all", prompt=False, help="The scope of vector database to migrate, Default is All.")
|
142 |
+
def vdb_migrate(scope: str):
|
143 |
+
if scope in {"knowledge", "all"}:
|
144 |
+
migrate_knowledge_vector_database()
|
145 |
+
if scope in {"annotation", "all"}:
|
146 |
+
migrate_annotation_vector_database()
|
147 |
+
|
148 |
+
|
149 |
+
def migrate_annotation_vector_database():
|
150 |
+
"""
|
151 |
+
Migrate annotation datas to target vector database .
|
152 |
+
"""
|
153 |
+
click.echo(click.style("Starting annotation data migration.", fg="green"))
|
154 |
+
create_count = 0
|
155 |
+
skipped_count = 0
|
156 |
+
total_count = 0
|
157 |
+
page = 1
|
158 |
+
while True:
|
159 |
+
try:
|
160 |
+
# get apps info
|
161 |
+
apps = (
|
162 |
+
App.query.filter(App.status == "normal")
|
163 |
+
.order_by(App.created_at.desc())
|
164 |
+
.paginate(page=page, per_page=50)
|
165 |
+
)
|
166 |
+
except NotFound:
|
167 |
+
break
|
168 |
+
|
169 |
+
page += 1
|
170 |
+
for app in apps:
|
171 |
+
total_count = total_count + 1
|
172 |
+
click.echo(
|
173 |
+
f"Processing the {total_count} app {app.id}. " + f"{create_count} created, {skipped_count} skipped."
|
174 |
+
)
|
175 |
+
try:
|
176 |
+
click.echo("Creating app annotation index: {}".format(app.id))
|
177 |
+
app_annotation_setting = (
|
178 |
+
db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app.id).first()
|
179 |
+
)
|
180 |
+
|
181 |
+
if not app_annotation_setting:
|
182 |
+
skipped_count = skipped_count + 1
|
183 |
+
click.echo("App annotation setting disabled: {}".format(app.id))
|
184 |
+
continue
|
185 |
+
# get dataset_collection_binding info
|
186 |
+
dataset_collection_binding = (
|
187 |
+
db.session.query(DatasetCollectionBinding)
|
188 |
+
.filter(DatasetCollectionBinding.id == app_annotation_setting.collection_binding_id)
|
189 |
+
.first()
|
190 |
+
)
|
191 |
+
if not dataset_collection_binding:
|
192 |
+
click.echo("App annotation collection binding not found: {}".format(app.id))
|
193 |
+
continue
|
194 |
+
annotations = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app.id).all()
|
195 |
+
dataset = Dataset(
|
196 |
+
id=app.id,
|
197 |
+
tenant_id=app.tenant_id,
|
198 |
+
indexing_technique="high_quality",
|
199 |
+
embedding_model_provider=dataset_collection_binding.provider_name,
|
200 |
+
embedding_model=dataset_collection_binding.model_name,
|
201 |
+
collection_binding_id=dataset_collection_binding.id,
|
202 |
+
)
|
203 |
+
documents = []
|
204 |
+
if annotations:
|
205 |
+
for annotation in annotations:
|
206 |
+
document = Document(
|
207 |
+
page_content=annotation.question,
|
208 |
+
metadata={"annotation_id": annotation.id, "app_id": app.id, "doc_id": annotation.id},
|
209 |
+
)
|
210 |
+
documents.append(document)
|
211 |
+
|
212 |
+
vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"])
|
213 |
+
click.echo(f"Migrating annotations for app: {app.id}.")
|
214 |
+
|
215 |
+
try:
|
216 |
+
vector.delete()
|
217 |
+
click.echo(click.style(f"Deleted vector index for app {app.id}.", fg="green"))
|
218 |
+
except Exception as e:
|
219 |
+
click.echo(click.style(f"Failed to delete vector index for app {app.id}.", fg="red"))
|
220 |
+
raise e
|
221 |
+
if documents:
|
222 |
+
try:
|
223 |
+
click.echo(
|
224 |
+
click.style(
|
225 |
+
f"Creating vector index with {len(documents)} annotations for app {app.id}.",
|
226 |
+
fg="green",
|
227 |
+
)
|
228 |
+
)
|
229 |
+
vector.create(documents)
|
230 |
+
click.echo(click.style(f"Created vector index for app {app.id}.", fg="green"))
|
231 |
+
except Exception as e:
|
232 |
+
click.echo(click.style(f"Failed to created vector index for app {app.id}.", fg="red"))
|
233 |
+
raise e
|
234 |
+
click.echo(f"Successfully migrated app annotation {app.id}.")
|
235 |
+
create_count += 1
|
236 |
+
except Exception as e:
|
237 |
+
click.echo(
|
238 |
+
click.style(
|
239 |
+
"Error creating app annotation index: {} {}".format(e.__class__.__name__, str(e)), fg="red"
|
240 |
+
)
|
241 |
+
)
|
242 |
+
continue
|
243 |
+
|
244 |
+
click.echo(
|
245 |
+
click.style(
|
246 |
+
f"Migration complete. Created {create_count} app annotation indexes. Skipped {skipped_count} apps.",
|
247 |
+
fg="green",
|
248 |
+
)
|
249 |
+
)
|
250 |
+
|
251 |
+
|
252 |
+
def migrate_knowledge_vector_database():
|
253 |
+
"""
|
254 |
+
Migrate vector database datas to target vector database .
|
255 |
+
"""
|
256 |
+
click.echo(click.style("Starting vector database migration.", fg="green"))
|
257 |
+
create_count = 0
|
258 |
+
skipped_count = 0
|
259 |
+
total_count = 0
|
260 |
+
vector_type = dify_config.VECTOR_STORE
|
261 |
+
upper_collection_vector_types = {
|
262 |
+
VectorType.MILVUS,
|
263 |
+
VectorType.PGVECTOR,
|
264 |
+
VectorType.RELYT,
|
265 |
+
VectorType.WEAVIATE,
|
266 |
+
VectorType.ORACLE,
|
267 |
+
VectorType.ELASTICSEARCH,
|
268 |
+
}
|
269 |
+
lower_collection_vector_types = {
|
270 |
+
VectorType.ANALYTICDB,
|
271 |
+
VectorType.CHROMA,
|
272 |
+
VectorType.MYSCALE,
|
273 |
+
VectorType.PGVECTO_RS,
|
274 |
+
VectorType.TIDB_VECTOR,
|
275 |
+
VectorType.OPENSEARCH,
|
276 |
+
VectorType.TENCENT,
|
277 |
+
VectorType.BAIDU,
|
278 |
+
VectorType.VIKINGDB,
|
279 |
+
VectorType.UPSTASH,
|
280 |
+
VectorType.COUCHBASE,
|
281 |
+
VectorType.OCEANBASE,
|
282 |
+
}
|
283 |
+
page = 1
|
284 |
+
while True:
|
285 |
+
try:
|
286 |
+
datasets = (
|
287 |
+
Dataset.query.filter(Dataset.indexing_technique == "high_quality")
|
288 |
+
.order_by(Dataset.created_at.desc())
|
289 |
+
.paginate(page=page, per_page=50)
|
290 |
+
)
|
291 |
+
except NotFound:
|
292 |
+
break
|
293 |
+
|
294 |
+
page += 1
|
295 |
+
for dataset in datasets:
|
296 |
+
total_count = total_count + 1
|
297 |
+
click.echo(
|
298 |
+
f"Processing the {total_count} dataset {dataset.id}. {create_count} created, {skipped_count} skipped."
|
299 |
+
)
|
300 |
+
try:
|
301 |
+
click.echo("Creating dataset vector database index: {}".format(dataset.id))
|
302 |
+
if dataset.index_struct_dict:
|
303 |
+
if dataset.index_struct_dict["type"] == vector_type:
|
304 |
+
skipped_count = skipped_count + 1
|
305 |
+
continue
|
306 |
+
collection_name = ""
|
307 |
+
dataset_id = dataset.id
|
308 |
+
if vector_type in upper_collection_vector_types:
|
309 |
+
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
310 |
+
elif vector_type == VectorType.QDRANT:
|
311 |
+
if dataset.collection_binding_id:
|
312 |
+
dataset_collection_binding = (
|
313 |
+
db.session.query(DatasetCollectionBinding)
|
314 |
+
.filter(DatasetCollectionBinding.id == dataset.collection_binding_id)
|
315 |
+
.one_or_none()
|
316 |
+
)
|
317 |
+
if dataset_collection_binding:
|
318 |
+
collection_name = dataset_collection_binding.collection_name
|
319 |
+
else:
|
320 |
+
raise ValueError("Dataset Collection Binding not found")
|
321 |
+
else:
|
322 |
+
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
|
323 |
+
|
324 |
+
elif vector_type in lower_collection_vector_types:
|
325 |
+
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
|
326 |
+
else:
|
327 |
+
raise ValueError(f"Vector store {vector_type} is not supported.")
|
328 |
+
|
329 |
+
index_struct_dict = {"type": vector_type, "vector_store": {"class_prefix": collection_name}}
|
330 |
+
dataset.index_struct = json.dumps(index_struct_dict)
|
331 |
+
vector = Vector(dataset)
|
332 |
+
click.echo(f"Migrating dataset {dataset.id}.")
|
333 |
+
|
334 |
+
try:
|
335 |
+
vector.delete()
|
336 |
+
click.echo(
|
337 |
+
click.style(f"Deleted vector index {collection_name} for dataset {dataset.id}.", fg="green")
|
338 |
+
)
|
339 |
+
except Exception as e:
|
340 |
+
click.echo(
|
341 |
+
click.style(
|
342 |
+
f"Failed to delete vector index {collection_name} for dataset {dataset.id}.", fg="red"
|
343 |
+
)
|
344 |
+
)
|
345 |
+
raise e
|
346 |
+
|
347 |
+
dataset_documents = (
|
348 |
+
db.session.query(DatasetDocument)
|
349 |
+
.filter(
|
350 |
+
DatasetDocument.dataset_id == dataset.id,
|
351 |
+
DatasetDocument.indexing_status == "completed",
|
352 |
+
DatasetDocument.enabled == True,
|
353 |
+
DatasetDocument.archived == False,
|
354 |
+
)
|
355 |
+
.all()
|
356 |
+
)
|
357 |
+
|
358 |
+
documents = []
|
359 |
+
segments_count = 0
|
360 |
+
for dataset_document in dataset_documents:
|
361 |
+
segments = (
|
362 |
+
db.session.query(DocumentSegment)
|
363 |
+
.filter(
|
364 |
+
DocumentSegment.document_id == dataset_document.id,
|
365 |
+
DocumentSegment.status == "completed",
|
366 |
+
DocumentSegment.enabled == True,
|
367 |
+
)
|
368 |
+
.all()
|
369 |
+
)
|
370 |
+
|
371 |
+
for segment in segments:
|
372 |
+
document = Document(
|
373 |
+
page_content=segment.content,
|
374 |
+
metadata={
|
375 |
+
"doc_id": segment.index_node_id,
|
376 |
+
"doc_hash": segment.index_node_hash,
|
377 |
+
"document_id": segment.document_id,
|
378 |
+
"dataset_id": segment.dataset_id,
|
379 |
+
},
|
380 |
+
)
|
381 |
+
|
382 |
+
documents.append(document)
|
383 |
+
segments_count = segments_count + 1
|
384 |
+
|
385 |
+
if documents:
|
386 |
+
try:
|
387 |
+
click.echo(
|
388 |
+
click.style(
|
389 |
+
f"Creating vector index with {len(documents)} documents of {segments_count}"
|
390 |
+
f" segments for dataset {dataset.id}.",
|
391 |
+
fg="green",
|
392 |
+
)
|
393 |
+
)
|
394 |
+
vector.create(documents)
|
395 |
+
click.echo(click.style(f"Created vector index for dataset {dataset.id}.", fg="green"))
|
396 |
+
except Exception as e:
|
397 |
+
click.echo(click.style(f"Failed to created vector index for dataset {dataset.id}.", fg="red"))
|
398 |
+
raise e
|
399 |
+
db.session.add(dataset)
|
400 |
+
db.session.commit()
|
401 |
+
click.echo(f"Successfully migrated dataset {dataset.id}.")
|
402 |
+
create_count += 1
|
403 |
+
except Exception as e:
|
404 |
+
db.session.rollback()
|
405 |
+
click.echo(
|
406 |
+
click.style("Error creating dataset index: {} {}".format(e.__class__.__name__, str(e)), fg="red")
|
407 |
+
)
|
408 |
+
continue
|
409 |
+
|
410 |
+
click.echo(
|
411 |
+
click.style(
|
412 |
+
f"Migration complete. Created {create_count} dataset indexes. Skipped {skipped_count} datasets.", fg="green"
|
413 |
+
)
|
414 |
+
)
|
415 |
+
|
416 |
+
|
417 |
+
@click.command("convert-to-agent-apps", help="Convert Agent Assistant to Agent App.")
|
418 |
+
def convert_to_agent_apps():
|
419 |
+
"""
|
420 |
+
Convert Agent Assistant to Agent App.
|
421 |
+
"""
|
422 |
+
click.echo(click.style("Starting convert to agent apps.", fg="green"))
|
423 |
+
|
424 |
+
proceeded_app_ids = []
|
425 |
+
|
426 |
+
while True:
|
427 |
+
# fetch first 1000 apps
|
428 |
+
sql_query = """SELECT a.id AS id FROM apps a
|
429 |
+
INNER JOIN app_model_configs am ON a.app_model_config_id=am.id
|
430 |
+
WHERE a.mode = 'chat'
|
431 |
+
AND am.agent_mode is not null
|
432 |
+
AND (
|
433 |
+
am.agent_mode like '%"strategy": "function_call"%'
|
434 |
+
OR am.agent_mode like '%"strategy": "react"%'
|
435 |
+
)
|
436 |
+
AND (
|
437 |
+
am.agent_mode like '{"enabled": true%'
|
438 |
+
OR am.agent_mode like '{"max_iteration": %'
|
439 |
+
) ORDER BY a.created_at DESC LIMIT 1000
|
440 |
+
"""
|
441 |
+
|
442 |
+
with db.engine.begin() as conn:
|
443 |
+
rs = conn.execute(db.text(sql_query))
|
444 |
+
|
445 |
+
apps = []
|
446 |
+
for i in rs:
|
447 |
+
app_id = str(i.id)
|
448 |
+
if app_id not in proceeded_app_ids:
|
449 |
+
proceeded_app_ids.append(app_id)
|
450 |
+
app = db.session.query(App).filter(App.id == app_id).first()
|
451 |
+
if app is not None:
|
452 |
+
apps.append(app)
|
453 |
+
|
454 |
+
if len(apps) == 0:
|
455 |
+
break
|
456 |
+
|
457 |
+
for app in apps:
|
458 |
+
click.echo("Converting app: {}".format(app.id))
|
459 |
+
|
460 |
+
try:
|
461 |
+
app.mode = AppMode.AGENT_CHAT.value
|
462 |
+
db.session.commit()
|
463 |
+
|
464 |
+
# update conversation mode to agent
|
465 |
+
db.session.query(Conversation).filter(Conversation.app_id == app.id).update(
|
466 |
+
{Conversation.mode: AppMode.AGENT_CHAT.value}
|
467 |
+
)
|
468 |
+
|
469 |
+
db.session.commit()
|
470 |
+
click.echo(click.style("Converted app: {}".format(app.id), fg="green"))
|
471 |
+
except Exception as e:
|
472 |
+
click.echo(click.style("Convert app error: {} {}".format(e.__class__.__name__, str(e)), fg="red"))
|
473 |
+
|
474 |
+
click.echo(click.style("Conversion complete. Converted {} agent apps.".format(len(proceeded_app_ids)), fg="green"))
|
475 |
+
|
476 |
+
|
477 |
+
@click.command("add-qdrant-doc-id-index", help="Add Qdrant doc_id index.")
|
478 |
+
@click.option("--field", default="metadata.doc_id", prompt=False, help="Index field , default is metadata.doc_id.")
|
479 |
+
def add_qdrant_doc_id_index(field: str):
|
480 |
+
click.echo(click.style("Starting Qdrant doc_id index creation.", fg="green"))
|
481 |
+
vector_type = dify_config.VECTOR_STORE
|
482 |
+
if vector_type != "qdrant":
|
483 |
+
click.echo(click.style("This command only supports Qdrant vector store.", fg="red"))
|
484 |
+
return
|
485 |
+
create_count = 0
|
486 |
+
|
487 |
+
try:
|
488 |
+
bindings = db.session.query(DatasetCollectionBinding).all()
|
489 |
+
if not bindings:
|
490 |
+
click.echo(click.style("No dataset collection bindings found.", fg="red"))
|
491 |
+
return
|
492 |
+
import qdrant_client
|
493 |
+
from qdrant_client.http.exceptions import UnexpectedResponse
|
494 |
+
from qdrant_client.http.models import PayloadSchemaType
|
495 |
+
|
496 |
+
from core.rag.datasource.vdb.qdrant.qdrant_vector import QdrantConfig
|
497 |
+
|
498 |
+
for binding in bindings:
|
499 |
+
if dify_config.QDRANT_URL is None:
|
500 |
+
raise ValueError("Qdrant URL is required.")
|
501 |
+
qdrant_config = QdrantConfig(
|
502 |
+
endpoint=dify_config.QDRANT_URL,
|
503 |
+
api_key=dify_config.QDRANT_API_KEY,
|
504 |
+
root_path=current_app.root_path,
|
505 |
+
timeout=dify_config.QDRANT_CLIENT_TIMEOUT,
|
506 |
+
grpc_port=dify_config.QDRANT_GRPC_PORT,
|
507 |
+
prefer_grpc=dify_config.QDRANT_GRPC_ENABLED,
|
508 |
+
)
|
509 |
+
try:
|
510 |
+
client = qdrant_client.QdrantClient(**qdrant_config.to_qdrant_params())
|
511 |
+
# create payload index
|
512 |
+
client.create_payload_index(binding.collection_name, field, field_schema=PayloadSchemaType.KEYWORD)
|
513 |
+
create_count += 1
|
514 |
+
except UnexpectedResponse as e:
|
515 |
+
# Collection does not exist, so return
|
516 |
+
if e.status_code == 404:
|
517 |
+
click.echo(click.style(f"Collection not found: {binding.collection_name}.", fg="red"))
|
518 |
+
continue
|
519 |
+
# Some other error occurred, so re-raise the exception
|
520 |
+
else:
|
521 |
+
click.echo(
|
522 |
+
click.style(
|
523 |
+
f"Failed to create Qdrant index for collection: {binding.collection_name}.", fg="red"
|
524 |
+
)
|
525 |
+
)
|
526 |
+
|
527 |
+
except Exception as e:
|
528 |
+
click.echo(click.style("Failed to create Qdrant client.", fg="red"))
|
529 |
+
|
530 |
+
click.echo(click.style(f"Index creation complete. Created {create_count} collection indexes.", fg="green"))
|
531 |
+
|
532 |
+
|
533 |
+
@click.command("create-tenant", help="Create account and tenant.")
|
534 |
+
@click.option("--email", prompt=True, help="Tenant account email.")
|
535 |
+
@click.option("--name", prompt=True, help="Workspace name.")
|
536 |
+
@click.option("--language", prompt=True, help="Account language, default: en-US.")
|
537 |
+
def create_tenant(email: str, language: Optional[str] = None, name: Optional[str] = None):
|
538 |
+
"""
|
539 |
+
Create tenant account
|
540 |
+
"""
|
541 |
+
if not email:
|
542 |
+
click.echo(click.style("Email is required.", fg="red"))
|
543 |
+
return
|
544 |
+
|
545 |
+
# Create account
|
546 |
+
email = email.strip()
|
547 |
+
|
548 |
+
if "@" not in email:
|
549 |
+
click.echo(click.style("Invalid email address.", fg="red"))
|
550 |
+
return
|
551 |
+
|
552 |
+
account_name = email.split("@")[0]
|
553 |
+
|
554 |
+
if language not in languages:
|
555 |
+
language = "en-US"
|
556 |
+
|
557 |
+
# Validates name encoding for non-Latin characters.
|
558 |
+
name = name.strip().encode("utf-8").decode("utf-8") if name else None
|
559 |
+
|
560 |
+
# generate random password
|
561 |
+
new_password = secrets.token_urlsafe(16)
|
562 |
+
|
563 |
+
# register account
|
564 |
+
account = RegisterService.register(
|
565 |
+
email=email,
|
566 |
+
name=account_name,
|
567 |
+
password=new_password,
|
568 |
+
language=language,
|
569 |
+
create_workspace_required=False,
|
570 |
+
)
|
571 |
+
TenantService.create_owner_tenant_if_not_exist(account, name)
|
572 |
+
|
573 |
+
click.echo(
|
574 |
+
click.style(
|
575 |
+
"Account and tenant created.\nAccount: {}\nPassword: {}".format(email, new_password),
|
576 |
+
fg="green",
|
577 |
+
)
|
578 |
+
)
|
579 |
+
|
580 |
+
|
581 |
+
@click.command("upgrade-db", help="Upgrade the database")
|
582 |
+
def upgrade_db():
|
583 |
+
click.echo("Preparing database migration...")
|
584 |
+
lock = redis_client.lock(name="db_upgrade_lock", timeout=60)
|
585 |
+
if lock.acquire(blocking=False):
|
586 |
+
try:
|
587 |
+
click.echo(click.style("Starting database migration.", fg="green"))
|
588 |
+
|
589 |
+
# run db migration
|
590 |
+
import flask_migrate # type: ignore
|
591 |
+
|
592 |
+
flask_migrate.upgrade()
|
593 |
+
|
594 |
+
click.echo(click.style("Database migration successful!", fg="green"))
|
595 |
+
|
596 |
+
except Exception as e:
|
597 |
+
logging.exception("Failed to execute database migration")
|
598 |
+
finally:
|
599 |
+
lock.release()
|
600 |
+
else:
|
601 |
+
click.echo("Database migration skipped")
|
602 |
+
|
603 |
+
|
604 |
+
@click.command("fix-app-site-missing", help="Fix app related site missing issue.")
|
605 |
+
def fix_app_site_missing():
|
606 |
+
"""
|
607 |
+
Fix app related site missing issue.
|
608 |
+
"""
|
609 |
+
click.echo(click.style("Starting fix for missing app-related sites.", fg="green"))
|
610 |
+
|
611 |
+
failed_app_ids = []
|
612 |
+
while True:
|
613 |
+
sql = """select apps.id as id from apps left join sites on sites.app_id=apps.id
|
614 |
+
where sites.id is null limit 1000"""
|
615 |
+
with db.engine.begin() as conn:
|
616 |
+
rs = conn.execute(db.text(sql))
|
617 |
+
|
618 |
+
processed_count = 0
|
619 |
+
for i in rs:
|
620 |
+
processed_count += 1
|
621 |
+
app_id = str(i.id)
|
622 |
+
|
623 |
+
if app_id in failed_app_ids:
|
624 |
+
continue
|
625 |
+
|
626 |
+
try:
|
627 |
+
app = db.session.query(App).filter(App.id == app_id).first()
|
628 |
+
if not app:
|
629 |
+
print(f"App {app_id} not found")
|
630 |
+
continue
|
631 |
+
|
632 |
+
tenant = app.tenant
|
633 |
+
if tenant:
|
634 |
+
accounts = tenant.get_accounts()
|
635 |
+
if not accounts:
|
636 |
+
print("Fix failed for app {}".format(app.id))
|
637 |
+
continue
|
638 |
+
|
639 |
+
account = accounts[0]
|
640 |
+
print("Fixing missing site for app {}".format(app.id))
|
641 |
+
app_was_created.send(app, account=account)
|
642 |
+
except Exception as e:
|
643 |
+
failed_app_ids.append(app_id)
|
644 |
+
click.echo(click.style("Failed to fix missing site for app {}".format(app_id), fg="red"))
|
645 |
+
logging.exception(f"Failed to fix app related site missing issue, app_id: {app_id}")
|
646 |
+
continue
|
647 |
+
|
648 |
+
if not processed_count:
|
649 |
+
break
|
650 |
+
|
651 |
+
click.echo(click.style("Fix for missing app-related sites completed successfully!", fg="green"))
|
api/dify_app.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask
|
2 |
+
|
3 |
+
|
4 |
+
class DifyApp(Flask):
|
5 |
+
pass
|
api/mypy.ini
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[mypy]
|
2 |
+
warn_return_any = True
|
3 |
+
warn_unused_configs = True
|
4 |
+
check_untyped_defs = True
|
5 |
+
exclude = (?x)(
|
6 |
+
core/tools/provider/builtin/
|
7 |
+
| core/model_runtime/model_providers/
|
8 |
+
| tests/
|
9 |
+
| migrations/
|
10 |
+
)
|
api/poetry.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|
api/poetry.toml
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[virtualenvs]
|
2 |
+
in-project = true
|
3 |
+
create = true
|
4 |
+
prefer-active-python = true
|
api/pyproject.toml
ADDED
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "dify-api"
|
3 |
+
requires-python = ">=3.11,<3.13"
|
4 |
+
dynamic = [ "dependencies" ]
|
5 |
+
|
6 |
+
[build-system]
|
7 |
+
requires = ["poetry-core>=2.0.0"]
|
8 |
+
build-backend = "poetry.core.masonry.api"
|
9 |
+
|
10 |
+
[tool.poetry]
|
11 |
+
package-mode = false
|
12 |
+
|
13 |
+
############################################################
|
14 |
+
# [ Main ] Dependency group
|
15 |
+
############################################################
|
16 |
+
|
17 |
+
[tool.poetry.dependencies]
|
18 |
+
anthropic = "~0.23.1"
|
19 |
+
authlib = "1.3.1"
|
20 |
+
azure-ai-inference = "~1.0.0b8"
|
21 |
+
azure-ai-ml = "~1.20.0"
|
22 |
+
azure-identity = "1.16.1"
|
23 |
+
beautifulsoup4 = "4.12.2"
|
24 |
+
boto3 = "1.36.12"
|
25 |
+
bs4 = "~0.0.1"
|
26 |
+
cachetools = "~5.3.0"
|
27 |
+
celery = "~5.4.0"
|
28 |
+
chardet = "~5.1.0"
|
29 |
+
cohere = "~5.2.4"
|
30 |
+
dashscope = { version = "~1.17.0", extras = ["tokenizer"] }
|
31 |
+
fal-client = "0.5.6"
|
32 |
+
flask = "~3.1.0"
|
33 |
+
flask-compress = "~1.17"
|
34 |
+
flask-cors = "~4.0.0"
|
35 |
+
flask-login = "~0.6.3"
|
36 |
+
flask-migrate = "~4.0.7"
|
37 |
+
flask-restful = "~0.3.10"
|
38 |
+
flask-sqlalchemy = "~3.1.1"
|
39 |
+
gevent = "~24.11.1"
|
40 |
+
gmpy2 = "~2.2.1"
|
41 |
+
google-ai-generativelanguage = "0.6.9"
|
42 |
+
google-api-core = "2.18.0"
|
43 |
+
google-api-python-client = "2.90.0"
|
44 |
+
google-auth = "2.29.0"
|
45 |
+
google-auth-httplib2 = "0.2.0"
|
46 |
+
google-cloud-aiplatform = "1.49.0"
|
47 |
+
google-generativeai = "0.8.1"
|
48 |
+
googleapis-common-protos = "1.63.0"
|
49 |
+
gunicorn = "~23.0.0"
|
50 |
+
httpx = { version = "~0.27.0", extras = ["socks"] }
|
51 |
+
huggingface-hub = "~0.16.4"
|
52 |
+
jieba = "0.42.1"
|
53 |
+
langfuse = "~2.51.3"
|
54 |
+
langsmith = "~0.1.77"
|
55 |
+
mailchimp-transactional = "~1.0.50"
|
56 |
+
markdown = "~3.5.1"
|
57 |
+
nomic = "~3.1.2"
|
58 |
+
novita-client = "~0.5.7"
|
59 |
+
numpy = "~1.26.4"
|
60 |
+
oci = "~2.135.1"
|
61 |
+
openai = "~1.61.0"
|
62 |
+
openpyxl = "~3.1.5"
|
63 |
+
opik = "~1.3.4"
|
64 |
+
pandas = { version = "~2.2.2", extras = ["performance", "excel"] }
|
65 |
+
pandas-stubs = "~2.2.3.241009"
|
66 |
+
psycogreen = "~1.0.2"
|
67 |
+
psycopg2-binary = "~2.9.6"
|
68 |
+
pycryptodome = "3.19.1"
|
69 |
+
pydantic = "~2.9.2"
|
70 |
+
pydantic-settings = "~2.6.0"
|
71 |
+
pydantic_extra_types = "~2.9.0"
|
72 |
+
pyjwt = "~2.8.0"
|
73 |
+
pypdfium2 = "~4.30.0"
|
74 |
+
python = ">=3.11,<3.13"
|
75 |
+
python-docx = "~1.1.0"
|
76 |
+
python-dotenv = "1.0.1"
|
77 |
+
pyyaml = "~6.0.1"
|
78 |
+
readabilipy = "0.2.0"
|
79 |
+
redis = { version = "~5.0.3", extras = ["hiredis"] }
|
80 |
+
replicate = "~0.22.0"
|
81 |
+
resend = "~0.7.0"
|
82 |
+
sagemaker = "~2.231.0"
|
83 |
+
scikit-learn = "~1.5.1"
|
84 |
+
sentry-sdk = { version = "~1.44.1", extras = ["flask"] }
|
85 |
+
sqlalchemy = "~2.0.29"
|
86 |
+
starlette = "0.41.0"
|
87 |
+
tencentcloud-sdk-python-hunyuan = "~3.0.1294"
|
88 |
+
tiktoken = "~0.8.0"
|
89 |
+
tokenizers = "~0.15.0"
|
90 |
+
transformers = "~4.35.0"
|
91 |
+
unstructured = { version = "~0.16.1", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"] }
|
92 |
+
validators = "0.21.0"
|
93 |
+
volcengine-python-sdk = {extras = ["ark"], version = "~1.0.98"}
|
94 |
+
websocket-client = "~1.7.0"
|
95 |
+
xinference-client = "0.15.2"
|
96 |
+
yarl = "~1.18.3"
|
97 |
+
youtube-transcript-api = "~0.6.2"
|
98 |
+
zhipuai = "~2.1.5"
|
99 |
+
# Before adding new dependency, consider place it in alphabet order (a-z) and suitable group.
|
100 |
+
|
101 |
+
############################################################
|
102 |
+
# [ Indirect ] dependency group
|
103 |
+
# Related transparent dependencies with pinned version
|
104 |
+
# required by main implementations
|
105 |
+
############################################################
|
106 |
+
[tool.poetry.group.indirect.dependencies]
|
107 |
+
kaleido = "0.2.1"
|
108 |
+
rank-bm25 = "~0.2.2"
|
109 |
+
safetensors = "~0.4.3"
|
110 |
+
|
111 |
+
############################################################
|
112 |
+
# [ Tools ] dependency group
|
113 |
+
############################################################
|
114 |
+
[tool.poetry.group.tools.dependencies]
|
115 |
+
arxiv = "2.1.0"
|
116 |
+
cloudscraper = "1.2.71"
|
117 |
+
duckduckgo-search = "~6.3.0"
|
118 |
+
jsonpath-ng = "1.6.1"
|
119 |
+
matplotlib = "~3.8.2"
|
120 |
+
mplfonts = "~0.0.8"
|
121 |
+
newspaper3k = "0.2.8"
|
122 |
+
nltk = "3.9.1"
|
123 |
+
numexpr = "~2.9.0"
|
124 |
+
pydub = "~0.25.1"
|
125 |
+
qrcode = "~7.4.2"
|
126 |
+
twilio = "~9.0.4"
|
127 |
+
vanna = { version = "0.7.5", extras = ["postgres", "mysql", "clickhouse", "duckdb", "oracle"] }
|
128 |
+
wikipedia = "1.4.0"
|
129 |
+
yfinance = "~0.2.40"
|
130 |
+
|
131 |
+
############################################################
|
132 |
+
# [ Storage ] dependency group
|
133 |
+
# Required for storage clients
|
134 |
+
############################################################
|
135 |
+
[tool.poetry.group.storage.dependencies]
|
136 |
+
azure-storage-blob = "12.13.0"
|
137 |
+
bce-python-sdk = "~0.9.23"
|
138 |
+
cos-python-sdk-v5 = "1.9.30"
|
139 |
+
esdk-obs-python = "3.24.6.1"
|
140 |
+
google-cloud-storage = "2.16.0"
|
141 |
+
opendal = "~0.45.12"
|
142 |
+
oss2 = "2.18.5"
|
143 |
+
supabase = "~2.8.1"
|
144 |
+
tos = "~2.7.1"
|
145 |
+
|
146 |
+
############################################################
|
147 |
+
# [ VDB ] dependency group
|
148 |
+
# Required by vector store clients
|
149 |
+
############################################################
|
150 |
+
[tool.poetry.group.vdb.dependencies]
|
151 |
+
alibabacloud_gpdb20160503 = "~3.8.0"
|
152 |
+
alibabacloud_tea_openapi = "~0.3.9"
|
153 |
+
chromadb = "0.5.20"
|
154 |
+
clickhouse-connect = "~0.7.16"
|
155 |
+
couchbase = "~4.3.0"
|
156 |
+
elasticsearch = "8.14.0"
|
157 |
+
opensearch-py = "2.4.0"
|
158 |
+
oracledb = "~2.2.1"
|
159 |
+
pgvecto-rs = { version = "~0.2.1", extras = ['sqlalchemy'] }
|
160 |
+
pgvector = "0.2.5"
|
161 |
+
pymilvus = "~2.5.0"
|
162 |
+
pymochow = "1.3.1"
|
163 |
+
pyobvector = "~0.1.6"
|
164 |
+
qdrant-client = "1.7.3"
|
165 |
+
tcvectordb = "1.3.2"
|
166 |
+
tidb-vector = "0.0.9"
|
167 |
+
upstash-vector = "0.6.0"
|
168 |
+
volcengine-compat = "~1.0.156"
|
169 |
+
weaviate-client = "~3.21.0"
|
170 |
+
|
171 |
+
############################################################
|
172 |
+
# [ Dev ] dependency group
|
173 |
+
# Required for development and running tests
|
174 |
+
############################################################
|
175 |
+
[tool.poetry.group.dev]
|
176 |
+
optional = true
|
177 |
+
[tool.poetry.group.dev.dependencies]
|
178 |
+
coverage = "~7.2.4"
|
179 |
+
faker = "~32.1.0"
|
180 |
+
mypy = "~1.13.0"
|
181 |
+
pytest = "~8.3.2"
|
182 |
+
pytest-benchmark = "~4.0.0"
|
183 |
+
pytest-env = "~1.1.3"
|
184 |
+
pytest-mock = "~3.14.0"
|
185 |
+
types-beautifulsoup4 = "~4.12.0.20241020"
|
186 |
+
types-flask-cors = "~5.0.0.20240902"
|
187 |
+
types-flask-migrate = "~4.1.0.20250112"
|
188 |
+
types-html5lib = "~1.1.11.20241018"
|
189 |
+
types-openpyxl = "~3.1.5.20241225"
|
190 |
+
types-protobuf = "~5.29.1.20241207"
|
191 |
+
types-psutil = "~6.1.0.20241221"
|
192 |
+
types-psycopg2 = "~2.9.21.20250121"
|
193 |
+
types-python-dateutil = "~2.9.0.20241206"
|
194 |
+
types-pytz = "~2024.2.0.20241221"
|
195 |
+
types-pyyaml = "~6.0.12.20241230"
|
196 |
+
types-regex = "~2024.11.6.20241221"
|
197 |
+
types-requests = "~2.32.0.20241016"
|
198 |
+
types-six = "~1.17.0.20241205"
|
199 |
+
types-tqdm = "~4.67.0.20241221"
|
200 |
+
|
201 |
+
############################################################
|
202 |
+
# [ Lint ] dependency group
|
203 |
+
# Required for code style linting
|
204 |
+
############################################################
|
205 |
+
[tool.poetry.group.lint]
|
206 |
+
optional = true
|
207 |
+
[tool.poetry.group.lint.dependencies]
|
208 |
+
dotenv-linter = "~0.5.0"
|
209 |
+
ruff = "~0.9.2"
|
api/pytest.ini
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[pytest]
|
2 |
+
env =
|
3 |
+
ANTHROPIC_API_KEY = sk-ant-api11-IamNotARealKeyJustForMockTestKawaiiiiiiiiii-NotBaka-ASkksz
|
4 |
+
AZURE_OPENAI_API_BASE = https://difyai-openai.openai.azure.com
|
5 |
+
AZURE_OPENAI_API_KEY = xxxxb1707exxxxxxxxxxaaxxxxxf94
|
6 |
+
CHATGLM_API_BASE = http://a.abc.com:11451
|
7 |
+
CODE_EXECUTION_API_KEY = dify-sandbox
|
8 |
+
CODE_EXECUTION_ENDPOINT = http://127.0.0.1:8194
|
9 |
+
CODE_MAX_STRING_LENGTH = 80000
|
10 |
+
FIRECRAWL_API_KEY = fc-
|
11 |
+
FIREWORKS_API_KEY = fw_aaaaaaaaaaaaaaaaaaaa
|
12 |
+
GOOGLE_API_KEY = abcdefghijklmnopqrstuvwxyz
|
13 |
+
HUGGINGFACE_API_KEY = hf-awuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu
|
14 |
+
HUGGINGFACE_EMBEDDINGS_ENDPOINT_URL = c
|
15 |
+
HUGGINGFACE_TEXT2TEXT_GEN_ENDPOINT_URL = b
|
16 |
+
HUGGINGFACE_TEXT_GEN_ENDPOINT_URL = a
|
17 |
+
MIXEDBREAD_API_KEY = mk-aaaaaaaaaaaaaaaaaaaa
|
18 |
+
MOCK_SWITCH = true
|
19 |
+
NOMIC_API_KEY = nk-aaaaaaaaaaaaaaaaaaaa
|
20 |
+
OPENAI_API_KEY = sk-IamNotARealKeyJustForMockTestKawaiiiiiiiiii
|
21 |
+
TEI_EMBEDDING_SERVER_URL = http://a.abc.com:11451
|
22 |
+
TEI_RERANK_SERVER_URL = http://a.abc.com:11451
|
23 |
+
TEI_API_KEY = ttttttttttttttt
|
24 |
+
UPSTAGE_API_KEY = up-aaaaaaaaaaaaaaaaaaaa
|
25 |
+
VOYAGE_API_KEY = va-aaaaaaaaaaaaaaaaaaaa
|
26 |
+
XINFERENCE_CHAT_MODEL_UID = chat
|
27 |
+
XINFERENCE_EMBEDDINGS_MODEL_UID = embedding
|
28 |
+
XINFERENCE_GENERATION_MODEL_UID = generate
|
29 |
+
XINFERENCE_RERANK_MODEL_UID = rerank
|
30 |
+
XINFERENCE_SERVER_URL = http://a.abc.com:11451
|
31 |
+
GITEE_AI_API_KEY = aaaaaaaaaaaaaaaaaaaa
|