Compare commits

..

338 Commits

Author SHA1 Message Date
8c1bca3119 fix: eslint run failed 2025-02-14 15:01:02 +08:00
a8982a98f4 chore: update libs 2025-02-14 14:13:44 +08:00
130964d9a7 update eslint.config.mjs 2025-02-14 14:00:59 +08:00
1a8a1a9574 fix: ignore .storybook folder 2025-02-08 17:52:10 +08:00
20bcb49932 fix: ignore rule no-explicit-any 2025-02-08 17:50:35 +08:00
91e411bbaa wip: update eslint config and stash 2025-02-08 15:45:16 +08:00
55ce3618ce fix: Dollar Sign Handling in Markdown (#13178)
Co-authored-by: crazywoola <427733928@qq.com>
2025-02-05 11:00:56 +08:00
e9e34c1ab2 Install apt dependencies using bookworm source, consistent with base image. Remove unnecessary, error-prone pins (#13176) 2025-02-05 10:07:22 +08:00
d4c916b496 chore(pyproject): Add type stubs into pyproject.toml (#13145)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-02-04 12:01:28 +08:00
8fbc9c9342 Solve circular dependency issue between workflow/constants.ts file and default.ts file (#13165) 2025-02-04 09:26:01 +08:00
1b6fd9dfe8 fix: set indexing technique from dataset during update-by-text (#13155) 2025-02-03 11:06:03 +08:00
304467e3f5 fix: not install libmagic raise error (#13146) 2025-02-03 11:05:20 +08:00
7452032d81 add azure openai api version 2024-12-01-preview (#13135) 2025-02-03 11:04:20 +08:00
87e2048f1b nitpick: fix small typos in template.en.mdx (#13156) 2025-02-03 11:03:11 +08:00
d876084392 chore: upgrade libldap2 (#13158) 2025-02-03 11:02:14 +08:00
840729afa5 feat: the think tag display of siliconflow's deepseek r1 (#13153) 2025-02-02 21:55:13 +08:00
941ad03f3c pass model and cost so that langfuse can show cost (#13117) 2025-02-02 15:27:27 +08:00
d73d191f99 feature. add feat to modify metadata via dataset api (#13116) 2025-02-02 15:27:12 +08:00
c2664e0283 chore: fix wrong VectorType match case (#13123) 2025-02-02 15:26:59 +08:00
ee61cede4e test(huggingface_hub): Skip the failed test temporarily. (#13142)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-02-02 14:47:26 +08:00
b47669b80b fix: deduct LLM quota after processing invoke result (#13075)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-02-02 12:05:11 +08:00
c0d0c63592 feat: switch to chat messages before regenerated (#11301)
Co-authored-by: zuodongxu <192560071+zuodongxu@users.noreply.github.com>
2025-01-31 13:05:10 +08:00
b09c39c8dc refactor: avoid to use extra space when finding model by name (#13043) 2025-01-30 15:08:29 +08:00
b4b09ddc3c add tongyi qwen2.5-14b/7b-instruct-1m model (#13089) 2025-01-29 11:58:01 +08:00
d0a21086bd refactor: Update Firecrawl API parameters and default settings (#13082) 2025-01-29 11:21:05 +08:00
d44882c1b5 refactor: reduce duplciate code by inheritance (#13073) 2025-01-28 10:52:01 +08:00
23c68efa2d fix: fix the formatter is not applied on log file (#12704) 2025-01-28 10:49:58 +08:00
560c5de1b7 Fixed Novita AI color and added DeepSeek R1 model (#13074) 2025-01-28 10:38:54 +08:00
5d91dbd000 Set default LOG_LEVEL to INFO for celery workers and beat (#13066)
Co-authored-by: Abdullah AlOsaimi <189027247+osaimi@users.noreply.github.com>
2025-01-27 17:09:41 +08:00
6c31ee36cd fix qwen-vl blocking mode (#13052) 2025-01-27 11:35:23 +08:00
edc29780ed fix: "Model schema not found" error only in agents (#12655) (#12760) 2025-01-27 11:33:13 +08:00
aad7e4dd1c fix:Improve MIME type detection for remote URL uploads using python-magic (#12693) 2025-01-27 11:33:03 +08:00
a6a727e8a4 feat: add inner API to create workspace without requiring email (#13021) 2025-01-26 15:36:56 +08:00
d1fc65fabc fix: adjust iteration node dark style (#13051) 2025-01-26 11:19:41 +08:00
d4be5ef9de Update Novita AI predefined models (#13045) 2025-01-26 09:25:29 +08:00
1374be5a31 fix: Unexpected tag creation when pressing enter during tag conversion (#13041) 2025-01-25 19:30:26 +08:00
b2bbc28580 support bedrock kb: retrieve and generate (#13027) 2025-01-25 17:28:06 +08:00
59b3e672aa feat: add agent thinking content display of deepseek R1 (#12949) 2025-01-24 20:13:42 +08:00
a2f8bce8f5 chore: add Japanese translation: model_providers/bedrock (#13016) 2025-01-24 18:43:33 +08:00
a2b9adb3a2 Change typo in translation (#13004) 2025-01-24 13:48:21 +08:00
28067640b5 fix: wrong zh_Hans translation: Ohio (#13006) 2025-01-24 13:41:20 +08:00
da67916843 feat: add glm-4-air-0111 (#12997)
Co-authored-by: lowell <lowell.hu@zkteco.in>
2025-01-24 10:04:46 +08:00
e54ce479ad Feat/prompt editor dark theme (#12976) 2025-01-23 16:20:00 +08:00
6024d8a42d refactor: Update Firecrawl to use v1 API (#12574)
Co-authored-by: Ademílson Tonato <ademilson.tonato@refurbed.com>
2025-01-23 11:14:48 +08:00
f565f08aa0 fix: get property of string type variable caused page crash (#12969) 2025-01-23 11:02:29 +08:00
fd4afe09f8 fix: tools translate search (#12950)
Co-authored-by: lowell <lowell.hu@zkteco.in>
2025-01-22 19:27:02 +08:00
dd0904f95c feat: add giteeAI risk control identification. (#12946) 2025-01-22 19:26:25 +08:00
4c3076f2a4 feat: add pg vector index (#12338)
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
2025-01-22 17:07:18 +08:00
1e73f63ff8 chore: update version to 0.15.2 in packaging and docker configurations (#12940)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-22 16:40:44 +08:00
d167d5b1be feat(ark): support doubao 1.5 series of models (#12935) 2025-01-22 15:25:57 +08:00
71fa14f791 fix: resolve clipboard.writeText failure under HTTP protocol (#12936) 2025-01-22 15:18:23 +08:00
8dd1873e76 feat: workflow note dark theme (#12932) 2025-01-22 14:22:33 +08:00
f91f5c7401 fix(batch_create_segment_to_index_task): count max_position in memory. (#12929) 2025-01-22 13:39:02 +08:00
c62b7cc679 chore(build): bump poetry from 1.x to 2.x (#12369) 2025-01-22 13:38:24 +08:00
3ee213ddca add milvus full text search setting (#12930) 2025-01-22 13:36:39 +08:00
8429877b02 fix: Agent is configured for ReAct inference mode, an error is reported when viewing the agent log (#12920)
Co-authored-by: crazywoola <427733928@qq.com>
2025-01-22 13:20:32 +08:00
05a0faff6a fix: app token's last_used_at can't be updated when last_used_at is null (#12770) 2025-01-22 11:01:45 +08:00
e09f6e4987 feat: support config chunk length by env (#12925) 2025-01-22 10:43:40 +08:00
e23f4b0265 feat: add gemini-2.0-flash-thinking-exp-01-21 (#12924) 2025-01-22 10:14:37 +08:00
f582d4a13e feat: Add ability to change profile avatar (#12642) 2025-01-22 10:11:31 +08:00
2f41bd495d fix:Fix a bug that returns null when the passed path is a file. (#12775)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-01-22 10:10:03 +08:00
162a8c4393 fix update segment keyword with same content (#12908) 2025-01-21 19:19:32 +08:00
3d1ce4c53f bug: fixed bedrock rerank bug (#12774)
Co-authored-by: hobo.l <hobo.l@binance.com>
2025-01-21 19:09:36 +08:00
6db3ae9b8e chore: remove webapp ga (#12909) 2025-01-21 18:38:33 +08:00
6d0cb9dc33 fix: variable panel scrollable (#12769)
Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com>
2025-01-21 17:50:42 +08:00
46e95e8309 fix: OpenAI o1 Bad Request Error (#12839) 2025-01-21 15:29:13 +08:00
a7b9375877 Update deepseek model configuration (#12899) 2025-01-21 15:28:11 +08:00
0c6a8a130e fix: external dataset hit test display issue(#12564) (#12612)
Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com>
2025-01-21 14:31:45 +08:00
9903f1e703 add deepseek-reasoner (#12898) 2025-01-21 12:40:58 +08:00
6fad719e42 chore(fix): Invalid quotes for using Array[String] in HTTP request node as JSON body (#12761) 2025-01-21 10:38:44 +08:00
9aaee8ee47 fix: Issues related to the deletion of conversation_id (#12488) (#12665) 2025-01-21 10:25:35 +08:00
166221d784 chore(lint): fix quotes for f-string formatting by bumping ruff to 0.9.x (#12702) 2025-01-21 10:12:29 +08:00
925d69a2ee feat:Support Minimax-Text-01 (#12763) 2025-01-21 10:08:53 +08:00
5ff08e241a fix: serply credential check query might return empty records (#12784) 2025-01-21 09:38:56 +08:00
3defd24087 feat: allow updating chunk settings for the existing documents (#12833) 2025-01-21 09:25:40 +08:00
9d86147d20 fix: SparkLite API Auth error (#12781) (#12790) 2025-01-20 22:21:21 +08:00
80801ac4ab fix: "parmas" spelling mistake. (#12875) 2025-01-20 22:18:30 +08:00
210926cd91 Fix suggested_question_prompt (#12738) 2025-01-20 22:16:30 +08:00
677a69deed fix(i18n): correct typo in zh-Hant translation (#12852) 2025-01-20 22:15:41 +08:00
8dfdee21ce chore: fix chinese translation for 'recall' (#12772)
Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com>
2025-01-20 22:15:26 +08:00
6ea77ab4cd fix: DeepSeek API Error with response format active (text and json_object) (#12747) 2025-01-20 22:04:18 +08:00
e3c996688d feat: enhance credential extraction logic based on configurate method (#12853) 2025-01-20 21:59:22 +08:00
bc3a570dda fix: Fix rerank model switching issue (#12721)
ok
2025-01-14 15:42:45 +08:00
0800021a2d chore: translate i18n files (#12708)
Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com>
2025-01-14 13:35:23 +08:00
435eddd867 Feat: copyright modification (#12707) 2025-01-14 10:00:57 +08:00
6e0fb055d1 chore: bump version to 0.15.1 (#12690)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-13 19:21:06 +08:00
eux
1e9ac7ffeb feat: add table of contents to Knowledge API doc (#12688) 2025-01-13 18:31:43 +08:00
b4873ecb43 [fix] support feature restore (#12563) 2025-01-13 18:29:06 +08:00
mbo
1859d57784 api tool support multiple env url (#12249)
Co-authored-by: mabo <mabo@aeyes.ai>
2025-01-13 17:49:30 +08:00
69d58fbb50 Add new integration with Opik Tracking tool (#11501) 2025-01-13 17:41:44 +08:00
cb34991663 fix: add type hints for App model and improve error handling in audio services (#12677)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-13 15:55:16 +08:00
c700364e1c fix: Update variable handling in VariableAssignerNode and clean up app_dsl_service (#12672)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-13 15:54:26 +08:00
9a6b1dc3a1 Revert "Feat/new saas billing" (#12673) 2025-01-13 15:17:43 +08:00
54b5b80a07 fix(workflow): fix answer node stream processing in conditional branches (#12510) 2025-01-13 14:54:21 +08:00
831459b895 fix: ruff with statements (#12578)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-01-13 09:55:55 +08:00
4e101604c3 fix: ruff check for True if ... else (#12576)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2025-01-13 09:38:48 +08:00
a6455269f0 chore: Adjust translations to align with Taiwanese Mandarin conventions (#12633) 2025-01-13 09:12:43 +08:00
cd257b91c5 Fix pandas indexing method for knowledge base imports (#12637) (#12638)
Co-authored-by: CN-P5 <heibai2006@qq.com>
2025-01-13 09:06:59 +08:00
d8f57bf899 Feat/new saas billing (#12591) 2025-01-12 14:50:46 +08:00
989fb11fd7 improve the readability of the function generate_api_key (#12552) 2025-01-09 21:30:17 +08:00
140965b738 chore: translate i18n files (#12543)
Co-authored-by: WTW0313 <30284043+WTW0313@users.noreply.github.com>
2025-01-09 20:30:06 +08:00
14ee51aead Feat/add knowledge include all filter (#12537) 2025-01-09 20:21:25 +08:00
2e97ba5700 fix: Add datasets list access control and fix datasets config display issue (#12533)
Co-authored-by: nite-knite <nkCoding@gmail.com>
2025-01-09 17:44:11 +08:00
f549d53b68 fix: sum costs return error value on overview page (#12534) 2025-01-09 16:04:14 +08:00
a085ad4719 feat: show workflow running status (#12531) 2025-01-09 15:36:13 +08:00
f230a9232e fix: Parsing OpenAPI spec for external tools (#12518) (#12530) 2025-01-09 15:30:43 +08:00
e84bf35e2a fix: same chunk insert deadlock (#12502)
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
2025-01-09 15:16:41 +08:00
eux
20f090537f feat: add GET upload file API endpoint to dataset service api (#11899) 2025-01-09 14:52:09 +08:00
dbe7a7c4fd Fix: Add a INFO-level log when fallback to gpt2tokenizer (#12508) 2025-01-09 14:37:46 +08:00
b7a4e3903e fix: add last_refresh_time to track the validity of is_other_tab_refreshing (#12517) 2025-01-09 10:40:45 +08:00
b4c1c2f731 fix: Reverse sync docker-compose-template.yaml (#12509) 2025-01-09 10:21:22 +08:00
1b940e7daa feat: add ci job to test template for docker compose (#12514) 2025-01-09 00:04:58 +08:00
f4ee50a7ad chore: improve app doc (#12490) 2025-01-08 18:37:12 +08:00
bee32d960a fix #12453 #12482 (#12495) 2025-01-08 18:26:05 +08:00
040a3b782c FEAT: support milvus to full text search (#11430)
Signed-off-by: YoungLH <974840768@qq.com>
2025-01-08 17:39:53 +08:00
d649037c3e feat: support single run doc extractor node (#11318) 2025-01-08 15:20:15 +08:00
0a49d3dd52 fix: tiktoken cannot be loaded without internet (#12478)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-08 14:49:44 +08:00
53bb37b749 fix: fix the incorrect plaintext file key when saving (#10429) 2025-01-08 12:52:45 +08:00
d2586278d6 Feat elasticsearch japanese (#12194) 2025-01-08 12:35:41 +08:00
6635c393e9 fix: adjust opacity for model selector based on readonly state (#12472) 2025-01-08 12:11:45 +08:00
6222179a57 Revert "fix:deepseek tool call not working correctly" (#12463) 2025-01-08 10:50:34 +08:00
05bda6f38d add tidb on qdrant redis lock (#12462) 2025-01-08 08:55:44 +08:00
4295cefeb1 fix: allow fallback to remote_url when url is not provided (#12455) 2025-01-07 22:33:25 +08:00
67228c9b26 fix: url with variable not work (#12452) 2025-01-07 21:55:51 +08:00
fd2bfff023 remove knowledge admin role (#12450) 2025-01-07 21:30:23 +08:00
4e6c86341d Add 'document' feature to Sonnet 3.5 through OpenRouter (#12444) 2025-01-07 19:51:38 +08:00
2a14c67edc Fix #12448 - update bedrock retrieve tool, support hybrid search type and re… (#12446)
Co-authored-by: Yuanbo Li <ybalbert@amazon.com>
2025-01-07 19:51:23 +08:00
c236f05f4b chore: bump version to 0.15.0 (#12297)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-07 18:05:14 +08:00
0eeacdc80c refactor: enhance API token validation with session locking and last used timestamp update (#12426)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-07 18:04:41 +08:00
41f39bf3fc Fix newline characters in tables during document parsing (#12112)
Co-authored-by: hisir <admin@qq.com>
2025-01-07 17:26:24 +08:00
9677144015 fix:deepseek tool call not working correctly (#12437) 2025-01-07 17:25:38 +08:00
15797c556f add fish-speech-1.5 from siliconflow (#12425) 2025-01-07 15:27:34 +08:00
acacf35a2a chore(docker/.env.example): Add TOP_K_MAX_VALUE to the .env.example… (#12422)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-07 14:51:16 +08:00
d3f5b1cbb6 refactor: use tiktoken for token calculation (#12416)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-07 13:32:30 +08:00
why
196ed8101b fix: [PromptEditorHeightResizeWrap] Bug #12410 (#12406) 2025-01-07 12:21:54 +08:00
dc650c5368 Fixes #12414: Add cheaper model and long context model for Qwen2.5-72B-Instruct from siliconflow (#12415) 2025-01-07 11:28:24 +08:00
2bb521b135 Support TTS and Speech2Text for Model Provider GPUStack (#12381) 2025-01-07 09:42:11 +08:00
409cc7d9b0 mark deprecated models in siliconflow #12399 (#12405)
Co-authored-by: crazywoola <427733928@qq.com>
2025-01-07 09:08:58 +08:00
fe26be2312 fix: http method can be upper case and lower case close #11877 (#12401)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2025-01-06 20:35:53 +08:00
34519de3b7 fix: enhance chunk list management with new invalidation keys and imp… (#12396)
ok
2025-01-06 18:22:16 +08:00
147d578922 [Fix] revert sagemaker llm to support model hub (#12378) 2025-01-06 18:01:45 +08:00
9c317b64c3 sandbox doesn't provide auto disable log (#12388) 2025-01-06 15:57:13 +08:00
3b8f6233b0 feat: support config top max value by env (#12375) 2025-01-06 10:38:14 +08:00
455b0cd696 chore: chat app textarea auto focus (#12366) 2025-01-05 21:25:00 +08:00
eux
1fa66405c5 feat: support configuration of refresh token expiration by environment variable (#12335) 2025-01-04 11:56:44 +08:00
b680a85b57 fix: resolve issue with the opening statement generated by the AutomaticRes component failing to sync between states. (#12349) 2025-01-04 11:56:11 +08:00
682ebc5f64 Fix the issue where TextGeneration component does not correctly clear input data. (#12351) 2025-01-04 11:55:55 +08:00
b8ba39dfae Bugfix/style and i18n fixes (#12350) 2025-01-04 11:52:13 +08:00
6c9e6a3a5a fix: fix issue with chat-input-area clearing during Responding state. (#12352) 2025-01-04 11:51:35 +08:00
70698024f5 fix: empty delete bug (#12339)
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
2025-01-03 20:46:39 +08:00
6df17a334c fix: Update the API call address for the text_embedding model (#12342)
Co-authored-by: 方程 <fangcheng@oschina.cn>
2025-01-03 19:19:17 +08:00
a5fb59b17f fix: Encode Chinese characters with Unicode before querying to match the Unicode encoded Chinese characters in the db (#12337)
Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com>
2025-01-03 19:12:48 +08:00
7ed6485f86 refactor: streamline initialization of application_generate_entity and task_state in task pipeline classes (#12326)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-03 18:41:44 +08:00
478150e850 chore: fix typo in zh-Hant localization (#12329) 2025-01-03 17:38:30 +08:00
3c2e30f348 fix: #12143 support streaming mode content start with "data:" (#12171) 2025-01-03 16:33:37 +08:00
b873e6349c add child chunk preview number limit (#12309) 2025-01-03 16:14:27 +08:00
2b1a32fd9c feat: Add filter to show only apps created by the user (#11968) 2025-01-03 15:38:36 +08:00
a2105634a4 chore: change app card layout follow by #10847 (#12317) 2025-01-03 14:16:17 +08:00
7c71bd7be7 doc: Added explanation of chunk_overlap to knowledge API (#12247)
Co-authored-by: crazywoola <427733928@qq.com>
2025-01-03 10:02:17 +08:00
7c1961e618 feat: Add response format support to GLM-4 (#12252) 2025-01-03 09:38:50 +08:00
baeddd4d15 feat:Add support for stop parameter in hunyuan model #12313 (#12315)
Co-authored-by: xander-art <xander-art@gmail.com>
2025-01-03 09:15:04 +08:00
6f5a8a33d9 refactor: replace gevent threadpool with ProcessPoolExecutor in GPT2Tokenizer (#12316)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-03 09:13:18 +08:00
52b2559a14 fix(app.py): if condition (#12314)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-03 01:36:23 +08:00
3d150c30a7 fix: utcfromtimestamp is Deprecated change to new api (#12120)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2025-01-02 18:52:36 +08:00
e58e573f3e fix: add full doc mode preview length limit (#12310)
ok
2025-01-02 18:36:49 +08:00
375aa38f5d fix: improve content decoding in AppDslService (#12304)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-02 16:54:46 +08:00
0e6317678f Fix code scanning alert no. 111: Incomplete URL substring sanitization (#12305)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-01-02 16:52:43 +08:00
e7dffcd0f6 chore(deps): bump aiohttp from 3.10.5 to 3.10.11 in /api (#12303)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-02 16:42:47 +08:00
065304d175 Modify jp translation (#12292) 2025-01-02 16:28:27 +08:00
15f43dd326 chore(deps): update yarl version from 1.9.4 to 1.18.3 (#12302)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-02 16:26:47 +08:00
09d759d196 fix: Fix parent child retrieval issues (#12206)
Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
2025-01-02 16:07:21 +08:00
68757950ce chore(deps): bump jinja2 from 3.1.4 to 3.1.5 in /api (#12300)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-02 16:04:10 +08:00
3c45bdf18a fix: disable gevent in debug mode for better compatibility with JetBrains Python debugger (#12299)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-02 15:59:30 +08:00
c135967e59 refactor: simplify some exception catch code (#12246) 2025-01-02 14:25:12 +08:00
f71af7c2a8 fix: DocumentAddByFileApi miss data_source_type field but there is a mandatory value check (#12273) 2025-01-02 14:24:15 +08:00
5b01eb9437 chore: translate i18n files (#12208)
Co-authored-by: douxc <7553076+douxc@users.noreply.github.com>
2025-01-02 10:04:43 +08:00
2e716f80d2 fix:The chart of average interaction counts per conversation show not… (#12199) 2025-01-02 10:02:56 +08:00
d7c0bc8c23 feat: Add response format support for openai compat models (#12240)
Co-authored-by: Gio Gutierrez <giovannygutierrez@gmail.com>
2025-01-02 09:59:34 +08:00
f30bf08580 fix: close #12215 for yi special case (#12222)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2025-01-02 09:58:34 +08:00
a640803fc9 fix(models): use bigint on workflow_runs.total_tokens (#12279)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-01-02 09:49:34 +08:00
9954ddb780 [Fix] modify sagemaker llm (#12274) 2025-01-02 09:49:11 +08:00
b218df6920 fix: draft run single node can't get env variable (#12266) 2025-01-01 13:31:44 +08:00
5b6950e545 fix: improve error handling in NotionOAuth for block parent page ID r… (#12268)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 17:03:26 +08:00
c7911c7130 fix: improve JSON parsing error handling in Executor class (#12265)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 17:03:07 +08:00
62f792ea14 fix: ensure workflow_run_id is always set and improve handling in Wor… (#12264)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 17:02:44 +08:00
6a85960605 feat: implement asynchronous token counting in GPT2Tokenizer (#12239)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 17:02:08 +08:00
63a0b8ba79 feat: integrate psycogreen for gevent compatibility in PostgreSQL (#12253)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 14:45:59 +08:00
634b382a3d fix: enhance ToolEngineInvokeError to include meta information (#12238)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 14:01:24 +08:00
fbf5deda21 fix(env): docker compose variable interpolation issue for COMPOSE_PRO… (#12093)
Co-authored-by: Han Kyaw <hankyaw@Hans-MBP.lan>
2024-12-31 13:46:51 +08:00
d4b848272e fix: apply gevent threading patch early and ensure unique workflow node execution IDs (#12196)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-31 11:42:51 +08:00
fc29f2003e translate comments (#12234) 2024-12-31 00:36:03 +08:00
ab469aa07d feat: support opening new tab in markdown button (#12213) 2024-12-30 22:27:25 +08:00
562450751f [Fix] Fix sagemaker_chinese_toxicity_detector and bedrock_retrieve (#12227) 2024-12-30 22:26:04 +08:00
adacd01f82 Feat: support account deletion (#10008) 2024-12-30 13:39:26 +08:00
74d3320519 feat: account delete (#11829)
Co-authored-by: NFish <douxc512@gmail.com>
2024-12-30 11:33:42 +08:00
309a15d1ba fix: update api libldap package version in Dockerfile for security im… (#12195) 2024-12-29 21:36:07 +08:00
bcef11681d fix: default value of google storage sa to empty (#12188) 2024-12-29 17:32:55 +08:00
8d15c8cfbf fix: improve error handling in NotionExtractor data fetching (#12182)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-29 11:53:09 +08:00
716bb8574d fix: rendering errors occur when displaying hit test results in full text search (#12191) 2024-12-29 11:52:45 +08:00
bd2fec4813 fix: #12125 (#12184) 2024-12-29 11:52:12 +08:00
ead4b34127 fix: test run custom tool raise error (#12178) 2024-12-28 17:51:22 +08:00
72ae414da4 chore(lint): correct allowed-unused-imports settings for cleanup unused imports in tests (#11922) 2024-12-28 01:19:31 +08:00
4c9618be3f [fix] modify en-US (#12169) 2024-12-27 21:18:34 +08:00
901028f1e8 [feat] Support Multi-Version Workflows (#11990)
Co-authored-by: hobo.l <hobo.l@binance.com>
Co-authored-by: crazywoola <427733928@qq.com>
2024-12-27 21:05:06 +08:00
adfbfc1255 Modify translation for error branch and update for the parent-child f… (#12127) 2024-12-27 20:55:33 +08:00
b66c03dfe9 fix: workflow_as_tool output files raise error (#12061) 2024-12-27 20:33:28 +08:00
2a909e634b feat: support Ernie-lite-pro-128k (#12161)
Co-authored-by: bigfish49 <bigfish49@126.com>
2024-12-27 20:23:46 +08:00
9d86056f1c chore: add cursor pointer for option card (#12119) 2024-12-27 20:20:25 +08:00
309fd76ddf fix: comfyui output image's format (#12121) 2024-12-27 20:20:03 +08:00
a3293b154e fix: type is wrong issue (#12165)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-27 18:33:53 +08:00
eb8963a673 fix: workflow page throw warning: Attempts to access this ref will fail (#12166) 2024-12-27 18:33:15 +08:00
89ce9a5db2 Fix: avatar dropdown keyboard navigation (#12155) 2024-12-27 18:10:36 +08:00
f4f2567105 owner and admin have all permission of knowledge base (#12157) 2024-12-27 17:09:13 +08:00
5a3fe61f2a disable all chunks status when disable document (#12149) 2024-12-27 17:08:44 +08:00
55c327ffcb fix: handle case where member is not found in role update API (#12156)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-27 16:04:50 +08:00
0fdb39f1c3 Fix: The topk parameter doesn't work in sagemaker rerank tool (#12150)
Co-authored-by: Yuanbo Li <ybalbert@amazon.com>
2024-12-27 14:42:25 +08:00
dae1b5a619 fix: import jieba.analyse (#12133)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-27 11:37:55 +08:00
26b5680913 fix: improve merge branch node ID checks in graph engine (#12128)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-26 23:33:45 +08:00
a2855fa24a chore: translate i18n files (#12118)
Co-authored-by: laipz8200 <16485841+laipz8200@users.noreply.github.com>
2024-12-26 23:33:11 +08:00
9c3cf7b69a fix: 12050 (#12109)
Co-authored-by: Xiaoba Yu <xb1823725853@gmail.com>
2024-12-26 23:32:54 +08:00
be7877f526 fix: enhance file upload error handling and update base error class (#12132)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-26 23:31:29 +08:00
e765d8e69e fix: validate imported_version type in AppDslService (#12135)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-26 23:26:55 +08:00
4bd8df1fd3 fix: update MessageService.create_feedback to use keyword arguments f… (#12134)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-26 23:26:44 +08:00
4e76f2fc44 fix: add properties to retrieve created by account and end user in Wo… (#12129)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-26 19:50:45 +08:00
cf00ee42f5 send knowledge base auto disable notification (#12126) 2024-12-26 18:14:08 +08:00
886758d2be fix: typo in clean messages periodical task's logging (#12090) 2024-12-26 17:42:32 +08:00
8339d2c7c9 fix: issue #11868 bring old logic back (#12100)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-26 12:17:55 +08:00
811e4bd0cf fix unstructured setting (#12116) 2024-12-26 12:08:36 +08:00
49feff082f feat: parent child retrieval (#12106)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-12-26 12:01:51 +08:00
efdd54a670 fix: issue #12068 by test is answer in the ids (#12105)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-26 10:31:05 +08:00
84ac004772 py lint (#12102)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-12-26 00:16:35 +08:00
bb35818976 fix: issue #12078 by redirect the right (#12088)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-25 21:36:41 +08:00
822af70dce fix(workflow_service): assign UUID to workflow_node_execution id and update optional fields in WorkflowRun and WorkflowNodeExecution models (#12096)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 21:34:53 +08:00
d5f33212ac fix: add type ignore comments for shared_task imports and clean up re… (#12099)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 21:24:06 +08:00
db2aa83a7c Revert "Feat/parent child retrieval" (#12095) 2024-12-25 20:55:44 +08:00
9231fdbf4c Feat/support parent child chunk (#12092) 2024-12-25 19:49:07 +08:00
017d7538ae fix(billing_service): change retry condition to handle specific reque… (#12091)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
2024-12-25 18:40:27 +08:00
a1c78ad9d4 fix(audio_service): validate message_id format using UUID (#12087)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 18:36:52 +08:00
754791efd3 fix(file_factory): validate upload_file_id format as UUID (#12084)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 18:36:42 +08:00
50b7ec3c73 fix(http_request): add error handling for invalid URLs (#12082)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 18:36:33 +08:00
7ae417b4b6 fix(tool): validate return type from _invoke method to ensure ToolInv… (#12079)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 18:36:24 +08:00
5d04638091 feat(message_feedback): add content argument to feedback creation (#12077)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 18:36:15 +08:00
2b2263a349 Feat/parent child retrieval (#12086)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: AkaraChen <akarachen@outlook.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Warren Chen <warren.chen830@gmail.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com>
Co-authored-by: Charlie.Wei <luowei@cvte.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: huayaoyue6 <huayaoyue@163.com>
Co-authored-by: kurokobo <kuro664@gmail.com>
Co-authored-by: Matsuda <yiyth.fcb6@gmail.com>
Co-authored-by: shirochan <s.yusuke0711@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: Huỳnh Gia Bôi <boihuynh147@gmail.com>
Co-authored-by: Julian Huynh <julian.huynh@immersio.io>
Co-authored-by: Hash Brown <hi@xzd.me>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Kazuki Takamatsu <kazuki.takamatsu@chowagiken.co.jp>
Co-authored-by: Trey Dong <1346650911@qq.com>
Co-authored-by: VoidIsVoid <343750470@qq.com>
Co-authored-by: Gimling <huangjl@ruyi.ai>
Co-authored-by: xiandan-erizo <xiandan.erizo@gmail.com>
Co-authored-by: Muneyuki Noguchi <nogu.dev@gmail.com>
Co-authored-by: zhaobingshuang <1475195565@qq.com>
Co-authored-by: zhaobs <zhaobs@cailian.net>
Co-authored-by: suzuki.sh <s2terminal@users.noreply.github.com>
Co-authored-by: Yingchun Lai <laiyingchun@apache.org>
Co-authored-by: huanshare <huanshare@live.com>
Co-authored-by: huanshare <liuhuan101@longfor.com>
Co-authored-by: orangeclk <orangeclk@users.noreply.github.com>
Co-authored-by: 문정현 <120004247+JungHyunMoon@users.noreply.github.com>
Co-authored-by: barabicu <kztk533@gmail.com>
Co-authored-by: Wei Mingzhi <whistler_wmz@users.sf.net>
Co-authored-by: Paul van Oorschot <20116814+pvoo@users.noreply.github.com>
Co-authored-by: zkyTech <zhangkunyuan@hotmail.com>
Co-authored-by: zhangkunyuan <zhangkunyuan@cmhi.chinamobile.com>
Co-authored-by: Tommy <34446820+Asterovim@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Novice <857526207@qq.com>
Co-authored-by: Novice Lee <novicelee@NovicedeMacBook-Pro.local>
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
Co-authored-by: zxhlyh <16177003+zxhlyh@users.noreply.github.com>
Co-authored-by: liuzhenghua <1090179900@qq.com>
Co-authored-by: Jiang <65766008+AlwaysBluer@users.noreply.github.com>
Co-authored-by: jiangzhijie <jiangzhijie.jzj@alibaba-inc.com>
Co-authored-by: Joe <79627742+ZhouhaoJiang@users.noreply.github.com>
Co-authored-by: Alok Shrivastwa <alok.shrivastwa@gmail.com>
Co-authored-by: Alok Shrivastwa <Alok.Shrivastwa@microland.com>
Co-authored-by: JasonVV <jasonwangiii@outlook.com>
Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com>
Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com>
Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: Junyan Qin <1010553892@qq.com>
Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com>
Co-authored-by: IWAI, Masaharu <iwai_masaharu@funkit.co.jp>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: luckylhb90 <luckylhb90@gmail.com>
Co-authored-by: hobo.l <hobo.l@binance.com>
Co-authored-by: douxc <7553076+douxc@users.noreply.github.com>
2024-12-25 18:17:15 +08:00
39ace9bdee fix(app_generator): improve error handling for closed file I/O operat… (#12073)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 16:34:38 +08:00
1885d3df99 fix: unquote urls in docker-compose.yaml (#12072)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-12-25 16:31:01 +08:00
83ea931e3c refactor: optimize database usage (#12071)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-25 16:24:52 +08:00
b281a80150 fix: zoom in/out click (#12056)
Co-authored-by: marvin <sea-son@foxmail.com>
2024-12-25 13:30:51 +08:00
c98d91e44d fix: o1 model error, use max_completion_tokens instead of max_tokens. (#12037)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2024-12-25 13:29:43 +08:00
3ea54e9d25 fix: update S3 and Azure configuration typos in .env.example and corr… (#12055) 2024-12-25 11:00:45 +08:00
1d3f218662 fix: like failed close #12057 (#12058)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-25 10:57:52 +08:00
7da4fb68da fix: can not find model bug (#12051)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-25 08:42:52 +08:00
7a24c957bd fix: i18n error (#12052)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-24 23:14:51 +08:00
0ea6a926c5 fix: tool can not run (#12054)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-24 23:14:32 +08:00
eux
49bc602fb2 fix: --name option for the create-tenant command does not take effect (#11993) 2024-12-24 21:58:05 +08:00
cdaef30cc9 refactor: replace div with button for better accessibility (#12046) 2024-12-24 19:13:24 +08:00
56e15d09a9 feat: mypy for all type check (#10921) 2024-12-24 18:38:51 +08:00
c91e8b1737 fix: modal bg color (#12042) 2024-12-24 16:26:47 +08:00
094343739b fix/array file cannot use in iteration node (#12035)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-24 15:56:59 +08:00
82134a1d50 fix: Replace generic exceptions with specific error classes in task p… (#12036)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-24 15:24:19 +08:00
6a0ff3686c fix: fix typo (#12034)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-24 15:23:27 +08:00
e0c24c0e99 fix: Fix session typo in workflow_trace method (#12031) 2024-12-24 10:30:31 +08:00
1c80941c69 fix: add FileNotFoundError to ignored errors in Sentry integration (#12023)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 22:36:44 +08:00
e88ea71aef chore/bump version to 0.14.2 (#12017)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 19:15:48 +08:00
e0f1410b48 fix: issue Multiple Paths Between IF/ELSE Branches (#11646)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-23 18:56:59 +08:00
c3c85276d1 Fix/refactor invoke result handling in question classifier node (#12015)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 17:54:08 +08:00
af2888d394 fix: remove json_schema if response format is disabled. (#12014)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 17:53:57 +08:00
d0dd8b7955 fix: add UUID validation for tool file ID extraction (#12011)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 17:53:42 +08:00
75bce2822e fix: add logging for missing edge mapping in StreamProcessor (#12008)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 17:53:36 +08:00
425cc1ea85 Fix/add retry mechanism to billing service request handling (#12006)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 17:53:25 +08:00
2bf33c4dd2 Fix/workflow retry log (#12013) 2024-12-23 17:18:09 +08:00
dc19cd5d9d fix: add retry feature to code node (#12005)
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
2024-12-23 16:42:28 +08:00
dfc25dbdd0 fix: drop useless and wrong code in Account (#11961)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-23 16:30:04 +08:00
c4091c4c66 fix: improve error handling for file retrieval in AwsS3Storage (#12002)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 16:28:54 +08:00
ef95b1268e Fix/workflow retry (#11999) 2024-12-23 15:55:50 +08:00
e068bbec73 feat: add RequestBodyError for invalid request body handling (#11994)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 15:53:03 +08:00
9cfd1c67b6 fix: Introduce ArrayVariable and update iteration node to handle it (#12001)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 15:52:50 +08:00
8978a6a3ff fix: remove unused credential validation logic in VectorizerProvider (#12000)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 15:30:58 +08:00
4e3d732934 feat: Warning on invite modal when mail setup is incomplete (#11809) 2024-12-23 15:27:49 +08:00
03548cdfbc fix: handle broader request exceptions in OAuth process (#11997)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-23 15:23:11 +08:00
70dd69d533 fix: Multiple Paths Between IF/ELSE Branches Invalidate Conditions (#11544)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-23 15:09:02 +08:00
453f324f54 fix: retry node in iteration logs wrong (#11995)
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
2024-12-23 15:06:01 +08:00
c1aa55f3ea fix: remove the unused retry index field (#11903)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-12-23 14:32:11 +08:00
4b1e13e982 Fix 11979 (#11984) 2024-12-23 14:30:04 +08:00
4584eb3058 Add custom to file types (#11966)
Co-authored-by: yagiyuki <yagiyuki>
2024-12-23 13:53:46 +08:00
02a7ae15f9 fix: fix update external dataset error in dataset list (#11989) 2024-12-23 13:53:05 +08:00
74b1b60125 Feat: account page dark mode (#11977) 2024-12-23 11:17:49 +08:00
39df994ff9 fix: create_feedback args are wrong (#11962)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-23 09:20:47 +08:00
d9875fe232 fix(commands): validate name encoding for non-Latin characters (#11965) 2024-12-23 09:20:30 +08:00
26c10b9931 fix: 'dict_keys' object is not subscriptable error (#11957)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-22 14:58:33 +08:00
750662eb08 fix(workflow): update updated_at default to use UTC timezone (#11960)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 14:55:18 +08:00
6b49889041 fix: messagefeedbackapi support content (#11716)
Signed-off-by: weiyang <24080293@smb956101.com>
Co-authored-by: weiyang <24080293@smb956101.com>
2024-12-22 10:45:55 +08:00
03ddee3663 fix(variable_assigner): change VariableOperatorNodeError to inherit from ValueError (#11951)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:43:40 +08:00
10caab1729 fix: change CredentialsValidateFailedError to inherit from ValueError (#11950)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:43:31 +08:00
c6a72def88 fix(ops_trace_manager): handle None workflow_run in workflow_trace method and raise ValueError (#11953)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:42:51 +08:00
21a31d7f8b fix(base_node): change BaseNodeError exception type from Exception to ValueError (#11952)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:42:30 +08:00
2c4df108e5 fix: raise http request node error on httpx.request error (#11954)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:41:53 +08:00
5db8addcc6 fix(core/errors): change base class of custom exceptions to ValueError (#11955)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:41:34 +08:00
dd0e81d094 fix: enhance type hints and improve audio message handling in TTS pub… (#11947)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:41:06 +08:00
90f093eb67 fix(json_in_md_parser): improve error messages for JSON parsing failures (#11948)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:40:56 +08:00
a056a9d601 feat(code_node): add more check (#11949)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:40:43 +08:00
2ad2a402fb fix(app_dsl_service): handle missing app mode with a ValueError (#11945)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:40:12 +08:00
3d07a94bd7 fix: refactor conversation pagination to use SQLAlchemy session manag… (#11956)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-22 10:39:29 +08:00
366857cd26 fix: gemini system prompt with variable raise error (#11946) 2024-12-21 23:14:05 +08:00
9578246bbb fix: The default updated_at when a workflow is created (#11709)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2024-12-21 23:13:58 +08:00
9ee9e9c6de fix: self.method should method in api_tool.py (#11926)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-21 21:24:59 +08:00
e22cc28114 fix:log error(#11942) (#11943) 2024-12-21 21:24:33 +08:00
a227af3664 fix(code_node): update type hints for string and number checks in Cod… (#11936)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:24:22 +08:00
599d410d99 fix: validate reranking model attributes before processing (#11930)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:23:12 +08:00
5e37ab60d8 fix: validate response type in transform_response method (#11931)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:23:03 +08:00
0b06235527 fix: add RemoteFileUploadError for better error handling in remote fi… (#11933)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:22:57 +08:00
b8d42cdea7 fix: change MaxRetriesExceededError to inherit from ValueError (#11934)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:22:47 +08:00
455791b710 fix(model_runtime): make invoke as ValueError (#11929)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:22:14 +08:00
90323cd355 fix(tool_file_manager): raise ValueError when get timeout (#11928)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:22:06 +08:00
c07d9e96ce fix(nodes): handle errors in question_classifier and parameter_extractor (#11927)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:21:57 +08:00
810adb8a94 fix: change OutputParserError to inherit from ValueError (#11935)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:21:30 +08:00
606aadb891 refactor: update builtin tool provider methods to use session management (#11938)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:21:09 +08:00
8f73670925 fix(file_factory): validate upload_file_id before querying UploadFile (#11937)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:21:00 +08:00
8c559d6231 fix(retrieval_service): avoid to use exception (#11925)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-21 21:19:46 +08:00
786cb6859b fix: ensure WorkflowRun attributes are refreshed in WorkflowCycleMana… (#11913) 2024-12-21 15:05:04 +08:00
de8800f41a add three aws tools (#11905)
Co-authored-by: Yuanbo Li <ybalbert@amazon.com>
2024-12-21 13:36:13 +08:00
7a00798027 Update input-var-list.tsx (#9987) 2024-12-21 11:54:12 +08:00
6ded06c6d9 fix: dataset search-input compostion can't work in chrome (#11907)
Co-authored-by: zhaoqingyu.1075 <zhaoqingyu.1075@bytedance.com>
2024-12-21 11:26:17 +08:00
f53741c5b9 revert: these 2 settings (#11906)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2024-12-20 18:24:47 +08:00
2681bafb76 fix: handle document fetching from URL in Anthropic LLM model, solving base64 decoding error (#11858) 2024-12-20 18:23:42 +08:00
ac635c70cd fix: doc can not extract tables (#11879)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Co-authored-by: akinobu-i <akinobu-i@users.noreply.github.com>
2024-12-20 17:19:46 +08:00
ef7e47d162 fix: rerank switch (#11897) 2024-12-20 16:12:34 +08:00
4211b9abbd chore: translate i18n files (#11892)
Co-authored-by: zxhlyh <16177003+zxhlyh@users.noreply.github.com>
2024-12-20 16:12:01 +08:00
0c0120ef27 Feat/workflow retry (#11885) 2024-12-20 15:44:37 +08:00
dacd457478 feat: add workflow parallel depth limit configuration (#11460)
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
2024-12-20 14:52:20 +08:00
7b03a0316d fix: better memory usage from 800+ to 500+ (#11796)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2024-12-20 14:51:43 +08:00
52201d95b1 chore: add retry index migration (#11887)
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
2024-12-20 14:40:33 +08:00
e2cde628bb chore: translate i18n files (#11855)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2024-12-20 14:19:47 +08:00
3335fa78fc fix: node 22 build (#11883) 2024-12-20 14:14:27 +08:00
7abc7fa573 Feat: Retry on node execution errors (#11871)
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
2024-12-20 14:14:06 +08:00
f6247fe67c Feat: Add partial success status to the app log (#11869)
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
2024-12-20 14:13:44 +08:00
996a9135f6 feat(llm_node): support order in text and files (#11837)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-20 14:12:50 +08:00
3599751f93 chore(db): use a better way to export models and remove unused table (#11838)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2024-12-20 14:12:29 +08:00
2d186e1e76 chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /web (#11876)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-20 13:46:54 +08:00
1469 changed files with 41220 additions and 17909 deletions

View File

@ -8,7 +8,7 @@ inputs:
poetry-version: poetry-version:
description: Poetry version to set up description: Poetry version to set up
required: true required: true
default: '1.8.4' default: '2.0.1'
poetry-lockfile: poetry-lockfile:
description: Path to the Poetry lockfile to restore cache from description: Path to the Poetry lockfile to restore cache from
required: true required: true

View File

@ -42,19 +42,23 @@ jobs:
run: poetry install -C api --with dev run: poetry install -C api --with dev
- name: Check dependencies in pyproject.toml - name: Check dependencies in pyproject.toml
run: poetry run -C api bash dev/pytest/pytest_artifacts.sh run: poetry run -P api bash dev/pytest/pytest_artifacts.sh
- name: Run Unit tests - name: Run Unit tests
run: poetry run -C api bash dev/pytest/pytest_unit_tests.sh run: poetry run -P api bash dev/pytest/pytest_unit_tests.sh
- name: Run ModelRuntime - name: Run ModelRuntime
run: poetry run -C api bash dev/pytest/pytest_model_runtime.sh run: poetry run -P api bash dev/pytest/pytest_model_runtime.sh
- name: Run dify config tests - name: Run dify config tests
run: poetry run -C api python dev/pytest/pytest_config_tests.py run: poetry run -P api python dev/pytest/pytest_config_tests.py
- name: Run Tool - name: Run Tool
run: poetry run -C api bash dev/pytest/pytest_tools.sh run: poetry run -P api bash dev/pytest/pytest_tools.sh
- name: Run mypy
run: |
poetry run -C api python -m mypy --install-types --non-interactive .
- name: Set up dotenvs - name: Set up dotenvs
run: | run: |
@ -74,4 +78,4 @@ jobs:
ssrf_proxy ssrf_proxy
- name: Run Workflow - name: Run Workflow
run: poetry run -C api bash dev/pytest/pytest_workflow.sh run: poetry run -P api bash dev/pytest/pytest_workflow.sh

View File

@ -38,12 +38,12 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: | run: |
poetry run -C api ruff --version poetry run -C api ruff --version
poetry run -C api ruff check ./api poetry run -C api ruff check ./
poetry run -C api ruff format --check ./api poetry run -C api ruff format --check ./
- name: Dotenv check - name: Dotenv check
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: poetry run -C api dotenv-linter ./api/.env.example ./web/.env.example run: poetry run -P api dotenv-linter ./api/.env.example ./web/.env.example
- name: Lint hints - name: Lint hints
if: failure() if: failure()
@ -82,6 +82,33 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
run: yarn run lint run: yarn run lint
docker-compose-template:
name: Docker Compose Template
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: |
docker/generate_docker_compose
docker/.env.example
docker/docker-compose-template.yaml
docker/docker-compose.yaml
- name: Generate Docker Compose
if: steps.changed-files.outputs.any_changed == 'true'
run: |
cd docker
./generate_docker_compose
- name: Check for changes
if: steps.changed-files.outputs.any_changed == 'true'
run: git diff --exit-code
superlinter: superlinter:
name: SuperLinter name: SuperLinter

View File

@ -70,4 +70,4 @@ jobs:
tidb tidb
- name: Test Vector Stores - name: Test Vector Stores
run: poetry run -C api bash dev/pytest/pytest_vdb.sh run: poetry run -P api bash dev/pytest/pytest_vdb.sh

View File

@ -23,6 +23,9 @@ FILES_ACCESS_TIMEOUT=300
# Access token expiration time in minutes # Access token expiration time in minutes
ACCESS_TOKEN_EXPIRE_MINUTES=60 ACCESS_TOKEN_EXPIRE_MINUTES=60
# Refresh token expiration time in days
REFRESH_TOKEN_EXPIRE_DAYS=30
# celery configuration # celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1 CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
@ -65,7 +68,7 @@ OPENDAL_FS_ROOT=storage
# S3 Storage configuration # S3 Storage configuration
S3_USE_AWS_MANAGED_IAM=false S3_USE_AWS_MANAGED_IAM=false
S3_ENDPOINT=https://your-bucket-name.storage.s3.clooudflare.com S3_ENDPOINT=https://your-bucket-name.storage.s3.cloudflare.com
S3_BUCKET_NAME=your-bucket-name S3_BUCKET_NAME=your-bucket-name
S3_ACCESS_KEY=your-access-key S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key S3_SECRET_KEY=your-secret-key
@ -74,7 +77,7 @@ S3_REGION=your-region
# Azure Blob Storage configuration # Azure Blob Storage configuration
AZURE_BLOB_ACCOUNT_NAME=your-account-name AZURE_BLOB_ACCOUNT_NAME=your-account-name
AZURE_BLOB_ACCOUNT_KEY=your-account-key AZURE_BLOB_ACCOUNT_KEY=your-account-key
AZURE_BLOB_CONTAINER_NAME=yout-container-name AZURE_BLOB_CONTAINER_NAME=your-container-name
AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net AZURE_BLOB_ACCOUNT_URL=https://<your_account_name>.blob.core.windows.net
# Aliyun oss Storage configuration # Aliyun oss Storage configuration
@ -88,7 +91,7 @@ ALIYUN_OSS_REGION=your-region
ALIYUN_OSS_PATH=your-path ALIYUN_OSS_PATH=your-path
# Google Storage configuration # Google Storage configuration
GOOGLE_STORAGE_BUCKET_NAME=yout-bucket-name GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string
# Tencent COS Storage configuration # Tencent COS Storage configuration
@ -399,6 +402,7 @@ INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
WORKFLOW_MAX_EXECUTION_STEPS=500 WORKFLOW_MAX_EXECUTION_STEPS=500
WORKFLOW_MAX_EXECUTION_TIME=1200 WORKFLOW_MAX_EXECUTION_TIME=1200
WORKFLOW_CALL_MAX_DEPTH=5 WORKFLOW_CALL_MAX_DEPTH=5
WORKFLOW_PARALLEL_DEPTH_LIMIT=3
MAX_VARIABLE_SIZE=204800 MAX_VARIABLE_SIZE=204800
# App configuration # App configuration

View File

@ -53,10 +53,12 @@ ignore = [
"FURB152", # math-constant "FURB152", # math-constant
"UP007", # non-pep604-annotation "UP007", # non-pep604-annotation
"UP032", # f-string "UP032", # f-string
"UP045", # non-pep604-annotation-optional
"B005", # strip-with-multi-characters "B005", # strip-with-multi-characters
"B006", # mutable-argument-default "B006", # mutable-argument-default
"B007", # unused-loop-control-variable "B007", # unused-loop-control-variable
"B026", # star-arg-unpacking-after-keyword-arg "B026", # star-arg-unpacking-after-keyword-arg
"B903", # class-as-data-structure
"B904", # raise-without-from-inside-except "B904", # raise-without-from-inside-except
"B905", # zip-without-explicit-strict "B905", # zip-without-explicit-strict
"N806", # non-lowercase-variable-in-function "N806", # non-lowercase-variable-in-function
@ -67,7 +69,7 @@ ignore = [
"SIM105", # suppressible-exception "SIM105", # suppressible-exception
"SIM107", # return-in-try-except-finally "SIM107", # return-in-try-except-finally
"SIM108", # if-else-block-instead-of-if-exp "SIM108", # if-else-block-instead-of-if-exp
"SIM113", # eumerate-for-loop "SIM113", # enumerate-for-loop
"SIM117", # multiple-with-statements "SIM117", # multiple-with-statements
"SIM210", # if-expr-with-true-false "SIM210", # if-expr-with-true-false
] ]
@ -85,11 +87,11 @@ ignore = [
] ]
"tests/*" = [ "tests/*" = [
"F811", # redefined-while-unused "F811", # redefined-while-unused
"F401", # unused-import
] ]
[lint.pyflakes] [lint.pyflakes]
extend-generics = [ allowed-unused-imports = [
"_pytest.monkeypatch", "_pytest.monkeypatch",
"tests.integration_tests", "tests.integration_tests",
"tests.unit_tests",
] ]

View File

@ -4,7 +4,7 @@ FROM python:3.12-slim-bookworm AS base
WORKDIR /app/api WORKDIR /app/api
# Install Poetry # Install Poetry
ENV POETRY_VERSION=1.8.4 ENV POETRY_VERSION=2.0.1
# if you located in China, you can use aliyun mirror to speed up # if you located in China, you can use aliyun mirror to speed up
# RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/ # RUN pip install --no-cache-dir poetry==${POETRY_VERSION} -i https://mirrors.aliyun.com/pypi/simple/
@ -52,12 +52,14 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends curl nodejs libgmp-dev libmpfr-dev libmpc-dev \ && apt-get install -y --no-install-recommends curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
# if you located in China, you can use aliyun mirror to speed up # if you located in China, you can use aliyun mirror to speed up
# && echo "deb http://mirrors.aliyun.com/debian testing main" > /etc/apt/sources.list \ # && echo "deb http://mirrors.aliyun.com/debian testing main" > /etc/apt/sources.list \
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \ && echo "deb http://deb.debian.org/debian bookworm main" > /etc/apt/sources.list \
&& apt-get update \ && apt-get update \
# For Security # For Security
&& apt-get install -y --no-install-recommends expat=2.6.4-1 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-8 libsqlite3-0=3.46.1-1 zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \ && apt-get install -y --no-install-recommends expat libldap-2.5-0 perl libsqlite3-0 zlib1g \
# install a chinese font to support the use of tools like matplotlib # install a chinese font to support the use of tools like matplotlib
&& apt-get install -y fonts-noto-cjk \ && apt-get install -y fonts-noto-cjk \
# install libmagic to support the use of python-magic guess MIMETYPE
&& apt-get install -y libmagic1 \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@ -79,5 +79,5 @@
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml` 2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
```bash ```bash
poetry run -C api bash dev/pytest/pytest_all_tests.sh poetry run -P api bash dev/pytest/pytest_all_tests.sh
``` ```

View File

@ -1,12 +1,8 @@
from libs import version_utils import os
import sys
# preparation before creating app
version_utils.check_supported_python_version()
def is_db_command(): def is_db_command():
import sys
if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db": if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db":
return True return True
return False return False
@ -18,10 +14,25 @@ if is_db_command():
app = create_migrations_app() app = create_migrations_app()
else: else:
from app_factory import create_app # It seems that JetBrains Python debugger does not work well with gevent,
from libs import threadings_utils # so we need to disable gevent in debug mode.
# If you are using debugpy and set GEVENT_SUPPORT=True, you can debug with gevent.
if (flask_debug := os.environ.get("FLASK_DEBUG", "0")) and flask_debug.lower() in {"false", "0", "no"}:
from gevent import monkey # type: ignore
threadings_utils.apply_gevent_threading_patch() # gevent
monkey.patch_all()
from grpc.experimental import gevent as grpc_gevent # type: ignore
# grpc gevent
grpc_gevent.init_gevent()
import psycogreen.gevent # type: ignore
psycogreen.gevent.patch_psycopg()
from app_factory import create_app
app = create_app() app = create_app()
celery = app.extensions["celery"] celery = app.extensions["celery"]

View File

@ -159,8 +159,7 @@ def migrate_annotation_vector_database():
try: try:
# get apps info # get apps info
apps = ( apps = (
db.session.query(App) App.query.filter(App.status == "normal")
.filter(App.status == "normal")
.order_by(App.created_at.desc()) .order_by(App.created_at.desc())
.paginate(page=page, per_page=50) .paginate(page=page, per_page=50)
) )
@ -285,8 +284,7 @@ def migrate_knowledge_vector_database():
while True: while True:
try: try:
datasets = ( datasets = (
db.session.query(Dataset) Dataset.query.filter(Dataset.indexing_technique == "high_quality")
.filter(Dataset.indexing_technique == "high_quality")
.order_by(Dataset.created_at.desc()) .order_by(Dataset.created_at.desc())
.paginate(page=page, per_page=50) .paginate(page=page, per_page=50)
) )
@ -450,7 +448,8 @@ def convert_to_agent_apps():
if app_id not in proceeded_app_ids: if app_id not in proceeded_app_ids:
proceeded_app_ids.append(app_id) proceeded_app_ids.append(app_id)
app = db.session.query(App).filter(App.id == app_id).first() app = db.session.query(App).filter(App.id == app_id).first()
apps.append(app) if app is not None:
apps.append(app)
if len(apps) == 0: if len(apps) == 0:
break break
@ -555,14 +554,20 @@ def create_tenant(email: str, language: Optional[str] = None, name: Optional[str
if language not in languages: if language not in languages:
language = "en-US" language = "en-US"
name = name.strip() # Validates name encoding for non-Latin characters.
name = name.strip().encode("utf-8").decode("utf-8") if name else None
# generate random password # generate random password
new_password = secrets.token_urlsafe(16) new_password = secrets.token_urlsafe(16)
# register account # register account
account = RegisterService.register(email=email, name=account_name, password=new_password, language=language) account = RegisterService.register(
email=email,
name=account_name,
password=new_password,
language=language,
create_workspace_required=False,
)
TenantService.create_owner_tenant_if_not_exist(account, name) TenantService.create_owner_tenant_if_not_exist(account, name)
click.echo( click.echo(
@ -582,7 +587,7 @@ def upgrade_db():
click.echo(click.style("Starting database migration.", fg="green")) click.echo(click.style("Starting database migration.", fg="green"))
# run db migration # run db migration
import flask_migrate import flask_migrate # type: ignore
flask_migrate.upgrade() flask_migrate.upgrade()
@ -620,6 +625,10 @@ where sites.id is null limit 1000"""
try: try:
app = db.session.query(App).filter(App.id == app_id).first() app = db.session.query(App).filter(App.id == app_id).first()
if not app:
print(f"App {app_id} not found")
continue
tenant = app.tenant tenant = app.tenant
if tenant: if tenant:
accounts = tenant.get_accounts() accounts = tenant.get_accounts()

View File

@ -146,7 +146,7 @@ class EndpointConfig(BaseSettings):
) )
CONSOLE_WEB_URL: str = Field( CONSOLE_WEB_URL: str = Field(
description="Base URL for the console web interface," "used for frontend references and CORS configuration", description="Base URL for the console web interface,used for frontend references and CORS configuration",
default="", default="",
) )
@ -239,7 +239,6 @@ class HttpConfig(BaseSettings):
) )
@computed_field @computed_field
@property
def CONSOLE_CORS_ALLOW_ORIGINS(self) -> list[str]: def CONSOLE_CORS_ALLOW_ORIGINS(self) -> list[str]:
return self.inner_CONSOLE_CORS_ALLOW_ORIGINS.split(",") return self.inner_CONSOLE_CORS_ALLOW_ORIGINS.split(",")
@ -250,7 +249,6 @@ class HttpConfig(BaseSettings):
) )
@computed_field @computed_field
@property
def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]: def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]:
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",") return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",")
@ -433,6 +431,11 @@ class WorkflowConfig(BaseSettings):
default=5, default=5,
) )
WORKFLOW_PARALLEL_DEPTH_LIMIT: PositiveInt = Field(
description="Maximum allowed depth for nested parallel executions",
default=3,
)
MAX_VARIABLE_SIZE: PositiveInt = Field( MAX_VARIABLE_SIZE: PositiveInt = Field(
description="Maximum size in bytes for a single variable in workflows. Default to 200 KB.", description="Maximum size in bytes for a single variable in workflows. Default to 200 KB.",
default=200 * 1024, default=200 * 1024,
@ -485,6 +488,11 @@ class AuthConfig(BaseSettings):
default=60, default=60,
) )
REFRESH_TOKEN_EXPIRE_DAYS: PositiveFloat = Field(
description="Expiration time for refresh tokens in days",
default=30,
)
LOGIN_LOCKOUT_DURATION: PositiveInt = Field( LOGIN_LOCKOUT_DURATION: PositiveInt = Field(
description="Time (in seconds) a user must wait before retrying login after exceeding the rate limit.", description="Time (in seconds) a user must wait before retrying login after exceeding the rate limit.",
default=86400, default=86400,
@ -598,7 +606,7 @@ class RagEtlConfig(BaseSettings):
UNSTRUCTURED_API_KEY: Optional[str] = Field( UNSTRUCTURED_API_KEY: Optional[str] = Field(
description="API key for Unstructured.io service", description="API key for Unstructured.io service",
default=None, default="",
) )
SCARF_NO_ANALYTICS: Optional[str] = Field( SCARF_NO_ANALYTICS: Optional[str] = Field(
@ -664,6 +672,11 @@ class IndexingConfig(BaseSettings):
default=4000, default=4000,
) )
CHILD_CHUNKS_PREVIEW_NUMBER: PositiveInt = Field(
description="Maximum number of child chunks to preview",
default=50,
)
class MultiModalTransferConfig(BaseSettings): class MultiModalTransferConfig(BaseSettings):
MULTIMODAL_SEND_FORMAT: Literal["base64", "url"] = Field( MULTIMODAL_SEND_FORMAT: Literal["base64", "url"] = Field(
@ -710,27 +723,27 @@ class PositionConfig(BaseSettings):
default="", default="",
) )
@computed_field @property
def POSITION_PROVIDER_PINS_LIST(self) -> list[str]: def POSITION_PROVIDER_PINS_LIST(self) -> list[str]:
return [item.strip() for item in self.POSITION_PROVIDER_PINS.split(",") if item.strip() != ""] return [item.strip() for item in self.POSITION_PROVIDER_PINS.split(",") if item.strip() != ""]
@computed_field @property
def POSITION_PROVIDER_INCLUDES_SET(self) -> set[str]: def POSITION_PROVIDER_INCLUDES_SET(self) -> set[str]:
return {item.strip() for item in self.POSITION_PROVIDER_INCLUDES.split(",") if item.strip() != ""} return {item.strip() for item in self.POSITION_PROVIDER_INCLUDES.split(",") if item.strip() != ""}
@computed_field @property
def POSITION_PROVIDER_EXCLUDES_SET(self) -> set[str]: def POSITION_PROVIDER_EXCLUDES_SET(self) -> set[str]:
return {item.strip() for item in self.POSITION_PROVIDER_EXCLUDES.split(",") if item.strip() != ""} return {item.strip() for item in self.POSITION_PROVIDER_EXCLUDES.split(",") if item.strip() != ""}
@computed_field @property
def POSITION_TOOL_PINS_LIST(self) -> list[str]: def POSITION_TOOL_PINS_LIST(self) -> list[str]:
return [item.strip() for item in self.POSITION_TOOL_PINS.split(",") if item.strip() != ""] return [item.strip() for item in self.POSITION_TOOL_PINS.split(",") if item.strip() != ""]
@computed_field @property
def POSITION_TOOL_INCLUDES_SET(self) -> set[str]: def POSITION_TOOL_INCLUDES_SET(self) -> set[str]:
return {item.strip() for item in self.POSITION_TOOL_INCLUDES.split(",") if item.strip() != ""} return {item.strip() for item in self.POSITION_TOOL_INCLUDES.split(",") if item.strip() != ""}
@computed_field @property
def POSITION_TOOL_EXCLUDES_SET(self) -> set[str]: def POSITION_TOOL_EXCLUDES_SET(self) -> set[str]:
return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""} return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""}
@ -762,6 +775,13 @@ class LoginConfig(BaseSettings):
) )
class AccountConfig(BaseSettings):
ACCOUNT_DELETION_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="Duration in minutes for which a account deletion token remains valid",
default=5,
)
class FeatureConfig( class FeatureConfig(
# place the configs in alphabet order # place the configs in alphabet order
AppExecutionConfig, AppExecutionConfig,
@ -789,6 +809,7 @@ class FeatureConfig(
WorkflowNodeExecutionConfig, WorkflowNodeExecutionConfig,
WorkspaceConfig, WorkspaceConfig,
LoginConfig, LoginConfig,
AccountConfig,
# hosted services config # hosted services config
HostedServiceConfig, HostedServiceConfig,
CeleryBeatConfig, CeleryBeatConfig,

View File

@ -181,7 +181,7 @@ class HostedFetchAppTemplateConfig(BaseSettings):
""" """
HOSTED_FETCH_APP_TEMPLATES_MODE: str = Field( HOSTED_FETCH_APP_TEMPLATES_MODE: str = Field(
description="Mode for fetching app templates: remote, db, or builtin" " default to remote,", description="Mode for fetching app templates: remote, db, or builtin default to remote,",
default="remote", default="remote",
) )

View File

@ -130,7 +130,6 @@ class DatabaseConfig(BaseSettings):
) )
@computed_field @computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> str: def SQLALCHEMY_DATABASE_URI(self) -> str:
db_extras = ( db_extras = (
f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS f"{self.DB_EXTRAS}&client_encoding={self.DB_CHARSET}" if self.DB_CHARSET else self.DB_EXTRAS
@ -168,7 +167,6 @@ class DatabaseConfig(BaseSettings):
) )
@computed_field @computed_field
@property
def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]: def SQLALCHEMY_ENGINE_OPTIONS(self) -> dict[str, Any]:
return { return {
"pool_size": self.SQLALCHEMY_POOL_SIZE, "pool_size": self.SQLALCHEMY_POOL_SIZE,
@ -206,7 +204,6 @@ class CeleryConfig(DatabaseConfig):
) )
@computed_field @computed_field
@property
def CELERY_RESULT_BACKEND(self) -> str | None: def CELERY_RESULT_BACKEND(self) -> str | None:
return ( return (
"db+{}".format(self.SQLALCHEMY_DATABASE_URI) "db+{}".format(self.SQLALCHEMY_DATABASE_URI)
@ -214,7 +211,6 @@ class CeleryConfig(DatabaseConfig):
else self.CELERY_BROKER_URL else self.CELERY_BROKER_URL
) )
@computed_field
@property @property
def BROKER_USE_SSL(self) -> bool: def BROKER_USE_SSL(self) -> bool:
return self.CELERY_BROKER_URL.startswith("rediss://") if self.CELERY_BROKER_URL else False return self.CELERY_BROKER_URL.startswith("rediss://") if self.CELERY_BROKER_URL else False

View File

@ -33,3 +33,9 @@ class MilvusConfig(BaseSettings):
description="Name of the Milvus database to connect to (default is 'default')", description="Name of the Milvus database to connect to (default is 'default')",
default="default", default="default",
) )
MILVUS_ENABLE_HYBRID_SEARCH: bool = Field(
description="Enable hybrid search features (requires Milvus >= 2.5.0). Set to false for compatibility with "
"older versions",
default=True,
)

View File

@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field( CURRENT_VERSION: str = Field(
description="Dify version", description="Dify version",
default="0.14.1", default="0.15.2",
) )
COMMIT_SHA: str = Field( COMMIT_SHA: str = Field(

View File

@ -4,6 +4,7 @@ import logging
import os import os
import threading import threading
import time import time
from collections.abc import Mapping
from pathlib import Path from pathlib import Path
from .python_3x import http_request, makedirs_wrapper from .python_3x import http_request, makedirs_wrapper
@ -255,8 +256,8 @@ class ApolloClient:
logger.info("stopped, long_poll") logger.info("stopped, long_poll")
# add the need for endorsement to the header # add the need for endorsement to the header
def _sign_headers(self, url): def _sign_headers(self, url: str) -> Mapping[str, str]:
headers = {} headers: dict[str, str] = {}
if self.secret == "": if self.secret == "":
return headers return headers
uri = url[len(self.config_url) : len(url)] uri = url[len(self.config_url) : len(url)]

View File

@ -1,8 +1,9 @@
import json import json
from collections.abc import Mapping
from models.model import AppMode from models.model import AppMode
default_app_templates = { default_app_templates: Mapping[AppMode, Mapping] = {
# workflow default mode # workflow default mode
AppMode.WORKFLOW: { AppMode.WORKFLOW: {
"app": { "app": {

View File

@ -4,3 +4,8 @@ from werkzeug.exceptions import HTTPException
class FilenameNotExistsError(HTTPException): class FilenameNotExistsError(HTTPException):
code = 400 code = 400
description = "The specified filename does not exist." description = "The specified filename does not exist."
class RemoteFileUploadError(HTTPException):
code = 400
description = "Error uploading remote file."

View File

@ -1,4 +1,4 @@
from flask_restful import fields from flask_restful import fields # type: ignore
parameters__system_parameters = { parameters__system_parameters = {
"image_file_size_limit": fields.Integer, "image_file_size_limit": fields.Integer,

View File

@ -1,12 +1,32 @@
import mimetypes import mimetypes
import os import os
import platform
import re import re
import urllib.parse import urllib.parse
import warnings
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any from typing import Any
from uuid import uuid4 from uuid import uuid4
import httpx import httpx
try:
import magic
except ImportError:
if platform.system() == "Windows":
warnings.warn(
"To use python-magic guess MIMETYPE, you need to run `pip install python-magic-bin`", stacklevel=2
)
elif platform.system() == "Darwin":
warnings.warn("To use python-magic guess MIMETYPE, you need to run `brew install libmagic`", stacklevel=2)
elif platform.system() == "Linux":
warnings.warn(
"To use python-magic guess MIMETYPE, you need to run `sudo apt-get install libmagic1`", stacklevel=2
)
else:
warnings.warn("To use python-magic guess MIMETYPE, you need to install `libmagic`", stacklevel=2)
magic = None # type: ignore
from pydantic import BaseModel from pydantic import BaseModel
from configs import dify_config from configs import dify_config
@ -47,6 +67,13 @@ def guess_file_info_from_response(response: httpx.Response):
# If guessing fails, use Content-Type from response headers # If guessing fails, use Content-Type from response headers
mimetype = response.headers.get("Content-Type", "application/octet-stream") mimetype = response.headers.get("Content-Type", "application/octet-stream")
# Use python-magic to guess MIME type if still unknown or generic
if mimetype == "application/octet-stream" and magic is not None:
try:
mimetype = magic.from_buffer(response.content[:1024], mime=True)
except magic.MagicException:
pass
extension = os.path.splitext(filename)[1] extension = os.path.splitext(filename)[1]
# Ensure filename has an extension # Ensure filename has an extension

View File

@ -3,6 +3,25 @@ from flask import Blueprint
from libs.external_api import ExternalApi from libs.external_api import ExternalApi
from .app.app_import import AppImportApi, AppImportConfirmApi from .app.app_import import AppImportApi, AppImportConfirmApi
from .explore.audio import ChatAudioApi, ChatTextApi
from .explore.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi
from .explore.conversation import (
ConversationApi,
ConversationListApi,
ConversationPinApi,
ConversationRenameApi,
ConversationUnPinApi,
)
from .explore.message import (
MessageFeedbackApi,
MessageListApi,
MessageMoreLikeThisApi,
MessageSuggestedQuestionApi,
)
from .explore.workflow import (
InstalledAppWorkflowRunApi,
InstalledAppWorkflowTaskStopApi,
)
from .files import FileApi, FilePreviewApi, FileSupportTypeApi from .files import FileApi, FilePreviewApi, FileSupportTypeApi
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
@ -66,15 +85,81 @@ from .datasets import (
# Import explore controllers # Import explore controllers
from .explore import ( from .explore import (
audio,
completion,
conversation,
installed_app, installed_app,
message,
parameter, parameter,
recommended_app, recommended_app,
saved_message, saved_message,
workflow, )
# Explore Audio
api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio")
api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text")
# Explore Completion
api.add_resource(
CompletionApi, "/installed-apps/<uuid:installed_app_id>/completion-messages", endpoint="installed_app_completion"
)
api.add_resource(
CompletionStopApi,
"/installed-apps/<uuid:installed_app_id>/completion-messages/<string:task_id>/stop",
endpoint="installed_app_stop_completion",
)
api.add_resource(
ChatApi, "/installed-apps/<uuid:installed_app_id>/chat-messages", endpoint="installed_app_chat_completion"
)
api.add_resource(
ChatStopApi,
"/installed-apps/<uuid:installed_app_id>/chat-messages/<string:task_id>/stop",
endpoint="installed_app_stop_chat_completion",
)
# Explore Conversation
api.add_resource(
ConversationRenameApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/name",
endpoint="installed_app_conversation_rename",
)
api.add_resource(
ConversationListApi, "/installed-apps/<uuid:installed_app_id>/conversations", endpoint="installed_app_conversations"
)
api.add_resource(
ConversationApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>",
endpoint="installed_app_conversation",
)
api.add_resource(
ConversationPinApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/pin",
endpoint="installed_app_conversation_pin",
)
api.add_resource(
ConversationUnPinApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/unpin",
endpoint="installed_app_conversation_unpin",
)
# Explore Message
api.add_resource(MessageListApi, "/installed-apps/<uuid:installed_app_id>/messages", endpoint="installed_app_messages")
api.add_resource(
MessageFeedbackApi,
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/feedbacks",
endpoint="installed_app_message_feedback",
)
api.add_resource(
MessageMoreLikeThisApi,
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/more-like-this",
endpoint="installed_app_more_like_this",
)
api.add_resource(
MessageSuggestedQuestionApi,
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/suggested-questions",
endpoint="installed_app_suggested_question",
)
# Explore Workflow
api.add_resource(InstalledAppWorkflowRunApi, "/installed-apps/<uuid:installed_app_id>/workflows/run")
api.add_resource(
InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop"
) )
# Import tag controllers # Import tag controllers

View File

@ -1,7 +1,7 @@
from functools import wraps from functools import wraps
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import NotFound, Unauthorized from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config from configs import dify_config
@ -56,7 +56,7 @@ class InsertExploreAppListApi(Resource):
app = App.query.filter(App.id == args["app_id"]).first() app = App.query.filter(App.id == args["app_id"]).first()
if not app: if not app:
raise NotFound(f'App \'{args["app_id"]}\' is not found') raise NotFound(f"App '{args['app_id']}' is not found")
site = app.site site = app.site
if not site: if not site:

View File

@ -1,5 +1,7 @@
import flask_restful from typing import Any
from flask_login import current_user
import flask_restful # type: ignore
from flask_login import current_user # type: ignore
from flask_restful import Resource, fields, marshal_with from flask_restful import Resource, fields, marshal_with
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
@ -35,14 +37,15 @@ def _get_resource(resource_id, tenant_id, resource_model):
class BaseApiKeyListResource(Resource): class BaseApiKeyListResource(Resource):
method_decorators = [account_initialization_required, login_required, setup_required] method_decorators = [account_initialization_required, login_required, setup_required]
resource_type = None resource_type: str | None = None
resource_model = None resource_model: Any = None
resource_id_field = None resource_id_field: str | None = None
token_prefix = None token_prefix: str | None = None
max_keys = 10 max_keys = 10
@marshal_with(api_key_list) @marshal_with(api_key_list)
def get(self, resource_id): def get(self, resource_id):
assert self.resource_id_field is not None, "resource_id_field must be set"
resource_id = str(resource_id) resource_id = str(resource_id)
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model) _get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
keys = ( keys = (
@ -54,6 +57,7 @@ class BaseApiKeyListResource(Resource):
@marshal_with(api_key_fields) @marshal_with(api_key_fields)
def post(self, resource_id): def post(self, resource_id):
assert self.resource_id_field is not None, "resource_id_field must be set"
resource_id = str(resource_id) resource_id = str(resource_id)
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model) _get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
if not current_user.is_editor: if not current_user.is_editor:
@ -86,11 +90,12 @@ class BaseApiKeyListResource(Resource):
class BaseApiKeyResource(Resource): class BaseApiKeyResource(Resource):
method_decorators = [account_initialization_required, login_required, setup_required] method_decorators = [account_initialization_required, login_required, setup_required]
resource_type = None resource_type: str | None = None
resource_model = None resource_model: Any = None
resource_id_field = None resource_id_field: str | None = None
def delete(self, resource_id, api_key_id): def delete(self, resource_id, api_key_id):
assert self.resource_id_field is not None, "resource_id_field must be set"
resource_id = str(resource_id) resource_id = str(resource_id)
api_key_id = str(api_key_id) api_key_id = str(api_key_id)
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model) _get_resource(resource_id, current_user.current_tenant_id, self.resource_model)

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.wraps import account_initialization_required, setup_required from controllers.console.wraps import account_initialization_required, setup_required

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model

View File

@ -1,6 +1,6 @@
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal, marshal_with, reqparse from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.console import api from controllers.console import api
@ -110,7 +110,7 @@ class AnnotationListApi(Resource):
page = request.args.get("page", default=1, type=int) page = request.args.get("page", default=1, type=int)
limit = request.args.get("limit", default=20, type=int) limit = request.args.get("limit", default=20, type=int)
keyword = request.args.get("keyword", default=None, type=str) keyword = request.args.get("keyword", default="", type=str)
app_id = str(app_id) app_id = str(app_id)
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword) annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(app_id, page, limit, keyword)

View File

@ -1,8 +1,8 @@
import uuid import uuid
from typing import cast from typing import cast
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse from flask_restful import Resource, inputs, marshal, marshal_with, reqparse # type: ignore
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, Forbidden, abort from werkzeug.exceptions import BadRequest, Forbidden, abort
@ -57,12 +57,13 @@ class AppListApi(Resource):
) )
parser.add_argument("name", type=str, location="args", required=False) parser.add_argument("name", type=str, location="args", required=False)
parser.add_argument("tag_ids", type=uuid_list, location="args", required=False) parser.add_argument("tag_ids", type=uuid_list, location="args", required=False)
parser.add_argument("is_created_by_me", type=inputs.boolean, location="args", required=False)
args = parser.parse_args() args = parser.parse_args()
# get app list # get app list
app_service = AppService() app_service = AppService()
app_pagination = app_service.get_paginate_apps(current_user.current_tenant_id, args) app_pagination = app_service.get_paginate_apps(current_user.id, current_user.current_tenant_id, args)
if not app_pagination: if not app_pagination:
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False} return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}

View File

@ -1,7 +1,7 @@
from typing import cast from typing import cast
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden

View File

@ -1,7 +1,7 @@
import logging import logging
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
import services import services
@ -22,7 +22,7 @@ from controllers.console.wraps import account_initialization_required, setup_req
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from libs.login import login_required from libs.login import login_required
from models.model import AppMode from models import App, AppMode
from services.audio_service import AudioService from services.audio_service import AudioService
from services.errors.audio import ( from services.errors.audio import (
AudioTooLargeServiceError, AudioTooLargeServiceError,
@ -79,7 +79,7 @@ class ChatMessageTextApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@get_app_model @get_app_model
def post(self, app_model): def post(self, app_model: App):
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
try: try:
@ -98,9 +98,13 @@ class ChatMessageTextApi(Resource):
and app_model.workflow.features_dict and app_model.workflow.features_dict
): ):
text_to_speech = app_model.workflow.features_dict.get("text_to_speech") text_to_speech = app_model.workflow.features_dict.get("text_to_speech")
if text_to_speech is None:
raise ValueError("TTS is not enabled")
voice = args.get("voice") or text_to_speech.get("voice") voice = args.get("voice") or text_to_speech.get("voice")
else: else:
try: try:
if app_model.app_model_config is None:
raise ValueError("AppModelConfig not found")
voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice")
except Exception: except Exception:
voice = None voice = None

View File

@ -1,7 +1,7 @@
import logging import logging
import flask_login import flask_login # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import InternalServerError, NotFound from werkzeug.exceptions import InternalServerError, NotFound
import services import services
@ -20,7 +20,6 @@ from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpErr
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 (
AppInvokeQuotaExceededError,
ModelCurrentlyNotSupportError, ModelCurrentlyNotSupportError,
ProviderTokenNotInitError, ProviderTokenNotInitError,
QuotaExceededError, QuotaExceededError,
@ -76,7 +75,7 @@ class CompletionMessageApi(Resource):
raise ProviderModelCurrentlyNotSupportError() raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e: except InvokeError as e:
raise CompletionRequestError(e.description) raise CompletionRequestError(e.description)
except (ValueError, AppInvokeQuotaExceededError) as e: except ValueError as e:
raise e raise e
except Exception as e: except Exception as e:
logging.exception("internal server error.") logging.exception("internal server error.")
@ -141,7 +140,7 @@ class ChatMessageApi(Resource):
raise InvokeRateLimitHttpError(ex.description) raise InvokeRateLimitHttpError(ex.description)
except InvokeError as e: except InvokeError as e:
raise CompletionRequestError(e.description) raise CompletionRequestError(e.description)
except (ValueError, AppInvokeQuotaExceededError) as e: except ValueError as e:
raise e raise e
except Exception as e: except Exception as e:
logging.exception("internal server error.") logging.exception("internal server error.")

View File

@ -1,9 +1,9 @@
from datetime import UTC, datetime from datetime import UTC, datetime
import pytz import pytz # pip install pytz
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from sqlalchemy import func, or_ from sqlalchemy import func, or_
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
@ -77,8 +77,9 @@ class CompletionConversationApi(Resource):
query = query.where(Conversation.created_at < end_datetime_utc) query = query.where(Conversation.created_at < end_datetime_utc)
# FIXME, the type ignore in this file
if args["annotation_status"] == "annotated": if args["annotation_status"] == "annotated":
query = query.options(joinedload(Conversation.message_annotations)).join( query = query.options(joinedload(Conversation.message_annotations)).join( # type: ignore
MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id
) )
elif args["annotation_status"] == "not_annotated": elif args["annotation_status"] == "not_annotated":
@ -222,7 +223,7 @@ class ChatConversationApi(Resource):
query = query.where(Conversation.created_at <= end_datetime_utc) query = query.where(Conversation.created_at <= end_datetime_utc)
if args["annotation_status"] == "annotated": if args["annotation_status"] == "annotated":
query = query.options(joinedload(Conversation.message_annotations)).join( query = query.options(joinedload(Conversation.message_annotations)).join( # type: ignore
MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id
) )
elif args["annotation_status"] == "not_annotated": elif args["annotation_status"] == "not_annotated":
@ -234,7 +235,7 @@ class ChatConversationApi(Resource):
if args["message_count_gte"] and args["message_count_gte"] >= 1: if args["message_count_gte"] and args["message_count_gte"] >= 1:
query = ( query = (
query.options(joinedload(Conversation.messages)) query.options(joinedload(Conversation.messages)) # type: ignore
.join(Message, Message.conversation_id == Conversation.id) .join(Message, Message.conversation_id == Conversation.id)
.group_by(Conversation.id) .group_by(Conversation.id)
.having(func.count(Message.id) >= args["message_count_gte"]) .having(func.count(Message.id) >= args["message_count_gte"])

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session

View File

@ -1,7 +1,7 @@
import os import os
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.error import ( from controllers.console.app.error import (

View File

@ -1,8 +1,8 @@
import logging import logging
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
from controllers.console import api from controllers.console import api

View File

@ -1,8 +1,9 @@
import json import json
from typing import cast
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource from flask_restful import Resource # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
@ -26,7 +27,9 @@ class ModelConfigResource(Resource):
"""Modify app model config""" """Modify app model config"""
# validate config # validate config
model_configuration = AppModelConfigService.validate_configuration( model_configuration = AppModelConfigService.validate_configuration(
tenant_id=current_user.current_tenant_id, config=request.json, app_mode=AppMode.value_of(app_model.mode) tenant_id=current_user.current_tenant_id,
config=cast(dict, request.json),
app_mode=AppMode.value_of(app_model.mode),
) )
new_app_model_config = AppModelConfig( new_app_model_config = AppModelConfig(
@ -38,9 +41,11 @@ class ModelConfigResource(Resource):
if app_model.mode == AppMode.AGENT_CHAT.value or app_model.is_agent: if app_model.mode == AppMode.AGENT_CHAT.value or app_model.is_agent:
# get original app model config # get original app model config
original_app_model_config: AppModelConfig = ( original_app_model_config = (
db.session.query(AppModelConfig).filter(AppModelConfig.id == app_model.app_model_config_id).first() db.session.query(AppModelConfig).filter(AppModelConfig.id == app_model.app_model_config_id).first()
) )
if original_app_model_config is None:
raise ValueError("Original app model config not found")
agent_mode = original_app_model_config.agent_mode_dict agent_mode = original_app_model_config.agent_mode_dict
# decrypt agent tool parameters if it's secret-input # decrypt agent tool parameters if it's secret-input
parameter_map = {} parameter_map = {}

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from controllers.console import api from controllers.console import api

View File

@ -1,7 +1,7 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
from constants.languages import supported_language from constants.languages import supported_language
@ -50,7 +50,7 @@ class AppSite(Resource):
if not current_user.is_editor: if not current_user.is_editor:
raise Forbidden() raise Forbidden()
site = db.session.query(Site).filter(Site.app_id == app_model.id).one_or_404() site = Site.query.filter(Site.app_id == app_model.id).one_or_404()
for attr_name in [ for attr_name in [
"title", "title",

View File

@ -3,8 +3,8 @@ from decimal import Decimal
import pytz import pytz
from flask import jsonify from flask import jsonify
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
@ -273,8 +273,7 @@ FROM
messages m messages m
ON c.id = m.conversation_id ON c.id = m.conversation_id
WHERE WHERE
c.override_model_configs IS NULL c.app_id = :app_id"""
AND c.app_id = :app_id"""
arg_dict = {"tz": account.timezone, "app_id": app_model.id} arg_dict = {"tz": account.timezone, "app_id": app_model.id}
timezone = pytz.timezone(account.timezone) timezone = pytz.timezone(account.timezone)

View File

@ -2,10 +2,11 @@ import json
import logging import logging
from flask import abort, request from flask import abort, request
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services import services
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
@ -13,7 +14,7 @@ from controllers.console.wraps import account_initialization_required, setup_req
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 factories import variable_factory from factories import variable_factory
from fields.workflow_fields import workflow_fields from fields.workflow_fields import workflow_fields, workflow_pagination_fields
from fields.workflow_run_fields import workflow_run_node_execution_fields from fields.workflow_run_fields import workflow_run_node_execution_fields
from libs import helper from libs import helper
from libs.helper import TimestampField, uuid_value from libs.helper import TimestampField, uuid_value
@ -426,7 +427,46 @@ class ConvertToWorkflowApi(Resource):
} }
class WorkflowConfigApi(Resource):
"""Resource for workflow configuration."""
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
return {
"parallel_depth_limit": dify_config.WORKFLOW_PARALLEL_DEPTH_LIMIT,
}
class PublishedAllWorkflowApi(Resource):
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@marshal_with(workflow_pagination_fields)
def get(self, app_model: App):
"""
Get published workflows
"""
if not current_user.is_editor:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args()
page = args.get("page")
limit = args.get("limit")
workflow_service = WorkflowService()
workflows, has_more = workflow_service.get_all_published_workflow(app_model=app_model, page=page, limit=limit)
return {"items": workflows, "page": page, "limit": limit, "has_more": has_more}
api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft") api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
api.add_resource(WorkflowConfigApi, "/apps/<uuid:app_id>/workflows/draft/config")
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run") api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run") api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop") api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
@ -439,6 +479,7 @@ api.add_resource(
WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run" WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run"
) )
api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish") api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish")
api.add_resource(PublishedAllWorkflowApi, "/apps/<uuid:app_id>/workflows")
api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs") api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
api.add_resource( api.add_resource(
DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>" DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>"

View File

@ -1,5 +1,5 @@
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model

View File

@ -1,5 +1,5 @@
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model

View File

@ -3,8 +3,8 @@ from decimal import Decimal
import pytz import pytz
from flask import jsonify from flask import jsonify
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model

View File

@ -5,11 +5,10 @@ from typing import Optional, Union
from controllers.console.app.error import AppNotFoundError from controllers.console.app.error import AppNotFoundError
from extensions.ext_database import db from extensions.ext_database import db
from libs.login import current_user from libs.login import current_user
from models import App from models import App, AppMode
from models.model import AppMode
def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[AppMode]] = None): def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[AppMode], None] = None):
def decorator(view_func): def decorator(view_func):
@wraps(view_func) @wraps(view_func)
def decorated_view(*args, **kwargs): def decorated_view(*args, **kwargs):

View File

@ -1,14 +1,14 @@
import datetime import datetime
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from constants.languages import supported_language from constants.languages import supported_language
from controllers.console import api from controllers.console import api
from controllers.console.error import AlreadyActivateError from controllers.console.error import AlreadyActivateError
from extensions.ext_database import db from extensions.ext_database import db
from libs.helper import StrLen, email, extract_remote_ip, timezone from libs.helper import StrLen, email, extract_remote_ip, timezone
from models.account import AccountStatus, Tenant from models.account import AccountStatus
from services.account_service import AccountService, RegisterService from services.account_service import AccountService, RegisterService
@ -27,7 +27,7 @@ class ActivateCheckApi(Resource):
invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token) invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token)
if invitation: if invitation:
data = invitation.get("data", {}) data = invitation.get("data", {})
tenant: Tenant = invitation.get("tenant", None) tenant = invitation.get("tenant", None)
workspace_name = tenant.name if tenant else None workspace_name = tenant.name if tenant else None
workspace_id = tenant.id if tenant else None workspace_id = tenant.id if tenant else None
invitee_email = data.get("email") if data else None invitee_email = data.get("email") if data else None

View File

@ -1,5 +1,5 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.console import api from controllers.console import api

View File

@ -2,8 +2,8 @@ import logging
import requests import requests
from flask import current_app, redirect, request from flask import current_app, redirect, request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource from flask_restful import Resource # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from configs import dify_config from configs import dify_config
@ -17,8 +17,8 @@ from ..wraps import account_initialization_required, setup_required
def get_oauth_providers(): def get_oauth_providers():
with current_app.app_context(): with current_app.app_context():
notion_oauth = NotionOAuth( notion_oauth = NotionOAuth(
client_id=dify_config.NOTION_CLIENT_ID, client_id=dify_config.NOTION_CLIENT_ID or "",
client_secret=dify_config.NOTION_CLIENT_SECRET, client_secret=dify_config.NOTION_CLIENT_SECRET or "",
redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/data-source/callback/notion", redirect_uri=dify_config.CONSOLE_API_URL + "/console/api/oauth/data-source/callback/notion",
) )

View File

@ -53,3 +53,9 @@ class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
error_code = "email_code_login_rate_limit_exceeded" error_code = "email_code_login_rate_limit_exceeded"
description = "Too many login emails have been sent. Please try again in 5 minutes." description = "Too many login emails have been sent. Please try again in 5 minutes."
code = 429 code = 429
class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
error_code = "email_code_account_deletion_rate_limit_exceeded"
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
code = 429

View File

@ -2,17 +2,12 @@ import base64
import secrets import secrets
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from constants.languages import languages from constants.languages import languages
from controllers.console import api from controllers.console import api
from controllers.console.auth.error import ( from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError
EmailCodeError, from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
InvalidEmailError,
InvalidTokenError,
PasswordMismatchError,
)
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
from controllers.console.wraps import setup_required from controllers.console.wraps import setup_required
from events.tenant_event import tenant_was_created from events.tenant_event import tenant_was_created
from extensions.ext_database import db from extensions.ext_database import db
@ -20,6 +15,7 @@ from libs.helper import email, extract_remote_ip
from libs.password import hash_password, valid_password from libs.password import hash_password, valid_password
from models.account import Account from models.account import Account
from services.account_service import AccountService, TenantService from services.account_service import AccountService, TenantService
from services.errors.account import AccountRegisterError
from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.errors.workspace import WorkSpaceNotAllowedCreateError
from services.feature_service import FeatureService from services.feature_service import FeatureService
@ -122,13 +118,15 @@ class ForgotPasswordResetApi(Resource):
else: else:
try: try:
account = AccountService.create_account_and_tenant( account = AccountService.create_account_and_tenant(
email=reset_data.get("email"), email=reset_data.get("email", ""),
name=reset_data.get("email"), name=reset_data.get("email", ""),
password=password_confirm, password=password_confirm,
interface_language=languages[0], interface_language=languages[0],
) )
except WorkSpaceNotAllowedCreateError: except WorkSpaceNotAllowedCreateError:
pass pass
except AccountRegisterError as are:
raise AccountInFreezeError()
return {"result": "success"} return {"result": "success"}

View File

@ -1,10 +1,11 @@
from typing import cast from typing import cast
import flask_login import flask_login # type: ignore
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
import services import services
from configs import dify_config
from constants.languages import languages from constants.languages import languages
from controllers.console import api from controllers.console import api
from controllers.console.auth.error import ( from controllers.console.auth.error import (
@ -16,6 +17,7 @@ from controllers.console.auth.error import (
) )
from controllers.console.error import ( from controllers.console.error import (
AccountBannedError, AccountBannedError,
AccountInFreezeError,
AccountNotFound, AccountNotFound,
EmailSendIpLimitError, EmailSendIpLimitError,
NotAllowedCreateWorkspace, NotAllowedCreateWorkspace,
@ -26,6 +28,8 @@ from libs.helper import email, extract_remote_ip
from libs.password import valid_password from libs.password import valid_password
from models.account import Account from models.account import Account
from services.account_service import AccountService, RegisterService, TenantService from services.account_service import AccountService, RegisterService, TenantService
from services.billing_service import BillingService
from services.errors.account import AccountRegisterError
from services.errors.workspace import WorkSpaceNotAllowedCreateError from services.errors.workspace import WorkSpaceNotAllowedCreateError
from services.feature_service import FeatureService from services.feature_service import FeatureService
@ -44,6 +48,9 @@ class LoginApi(Resource):
parser.add_argument("language", type=str, required=False, default="en-US", location="json") parser.add_argument("language", type=str, required=False, default="en-US", location="json")
args = parser.parse_args() args = parser.parse_args()
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
raise AccountInFreezeError()
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"]) is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"])
if is_login_error_rate_limit: if is_login_error_rate_limit:
raise EmailPasswordLoginLimitError() raise EmailPasswordLoginLimitError()
@ -113,8 +120,10 @@ class ResetPasswordSendEmailApi(Resource):
language = "zh-Hans" language = "zh-Hans"
else: else:
language = "en-US" language = "en-US"
try:
account = AccountService.get_user_through_email(args["email"]) account = AccountService.get_user_through_email(args["email"])
except AccountRegisterError as are:
raise AccountInFreezeError()
if account is None: if account is None:
if FeatureService.get_system_features().is_allow_register: if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language) token = AccountService.send_reset_password_email(email=args["email"], language=language)
@ -142,8 +151,11 @@ class EmailCodeLoginSendEmailApi(Resource):
language = "zh-Hans" language = "zh-Hans"
else: else:
language = "en-US" language = "en-US"
try:
account = AccountService.get_user_through_email(args["email"])
except AccountRegisterError as are:
raise AccountInFreezeError()
account = AccountService.get_user_through_email(args["email"])
if account is None: if account is None:
if FeatureService.get_system_features().is_allow_register: if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_email_code_login_email(email=args["email"], language=language) token = AccountService.send_email_code_login_email(email=args["email"], language=language)
@ -177,7 +189,10 @@ class EmailCodeLoginApi(Resource):
raise EmailCodeError() raise EmailCodeError()
AccountService.revoke_email_code_login_token(args["token"]) AccountService.revoke_email_code_login_token(args["token"])
account = AccountService.get_user_through_email(user_email) try:
account = AccountService.get_user_through_email(user_email)
except AccountRegisterError as are:
raise AccountInFreezeError()
if account: if account:
tenant = TenantService.get_join_tenants(account) tenant = TenantService.get_join_tenants(account)
if not tenant: if not tenant:
@ -196,6 +211,8 @@ class EmailCodeLoginApi(Resource):
) )
except WorkSpaceNotAllowedCreateError: except WorkSpaceNotAllowedCreateError:
return NotAllowedCreateWorkspace() return NotAllowedCreateWorkspace()
except AccountRegisterError as are:
raise AccountInFreezeError()
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request)) token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(args["email"]) AccountService.reset_login_error_rate_limit(args["email"])
return {"result": "success", "data": token_pair.model_dump()} return {"result": "success", "data": token_pair.model_dump()}

View File

@ -4,7 +4,7 @@ from typing import Optional
import requests import requests
from flask import current_app, redirect, request from flask import current_app, redirect, request
from flask_restful import Resource from flask_restful import Resource # type: ignore
from werkzeug.exceptions import Unauthorized from werkzeug.exceptions import Unauthorized
from configs import dify_config from configs import dify_config
@ -16,7 +16,7 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
from models import Account from models import Account
from models.account import AccountStatus from models.account import AccountStatus
from services.account_service import AccountService, RegisterService, TenantService from services.account_service import AccountService, RegisterService, TenantService
from services.errors.account import AccountNotFoundError from services.errors.account import AccountNotFoundError, AccountRegisterError
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
from services.feature_service import FeatureService from services.feature_service import FeatureService
@ -76,8 +76,9 @@ class OAuthCallback(Resource):
try: try:
token = oauth_provider.get_access_token(code) token = oauth_provider.get_access_token(code)
user_info = oauth_provider.get_user_info(token) user_info = oauth_provider.get_user_info(token)
except requests.exceptions.HTTPError as e: except requests.exceptions.RequestException as e:
logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}") error_text = e.response.text if e.response else str(e)
logging.exception(f"An error occurred during the OAuth process with {provider}: {error_text}")
return {"error": "OAuth process failed"}, 400 return {"error": "OAuth process failed"}, 400
if invite_token and RegisterService.is_valid_invite_token(invite_token): if invite_token and RegisterService.is_valid_invite_token(invite_token):
@ -98,6 +99,8 @@ class OAuthCallback(Resource):
f"{dify_config.CONSOLE_WEB_URL}/signin" f"{dify_config.CONSOLE_WEB_URL}/signin"
"?message=Workspace not found, please contact system admin to invite you to join in a workspace." "?message=Workspace not found, please contact system admin to invite you to join in a workspace."
) )
except AccountRegisterError as e:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message={e.description}")
# Check account status # Check account status
if account.status == AccountStatus.BANNED.value: if account.status == AccountStatus.BANNED.value:
@ -129,7 +132,7 @@ class OAuthCallback(Resource):
def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> Optional[Account]: def _get_account_by_openid_or_email(provider: str, user_info: OAuthUserInfo) -> Optional[Account]:
account = Account.get_by_openid(provider, user_info.id) account: Optional[Account] = Account.get_by_openid(provider, user_info.id)
if not account: if not account:
account = Account.query.filter_by(email=user_info.email).first() account = Account.query.filter_by(email=user_info.email).first()

View File

@ -1,5 +1,5 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required from controllers.console.wraps import account_initialization_required, only_edition_cloud, setup_required

View File

@ -2,8 +2,8 @@ import datetime
import json import json
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.console import api from controllers.console import api
@ -218,7 +218,7 @@ class DataSourceNotionApi(Resource):
args["doc_form"], args["doc_form"],
args["doc_language"], args["doc_language"],
) )
return response, 200 return response.model_dump(), 200
class DataSourceNotionDatasetSyncApi(Resource): class DataSourceNotionDatasetSyncApi(Resource):

View File

@ -1,7 +1,7 @@
import flask_restful import flask_restful # type: ignore
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore # type: ignore
from flask_restful import Resource, marshal, marshal_with, reqparse from flask_restful import Resource, marshal, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
import services import services
@ -52,12 +52,12 @@ class DatasetListApi(Resource):
# provider = request.args.get("provider", default="vendor") # provider = request.args.get("provider", default="vendor")
search = request.args.get("keyword", default=None, type=str) search = request.args.get("keyword", default=None, type=str)
tag_ids = request.args.getlist("tag_ids") tag_ids = request.args.getlist("tag_ids")
include_all = request.args.get("include_all", default="false").lower() == "true"
if ids: if ids:
datasets, total = DatasetService.get_datasets_by_ids(ids, current_user.current_tenant_id) datasets, total = DatasetService.get_datasets_by_ids(ids, current_user.current_tenant_id)
else: else:
datasets, total = DatasetService.get_datasets( datasets, total = DatasetService.get_datasets(
page, limit, current_user.current_tenant_id, current_user, search, tag_ids page, limit, current_user.current_tenant_id, current_user, search, tag_ids, include_all
) )
# check embedding setting # check embedding setting
@ -457,14 +457,14 @@ class DatasetIndexingEstimateApi(Resource):
) )
except LLMBadRequestError: except LLMBadRequestError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "in the Settings -> Model Provider." "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
except Exception as e: except Exception as e:
raise IndexingEstimateError(str(e)) raise IndexingEstimateError(str(e))
return response, 200 return response.model_dump(), 200
class DatasetRelatedAppListApi(Resource): class DatasetRelatedAppListApi(Resource):
@ -619,9 +619,7 @@ class DatasetRetrievalSettingApi(Resource):
vector_type = dify_config.VECTOR_STORE vector_type = dify_config.VECTOR_STORE
match vector_type: match vector_type:
case ( case (
VectorType.MILVUS VectorType.RELYT
| VectorType.RELYT
| VectorType.PGVECTOR
| VectorType.TIDB_VECTOR | VectorType.TIDB_VECTOR
| VectorType.CHROMA | VectorType.CHROMA
| VectorType.TENCENT | VectorType.TENCENT
@ -640,10 +638,12 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.MYSCALE | VectorType.MYSCALE
| VectorType.ORACLE | VectorType.ORACLE
| VectorType.ELASTICSEARCH | VectorType.ELASTICSEARCH
| VectorType.ELASTICSEARCH_JA
| VectorType.PGVECTOR | VectorType.PGVECTOR
| VectorType.TIDB_ON_QDRANT | VectorType.TIDB_ON_QDRANT
| VectorType.LINDORM | VectorType.LINDORM
| VectorType.COUCHBASE | VectorType.COUCHBASE
| VectorType.MILVUS
): ):
return { return {
"retrieval_method": [ "retrieval_method": [
@ -683,6 +683,7 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.MYSCALE | VectorType.MYSCALE
| VectorType.ORACLE | VectorType.ORACLE
| VectorType.ELASTICSEARCH | VectorType.ELASTICSEARCH
| VectorType.ELASTICSEARCH_JA
| VectorType.COUCHBASE | VectorType.COUCHBASE
| VectorType.PGVECTOR | VectorType.PGVECTOR
| VectorType.LINDORM | VectorType.LINDORM
@ -733,6 +734,18 @@ class DatasetPermissionUserListApi(Resource):
}, 200 }, 200
class DatasetAutoDisableLogApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self, 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 DatasetService.get_dataset_auto_disable_logs(dataset_id_str), 200
api.add_resource(DatasetListApi, "/datasets") api.add_resource(DatasetListApi, "/datasets")
api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>") api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>")
api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check") api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check")
@ -747,3 +760,4 @@ api.add_resource(DatasetApiBaseUrlApi, "/datasets/api-base-info")
api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting") api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting")
api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>") api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>")
api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users") api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users")
api.add_resource(DatasetAutoDisableLogApi, "/datasets/<uuid:dataset_id>/auto-disable-logs")

View File

@ -1,12 +1,13 @@
import logging import logging
from argparse import ArgumentTypeError from argparse import ArgumentTypeError
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import cast
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, fields, marshal, marshal_with, reqparse from flask_restful import Resource, fields, marshal, marshal_with, reqparse # type: ignore
from sqlalchemy import asc, desc from sqlalchemy import asc, desc
from transformers.hf_argparser import string_to_bool from transformers.hf_argparser import string_to_bool # type: ignore
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
import services import services
@ -51,6 +52,7 @@ from fields.document_fields import (
from libs.login import login_required from libs.login import login_required
from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile
from services.dataset_service import DatasetService, DocumentService from services.dataset_service import DatasetService, DocumentService
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
from tasks.add_document_to_index_task import add_document_to_index_task from tasks.add_document_to_index_task import add_document_to_index_task
from tasks.remove_document_from_index_task import remove_document_from_index_task from tasks.remove_document_from_index_task import remove_document_from_index_task
@ -254,20 +256,23 @@ class DatasetDocumentListApi(Resource):
parser.add_argument("duplicate", type=bool, default=True, nullable=False, location="json") parser.add_argument("duplicate", type=bool, default=True, nullable=False, location="json")
parser.add_argument("original_document_id", type=str, required=False, location="json") parser.add_argument("original_document_id", type=str, required=False, location="json")
parser.add_argument("doc_form", type=str, default="text_model", required=False, nullable=False, location="json") parser.add_argument("doc_form", type=str, default="text_model", required=False, nullable=False, location="json")
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
parser.add_argument("embedding_model", type=str, required=False, nullable=True, location="json")
parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json")
parser.add_argument( parser.add_argument(
"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")
args = parser.parse_args() args = parser.parse_args()
knowledge_config = KnowledgeConfig(**args)
if not dataset.indexing_technique and not args["indexing_technique"]: if not dataset.indexing_technique and not knowledge_config.indexing_technique:
raise ValueError("indexing_technique is required.") raise ValueError("indexing_technique is required.")
# validate args # validate args
DocumentService.document_create_args_validate(args) DocumentService.document_create_args_validate(knowledge_config)
try: try:
documents, batch = DocumentService.save_document_with_dataset_id(dataset, args, current_user) documents, batch = DocumentService.save_document_with_dataset_id(dataset, knowledge_config, current_user)
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
except QuotaExceededError: except QuotaExceededError:
@ -277,6 +282,25 @@ class DatasetDocumentListApi(Resource):
return {"documents": documents, "batch": batch} return {"documents": documents, "batch": batch}
@setup_required
@login_required
@account_initialization_required
def delete(self, dataset_id):
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if dataset is None:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
try:
document_ids = request.args.getlist("document_id")
DocumentService.delete_documents(dataset, document_ids)
except services.errors.document.DocumentIndexingError:
raise DocumentIndexingError("Cannot delete document during indexing.")
return {"result": "success"}, 204
class DatasetInitApi(Resource): class DatasetInitApi(Resource):
@setup_required @setup_required
@ -312,9 +336,9 @@ class DatasetInitApi(Resource):
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator # The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor: if not current_user.is_dataset_editor:
raise Forbidden() raise Forbidden()
knowledge_config = KnowledgeConfig(**args)
if args["indexing_technique"] == "high_quality": if knowledge_config.indexing_technique == "high_quality":
if args["embedding_model"] is None or args["embedding_model_provider"] is None: if knowledge_config.embedding_model is None or knowledge_config.embedding_model_provider is None:
raise ValueError("embedding model and embedding model provider are required for high quality indexing.") raise ValueError("embedding model and embedding model provider are required for high quality indexing.")
try: try:
model_manager = ModelManager() model_manager = ModelManager()
@ -326,18 +350,17 @@ class DatasetInitApi(Resource):
) )
except InvokeAuthorizationError: except InvokeAuthorizationError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
"in the Settings -> Model Provider."
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
# validate args # validate args
DocumentService.document_create_args_validate(args) DocumentService.document_create_args_validate(knowledge_config)
try: try:
dataset, documents, batch = DocumentService.save_document_without_dataset_id( dataset, documents, batch = DocumentService.save_document_without_dataset_id(
tenant_id=current_user.current_tenant_id, document_data=args, account=current_user tenant_id=current_user.current_tenant_id, knowledge_config=knowledge_config, account=current_user
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
@ -390,7 +413,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
indexing_runner = IndexingRunner() indexing_runner = IndexingRunner()
try: try:
response = indexing_runner.indexing_estimate( estimate_response = indexing_runner.indexing_estimate(
current_user.current_tenant_id, current_user.current_tenant_id,
[extract_setting], [extract_setting],
data_process_rule_dict, data_process_rule_dict,
@ -398,6 +421,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
"English", "English",
dataset_id, dataset_id,
) )
return estimate_response.model_dump(), 200
except LLMBadRequestError: except LLMBadRequestError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "No Embedding Model available. Please configure a valid provider "
@ -408,7 +432,7 @@ class DocumentIndexingEstimateApi(DocumentResource):
except Exception as e: except Exception as e:
raise IndexingEstimateError(str(e)) raise IndexingEstimateError(str(e))
return response return response, 200
class DocumentBatchIndexingEstimateApi(DocumentResource): class DocumentBatchIndexingEstimateApi(DocumentResource):
@ -419,9 +443,8 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
batch = str(batch) batch = str(batch)
documents = self.get_batch_documents(dataset_id, batch) documents = self.get_batch_documents(dataset_id, batch)
response = {"tokens": 0, "total_price": 0, "currency": "USD", "total_segments": 0, "preview": []}
if not documents: if not documents:
return response return {"tokens": 0, "total_price": 0, "currency": "USD", "total_segments": 0, "preview": []}, 200
data_process_rule = documents[0].dataset_process_rule data_process_rule = documents[0].dataset_process_rule
data_process_rule_dict = data_process_rule.to_dict() data_process_rule_dict = data_process_rule.to_dict()
info_list = [] info_list = []
@ -499,16 +522,15 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
"English", "English",
dataset_id, dataset_id,
) )
return response.model_dump(), 200
except LLMBadRequestError: except LLMBadRequestError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
"in the Settings -> Model Provider."
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
except Exception as e: except Exception as e:
raise IndexingEstimateError(str(e)) raise IndexingEstimateError(str(e))
return response
class DocumentBatchIndexingStatusApi(DocumentResource): class DocumentBatchIndexingStatusApi(DocumentResource):
@ -581,7 +603,8 @@ class DocumentDetailApi(DocumentResource):
if metadata == "only": if metadata == "only":
response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata} response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata}
elif metadata == "without": elif metadata == "without":
process_rules = DatasetService.get_process_rules(dataset_id) dataset_process_rules = DatasetService.get_process_rules(dataset_id)
document_process_rules = document.dataset_process_rule.to_dict()
data_source_info = document.data_source_detail_dict data_source_info = document.data_source_detail_dict
response = { response = {
"id": document.id, "id": document.id,
@ -589,7 +612,8 @@ class DocumentDetailApi(DocumentResource):
"data_source_type": document.data_source_type, "data_source_type": document.data_source_type,
"data_source_info": data_source_info, "data_source_info": data_source_info,
"dataset_process_rule_id": document.dataset_process_rule_id, "dataset_process_rule_id": document.dataset_process_rule_id,
"dataset_process_rule": process_rules, "dataset_process_rule": dataset_process_rules,
"document_process_rule": document_process_rules,
"name": document.name, "name": document.name,
"created_from": document.created_from, "created_from": document.created_from,
"created_by": document.created_by, "created_by": document.created_by,
@ -612,7 +636,8 @@ class DocumentDetailApi(DocumentResource):
"doc_language": document.doc_language, "doc_language": document.doc_language,
} }
else: else:
process_rules = DatasetService.get_process_rules(dataset_id) dataset_process_rules = DatasetService.get_process_rules(dataset_id)
document_process_rules = document.dataset_process_rule.to_dict()
data_source_info = document.data_source_detail_dict data_source_info = document.data_source_detail_dict
response = { response = {
"id": document.id, "id": document.id,
@ -620,7 +645,8 @@ class DocumentDetailApi(DocumentResource):
"data_source_type": document.data_source_type, "data_source_type": document.data_source_type,
"data_source_info": data_source_info, "data_source_info": data_source_info,
"dataset_process_rule_id": document.dataset_process_rule_id, "dataset_process_rule_id": document.dataset_process_rule_id,
"dataset_process_rule": process_rules, "dataset_process_rule": dataset_process_rules,
"document_process_rule": document_process_rules,
"name": document.name, "name": document.name,
"created_from": document.created_from, "created_from": document.created_from,
"created_by": document.created_by, "created_by": document.created_by,
@ -733,8 +759,7 @@ class DocumentMetadataApi(DocumentResource):
if not isinstance(doc_metadata, dict): if not isinstance(doc_metadata, dict):
raise ValueError("doc_metadata must be a dictionary.") raise ValueError("doc_metadata must be a dictionary.")
metadata_schema: dict = cast(dict, DocumentService.DOCUMENT_METADATA_SCHEMA[doc_type])
metadata_schema = DocumentService.DOCUMENT_METADATA_SCHEMA[doc_type]
document.doc_metadata = {} document.doc_metadata = {}
if doc_type == "others": if doc_type == "others":
@ -757,9 +782,8 @@ class DocumentStatusApi(DocumentResource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_resource_check("vector_space")
def patch(self, dataset_id, document_id, action): def patch(self, dataset_id, action):
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
document_id = str(document_id)
dataset = DatasetService.get_dataset(dataset_id) dataset = DatasetService.get_dataset(dataset_id)
if dataset is None: if dataset is None:
raise NotFound("Dataset not found.") raise NotFound("Dataset not found.")
@ -774,84 +798,79 @@ class DocumentStatusApi(DocumentResource):
# check user's permission # check user's permission
DatasetService.check_dataset_permission(dataset, current_user) DatasetService.check_dataset_permission(dataset, current_user)
document = self.get_document(dataset_id, document_id) document_ids = request.args.getlist("document_id")
for document_id in document_ids:
document = self.get_document(dataset_id, document_id)
indexing_cache_key = "document_{}_indexing".format(document.id) indexing_cache_key = "document_{}_indexing".format(document.id)
cache_result = redis_client.get(indexing_cache_key) cache_result = redis_client.get(indexing_cache_key)
if cache_result is not None: if cache_result is not None:
raise InvalidActionError("Document is being indexed, please try again later") raise InvalidActionError(f"Document:{document.name} is being indexed, please try again later")
if action == "enable": if action == "enable":
if document.enabled: if document.enabled:
raise InvalidActionError("Document already enabled.") continue
document.enabled = True
document.disabled_at = None
document.disabled_by = None
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
document.enabled = True # Set cache to prevent indexing the same document multiple times
document.disabled_at = None redis_client.setex(indexing_cache_key, 600, 1)
document.disabled_by = None
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
# Set cache to prevent indexing the same document multiple times add_document_to_index_task.delay(document_id)
redis_client.setex(indexing_cache_key, 600, 1)
add_document_to_index_task.delay(document_id) elif action == "disable":
if not document.completed_at or document.indexing_status != "completed":
raise InvalidActionError(f"Document: {document.name} is not completed.")
if not document.enabled:
continue
return {"result": "success"}, 200 document.enabled = False
document.disabled_at = datetime.now(UTC).replace(tzinfo=None)
document.disabled_by = current_user.id
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
elif action == "disable":
if not document.completed_at or document.indexing_status != "completed":
raise InvalidActionError("Document is not completed.")
if not document.enabled:
raise InvalidActionError("Document already disabled.")
document.enabled = False
document.disabled_at = datetime.now(UTC).replace(tzinfo=None)
document.disabled_by = current_user.id
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
# Set cache to prevent indexing the same document multiple times
redis_client.setex(indexing_cache_key, 600, 1)
remove_document_from_index_task.delay(document_id)
return {"result": "success"}, 200
elif action == "archive":
if document.archived:
raise InvalidActionError("Document already archived.")
document.archived = True
document.archived_at = datetime.now(UTC).replace(tzinfo=None)
document.archived_by = current_user.id
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
if document.enabled:
# Set cache to prevent indexing the same document multiple times # Set cache to prevent indexing the same document multiple times
redis_client.setex(indexing_cache_key, 600, 1) redis_client.setex(indexing_cache_key, 600, 1)
remove_document_from_index_task.delay(document_id) remove_document_from_index_task.delay(document_id)
return {"result": "success"}, 200 elif action == "archive":
elif action == "un_archive": if document.archived:
if not document.archived: continue
raise InvalidActionError("Document is not archived.")
document.archived = False document.archived = True
document.archived_at = None document.archived_at = datetime.now(UTC).replace(tzinfo=None)
document.archived_by = None document.archived_by = current_user.id
document.updated_at = datetime.now(UTC).replace(tzinfo=None) document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit() db.session.commit()
# Set cache to prevent indexing the same document multiple times if document.enabled:
redis_client.setex(indexing_cache_key, 600, 1) # Set cache to prevent indexing the same document multiple times
redis_client.setex(indexing_cache_key, 600, 1)
add_document_to_index_task.delay(document_id) remove_document_from_index_task.delay(document_id)
return {"result": "success"}, 200 elif action == "un_archive":
else: if not document.archived:
raise InvalidActionError() continue
document.archived = False
document.archived_at = None
document.archived_by = None
document.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
# Set cache to prevent indexing the same document multiple times
redis_client.setex(indexing_cache_key, 600, 1)
add_document_to_index_task.delay(document_id)
else:
raise InvalidActionError()
return {"result": "success"}, 200
class DocumentPauseApi(DocumentResource): class DocumentPauseApi(DocumentResource):
@ -1022,7 +1041,7 @@ api.add_resource(
) )
api.add_resource(DocumentDeleteApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>") api.add_resource(DocumentDeleteApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata") api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
api.add_resource(DocumentStatusApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/status/<string:action>") api.add_resource(DocumentStatusApi, "/datasets/<uuid:dataset_id>/documents/status/<string:action>/batch")
api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause") api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause")
api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume") api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry") api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry")

View File

@ -1,16 +1,21 @@
import uuid import uuid
from datetime import UTC, datetime
import pandas as pd import pandas as pd
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal, reqparse from flask_restful import Resource, marshal, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
import services import services
from controllers.console import api from controllers.console import api
from controllers.console.app.error import ProviderNotInitializeError from controllers.console.app.error import ProviderNotInitializeError
from controllers.console.datasets.error import InvalidActionError, NoFileUploadedError, TooManyFilesError from controllers.console.datasets.error import (
ChildChunkDeleteIndexError,
ChildChunkIndexingError,
InvalidActionError,
NoFileUploadedError,
TooManyFilesError,
)
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
cloud_edition_billing_knowledge_limit_check, cloud_edition_billing_knowledge_limit_check,
@ -20,15 +25,15 @@ from controllers.console.wraps import (
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 ModelType from core.model_runtime.entities.model_entities import ModelType
from extensions.ext_database import db
from extensions.ext_redis import redis_client from extensions.ext_redis import redis_client
from fields.segment_fields import segment_fields from fields.segment_fields import child_chunk_fields, segment_fields
from libs.login import login_required from libs.login import login_required
from models import DocumentSegment from models.dataset import ChildChunk, DocumentSegment
from services.dataset_service import DatasetService, DocumentService, SegmentService from services.dataset_service import DatasetService, DocumentService, SegmentService
from services.entities.knowledge_entities.knowledge_entities import ChildChunkUpdateArgs, SegmentUpdateArgs
from services.errors.chunk import ChildChunkDeleteIndexError as ChildChunkDeleteIndexServiceError
from services.errors.chunk import ChildChunkIndexingError as ChildChunkIndexingServiceError
from tasks.batch_create_segment_to_index_task import batch_create_segment_to_index_task from tasks.batch_create_segment_to_index_task import batch_create_segment_to_index_task
from tasks.disable_segment_from_index_task import disable_segment_from_index_task
from tasks.enable_segment_to_index_task import enable_segment_to_index_task
class DatasetDocumentSegmentListApi(Resource): class DatasetDocumentSegmentListApi(Resource):
@ -53,15 +58,16 @@ class DatasetDocumentSegmentListApi(Resource):
raise NotFound("Document not found.") raise NotFound("Document not found.")
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("last_id", type=str, default=None, location="args")
parser.add_argument("limit", type=int, default=20, location="args") parser.add_argument("limit", type=int, default=20, location="args")
parser.add_argument("status", type=str, action="append", default=[], location="args") parser.add_argument("status", type=str, action="append", default=[], location="args")
parser.add_argument("hit_count_gte", type=int, default=None, location="args") parser.add_argument("hit_count_gte", type=int, default=None, location="args")
parser.add_argument("enabled", type=str, default="all", location="args") parser.add_argument("enabled", type=str, default="all", location="args")
parser.add_argument("keyword", type=str, default=None, location="args") parser.add_argument("keyword", type=str, default=None, location="args")
parser.add_argument("page", type=int, default=1, location="args")
args = parser.parse_args() args = parser.parse_args()
last_id = args["last_id"] page = args["page"]
limit = min(args["limit"], 100) limit = min(args["limit"], 100)
status_list = args["status"] status_list = args["status"]
hit_count_gte = args["hit_count_gte"] hit_count_gte = args["hit_count_gte"]
@ -69,14 +75,7 @@ class DatasetDocumentSegmentListApi(Resource):
query = DocumentSegment.query.filter( query = DocumentSegment.query.filter(
DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id
) ).order_by(DocumentSegment.position.asc())
if last_id is not None:
last_segment = db.session.get(DocumentSegment, str(last_id))
if last_segment:
query = query.filter(DocumentSegment.position > last_segment.position)
else:
return {"data": [], "has_more": False, "limit": limit}, 200
if status_list: if status_list:
query = query.filter(DocumentSegment.status.in_(status_list)) query = query.filter(DocumentSegment.status.in_(status_list))
@ -93,21 +92,44 @@ class DatasetDocumentSegmentListApi(Resource):
elif args["enabled"].lower() == "false": elif args["enabled"].lower() == "false":
query = query.filter(DocumentSegment.enabled == False) query = query.filter(DocumentSegment.enabled == False)
total = query.count() segments = query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
segments = query.order_by(DocumentSegment.position).limit(limit + 1).all()
has_more = False response = {
if len(segments) > limit: "data": marshal(segments.items, segment_fields),
has_more = True
segments = segments[:-1]
return {
"data": marshal(segments, segment_fields),
"doc_form": document.doc_form,
"has_more": has_more,
"limit": limit, "limit": limit,
"total": total, "total": segments.total,
}, 200 "total_pages": segments.pages,
"page": page,
}
return response, 200
@setup_required
@login_required
@account_initialization_required
def delete(self, dataset_id, document_id):
# check dataset
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
segment_ids = request.args.getlist("segment_id")
# The role of the current user in the ta table must be admin or owner
if not current_user.is_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
SegmentService.delete_segments(segment_ids, document, dataset)
return {"result": "success"}, 200
class DatasetDocumentSegmentApi(Resource): class DatasetDocumentSegmentApi(Resource):
@ -115,11 +137,15 @@ class DatasetDocumentSegmentApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_resource_check("vector_space")
def patch(self, dataset_id, segment_id, action): def patch(self, dataset_id, document_id, action):
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id) dataset = DatasetService.get_dataset(dataset_id)
if not dataset: if not dataset:
raise NotFound("Dataset not found.") raise NotFound("Dataset not found.")
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check user's model setting # check user's model setting
DatasetService.check_dataset_model_setting(dataset) DatasetService.check_dataset_model_setting(dataset)
# The role of the current user in the ta table must be admin, owner, or editor # The role of the current user in the ta table must be admin, owner, or editor
@ -142,64 +168,21 @@ class DatasetDocumentSegmentApi(Resource):
) )
except LLMBadRequestError: except LLMBadRequestError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
"in the Settings -> Model Provider."
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
segment_ids = request.args.getlist("segment_id")
segment = DocumentSegment.query.filter( document_indexing_cache_key = "document_{}_indexing".format(document.id)
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
if not segment:
raise NotFound("Segment not found.")
if segment.status != "completed":
raise NotFound("Segment is not completed, enable or disable function is not allowed")
document_indexing_cache_key = "document_{}_indexing".format(segment.document_id)
cache_result = redis_client.get(document_indexing_cache_key) cache_result = redis_client.get(document_indexing_cache_key)
if cache_result is not None: if cache_result is not None:
raise InvalidActionError("Document is being indexed, please try again later") raise InvalidActionError("Document is being indexed, please try again later")
try:
indexing_cache_key = "segment_{}_indexing".format(segment.id) SegmentService.update_segments_status(segment_ids, action, dataset, document)
cache_result = redis_client.get(indexing_cache_key) except Exception as e:
if cache_result is not None: raise InvalidActionError(str(e))
raise InvalidActionError("Segment is being indexed, please try again later") return {"result": "success"}, 200
if action == "enable":
if segment.enabled:
raise InvalidActionError("Segment is already enabled.")
segment.enabled = True
segment.disabled_at = None
segment.disabled_by = None
db.session.commit()
# Set cache to prevent indexing the same segment multiple times
redis_client.setex(indexing_cache_key, 600, 1)
enable_segment_to_index_task.delay(segment.id)
return {"result": "success"}, 200
elif action == "disable":
if not segment.enabled:
raise InvalidActionError("Segment is already disabled.")
segment.enabled = False
segment.disabled_at = datetime.now(UTC).replace(tzinfo=None)
segment.disabled_by = current_user.id
db.session.commit()
# Set cache to prevent indexing the same segment multiple times
redis_client.setex(indexing_cache_key, 600, 1)
disable_segment_from_index_task.delay(segment.id)
return {"result": "success"}, 200
else:
raise InvalidActionError()
class DatasetDocumentSegmentAddApi(Resource): class DatasetDocumentSegmentAddApi(Resource):
@ -233,8 +216,7 @@ class DatasetDocumentSegmentAddApi(Resource):
) )
except LLMBadRequestError: except LLMBadRequestError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
"in the Settings -> Model Provider."
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
@ -283,8 +265,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
) )
except LLMBadRequestError: except LLMBadRequestError:
raise ProviderNotInitializeError( raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider " "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
"in the Settings -> Model Provider."
) )
except ProviderTokenNotInitError as ex: except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description) raise ProviderNotInitializeError(ex.description)
@ -307,9 +288,12 @@ class DatasetDocumentSegmentUpdateApi(Resource):
parser.add_argument("content", type=str, required=True, nullable=False, location="json") parser.add_argument("content", type=str, required=True, nullable=False, location="json")
parser.add_argument("answer", type=str, required=False, nullable=True, location="json") parser.add_argument("answer", type=str, required=False, nullable=True, location="json")
parser.add_argument("keywords", type=list, required=False, nullable=True, location="json") parser.add_argument("keywords", type=list, required=False, nullable=True, location="json")
parser.add_argument(
"regenerate_child_chunks", type=bool, required=False, nullable=True, default=False, location="json"
)
args = parser.parse_args() args = parser.parse_args()
SegmentService.segment_create_args_validate(args, document) SegmentService.segment_create_args_validate(args, document)
segment = SegmentService.update_segment(args, segment, document, dataset) segment = SegmentService.update_segment(SegmentUpdateArgs(**args), segment, document, dataset)
return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200 return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200
@setup_required @setup_required
@ -381,9 +365,9 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
result = [] result = []
for index, row in df.iterrows(): for index, row in df.iterrows():
if document.doc_form == "qa_model": if document.doc_form == "qa_model":
data = {"content": row[0], "answer": row[1]} data = {"content": row.iloc[0], "answer": row.iloc[1]}
else: else:
data = {"content": row[0]} data = {"content": row.iloc[0]}
result.append(data) result.append(data)
if len(result) == 0: if len(result) == 0:
raise ValueError("The CSV file is empty.") raise ValueError("The CSV file is empty.")
@ -412,8 +396,247 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
return {"job_id": job_id, "job_status": cache_result.decode()}, 200 return {"job_id": job_id, "job_status": cache_result.decode()}, 200
class ChildChunkAddApi(Resource):
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_knowledge_limit_check("add_segment")
def post(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
if not segment:
raise NotFound("Segment not found.")
if not current_user.is_editor:
raise Forbidden()
# check embedding model setting
if dataset.indexing_technique == "high_quality":
try:
model_manager = ModelManager()
model_manager.get_model_instance(
tenant_id=current_user.current_tenant_id,
provider=dataset.embedding_model_provider,
model_type=ModelType.TEXT_EMBEDDING,
model=dataset.embedding_model,
)
except LLMBadRequestError:
raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
parser = reqparse.RequestParser()
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
child_chunk = SegmentService.create_child_chunk(args.get("content"), segment, document, dataset)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
@setup_required
@login_required
@account_initialization_required
def get(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
if not segment:
raise NotFound("Segment not found.")
parser = reqparse.RequestParser()
parser.add_argument("limit", type=int, default=20, location="args")
parser.add_argument("keyword", type=str, default=None, location="args")
parser.add_argument("page", type=int, default=1, location="args")
args = parser.parse_args()
page = args["page"]
limit = min(args["limit"], 100)
keyword = args["keyword"]
child_chunks = SegmentService.get_child_chunks(segment_id, document_id, dataset_id, page, limit, keyword)
return {
"data": marshal(child_chunks.items, child_chunk_fields),
"total": child_chunks.total,
"total_pages": child_chunks.pages,
"page": page,
"limit": limit,
}, 200
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
def patch(self, dataset_id, document_id, segment_id):
# check dataset
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
if not segment:
raise NotFound("Segment not found.")
# The role of the current user in the ta table must be admin, owner, or editor
if not current_user.is_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
parser = reqparse.RequestParser()
parser.add_argument("chunks", type=list, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
chunks = [ChildChunkUpdateArgs(**chunk) for chunk in args.get("chunks")]
child_chunks = SegmentService.update_child_chunks(chunks, segment, document, dataset)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunks, child_chunk_fields)}, 200
class ChildChunkUpdateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
# check dataset
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
if not segment:
raise NotFound("Segment not found.")
# check child chunk
child_chunk_id = str(child_chunk_id)
child_chunk = ChildChunk.query.filter(
ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id
).first()
if not child_chunk:
raise NotFound("Child chunk not found.")
# The role of the current user in the ta table must be admin or owner
if not current_user.is_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
try:
SegmentService.delete_child_chunk(child_chunk, dataset)
except ChildChunkDeleteIndexServiceError as e:
raise ChildChunkDeleteIndexError(str(e))
return {"result": "success"}, 200
@setup_required
@login_required
@account_initialization_required
@cloud_edition_billing_resource_check("vector_space")
def patch(self, dataset_id, document_id, segment_id, child_chunk_id):
# check dataset
dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id)
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
if not segment:
raise NotFound("Segment not found.")
# check child chunk
child_chunk_id = str(child_chunk_id)
child_chunk = ChildChunk.query.filter(
ChildChunk.id == str(child_chunk_id), ChildChunk.tenant_id == current_user.current_tenant_id
).first()
if not child_chunk:
raise NotFound("Child chunk not found.")
# The role of the current user in the ta table must be admin or owner
if not current_user.is_editor:
raise Forbidden()
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
parser = reqparse.RequestParser()
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
child_chunk = SegmentService.update_child_chunk(
args.get("content"), child_chunk, segment, document, dataset
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
api.add_resource(DatasetDocumentSegmentListApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments") api.add_resource(DatasetDocumentSegmentListApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments")
api.add_resource(DatasetDocumentSegmentApi, "/datasets/<uuid:dataset_id>/segments/<uuid:segment_id>/<string:action>") api.add_resource(
DatasetDocumentSegmentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segment/<string:action>"
)
api.add_resource(DatasetDocumentSegmentAddApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segment") api.add_resource(DatasetDocumentSegmentAddApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segment")
api.add_resource( api.add_resource(
DatasetDocumentSegmentUpdateApi, DatasetDocumentSegmentUpdateApi,
@ -424,3 +647,11 @@ api.add_resource(
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/batch_import", "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/batch_import",
"/datasets/batch_import_status/<uuid:job_id>", "/datasets/batch_import_status/<uuid:job_id>",
) )
api.add_resource(
ChildChunkAddApi,
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks",
)
api.add_resource(
ChildChunkUpdateApi,
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks/<uuid:child_chunk_id>",
)

View File

@ -89,3 +89,15 @@ class IndexingEstimateError(BaseHTTPException):
error_code = "indexing_estimate_error" error_code = "indexing_estimate_error"
description = "Knowledge indexing estimate failed: {message}" description = "Knowledge indexing estimate failed: {message}"
code = 500 code = 500
class ChildChunkIndexingError(BaseHTTPException):
error_code = "child_chunk_indexing_error"
description = "Create child chunk index failed: {message}"
code = 500
class ChildChunkDeleteIndexError(BaseHTTPException):
error_code = "child_chunk_delete_index_error"
description = "Delete child chunk index failed: {message}"
code = 500

View File

@ -1,6 +1,6 @@
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal, reqparse from flask_restful import Resource, marshal, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services import services

View File

@ -1,4 +1,4 @@
from flask_restful import Resource from flask_restful import Resource # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase

View File

@ -1,7 +1,7 @@
import logging import logging
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import marshal, reqparse from flask_restful import marshal, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services.dataset_service import services.dataset_service

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from controllers.console import api from controllers.console import api
from controllers.console.datasets.error import WebsiteCrawlError from controllers.console.datasets.error import WebsiteCrawlError

View File

@ -92,3 +92,12 @@ class UnauthorizedAndForceLogout(BaseHTTPException):
error_code = "unauthorized_and_force_logout" error_code = "unauthorized_and_force_logout"
description = "Unauthorized and force logout." description = "Unauthorized and force logout."
code = 401 code = 401
class AccountInFreezeError(BaseHTTPException):
error_code = "account_in_freeze"
code = 400
description = (
"This email account has been deleted within the past 30 days"
"and is temporarily unavailable for new account registration."
)

View File

@ -4,7 +4,6 @@ from flask import request
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
import services import services
from controllers.console import api
from controllers.console.app.error import ( from controllers.console.app.error import (
AppUnavailableError, AppUnavailableError,
AudioTooLargeError, AudioTooLargeError,
@ -67,7 +66,7 @@ class ChatAudioApi(InstalledAppResource):
class ChatTextApi(InstalledAppResource): class ChatTextApi(InstalledAppResource):
def post(self, installed_app): def post(self, installed_app):
from flask_restful import reqparse from flask_restful import reqparse # type: ignore
app_model = installed_app.app app_model = installed_app.app
try: try:
@ -118,9 +117,3 @@ class ChatTextApi(InstalledAppResource):
except Exception as e: except Exception as e:
logging.exception("internal server error.") logging.exception("internal server error.")
raise InternalServerError() raise InternalServerError()
api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio")
api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text")
# api.add_resource(ChatTextApiWithMessageId, '/installed-apps/<uuid:installed_app_id>/text-to-audio/message-id',
# endpoint='installed_app_text_with_message_id')

View File

@ -1,12 +1,11 @@
import logging import logging
from datetime import UTC, datetime from datetime import UTC, datetime
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import reqparse from flask_restful import reqparse # type: ignore
from werkzeug.exceptions import InternalServerError, NotFound from werkzeug.exceptions import InternalServerError, NotFound
import services import services
from controllers.console import api
from controllers.console.app.error import ( from controllers.console.app.error import (
AppUnavailableError, AppUnavailableError,
CompletionRequestError, CompletionRequestError,
@ -19,7 +18,11 @@ from controllers.console.explore.error import NotChatAppError, NotCompletionAppE
from controllers.console.explore.wraps import InstalledAppResource from controllers.console.explore.wraps import InstalledAppResource
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 ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.errors.error import (
ModelCurrentlyNotSupportError,
ProviderTokenNotInitError,
QuotaExceededError,
)
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from extensions.ext_database import db from extensions.ext_database import db
from libs import helper from libs import helper
@ -147,21 +150,3 @@ class ChatStopApi(InstalledAppResource):
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id) AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)
return {"result": "success"}, 200 return {"result": "success"}, 200
api.add_resource(
CompletionApi, "/installed-apps/<uuid:installed_app_id>/completion-messages", endpoint="installed_app_completion"
)
api.add_resource(
CompletionStopApi,
"/installed-apps/<uuid:installed_app_id>/completion-messages/<string:task_id>/stop",
endpoint="installed_app_stop_completion",
)
api.add_resource(
ChatApi, "/installed-apps/<uuid:installed_app_id>/chat-messages", endpoint="installed_app_chat_completion"
)
api.add_resource(
ChatStopApi,
"/installed-apps/<uuid:installed_app_id>/chat-messages/<string:task_id>/stop",
endpoint="installed_app_stop_chat_completion",
)

View File

@ -1,12 +1,13 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import marshal_with, reqparse from flask_restful import marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.console import api
from controllers.console.explore.error import NotChatAppError from controllers.console.explore.error import NotChatAppError
from controllers.console.explore.wraps import InstalledAppResource from controllers.console.explore.wraps import InstalledAppResource
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 fields.conversation_fields import conversation_infinite_scroll_pagination_fields, simple_conversation_fields from fields.conversation_fields import conversation_infinite_scroll_pagination_fields, simple_conversation_fields
from libs.helper import uuid_value from libs.helper import uuid_value
from models.model import AppMode from models.model import AppMode
@ -31,17 +32,19 @@ class ConversationListApi(InstalledAppResource):
pinned = None pinned = None
if "pinned" in args and args["pinned"] is not None: if "pinned" in args and args["pinned"] is not None:
pinned = True if args["pinned"] == "true" else False pinned = args["pinned"] == "true"
try: try:
return WebConversationService.pagination_by_last_id( with Session(db.engine) as session:
app_model=app_model, return WebConversationService.pagination_by_last_id(
user=current_user, session=session,
last_id=args["last_id"], app_model=app_model,
limit=args["limit"], user=current_user,
invoke_from=InvokeFrom.EXPLORE, last_id=args["last_id"],
pinned=pinned, limit=args["limit"],
) invoke_from=InvokeFrom.EXPLORE,
pinned=pinned,
)
except LastConversationNotExistsError: except LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.") raise NotFound("Last Conversation Not Exists.")
@ -114,28 +117,3 @@ class ConversationUnPinApi(InstalledAppResource):
WebConversationService.unpin(app_model, conversation_id, current_user) WebConversationService.unpin(app_model, conversation_id, current_user)
return {"result": "success"} return {"result": "success"}
api.add_resource(
ConversationRenameApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/name",
endpoint="installed_app_conversation_rename",
)
api.add_resource(
ConversationListApi, "/installed-apps/<uuid:installed_app_id>/conversations", endpoint="installed_app_conversations"
)
api.add_resource(
ConversationApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>",
endpoint="installed_app_conversation",
)
api.add_resource(
ConversationPinApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/pin",
endpoint="installed_app_conversation_pin",
)
api.add_resource(
ConversationUnPinApi,
"/installed-apps/<uuid:installed_app_id>/conversations/<uuid:c_id>/unpin",
endpoint="installed_app_conversation_unpin",
)

View File

@ -1,8 +1,9 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, inputs, marshal_with, reqparse from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
from sqlalchemy import and_ from sqlalchemy import and_
from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.exceptions import BadRequest, Forbidden, NotFound
@ -34,7 +35,7 @@ class InstalledAppsListApi(Resource):
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all() installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant) current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
installed_apps = [ installed_app_list: list[dict[str, Any]] = [
{ {
"id": installed_app.id, "id": installed_app.id,
"app": installed_app.app, "app": installed_app.app,
@ -47,7 +48,7 @@ class InstalledAppsListApi(Resource):
for installed_app in installed_apps for installed_app in installed_apps
if installed_app.app is not None if installed_app.app is not None
] ]
installed_apps.sort( installed_app_list.sort(
key=lambda app: ( key=lambda app: (
-app["is_pinned"], -app["is_pinned"],
app["last_used_at"] is None, app["last_used_at"] is None,
@ -55,7 +56,7 @@ class InstalledAppsListApi(Resource):
) )
) )
return {"installed_apps": installed_apps} return {"installed_apps": installed_app_list}
@login_required @login_required
@account_initialization_required @account_initialization_required

View File

@ -1,12 +1,11 @@
import logging import logging
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import marshal_with, reqparse from flask_restful import marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from werkzeug.exceptions import InternalServerError, NotFound from werkzeug.exceptions import InternalServerError, NotFound
import services import services
from controllers.console import api
from controllers.console.app.error import ( from controllers.console.app.error import (
AppMoreLikeThisDisabledError, AppMoreLikeThisDisabledError,
CompletionRequestError, CompletionRequestError,
@ -51,7 +50,7 @@ class MessageListApi(InstalledAppResource):
try: try:
return MessageService.pagination_by_first_id( return MessageService.pagination_by_first_id(
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"], "desc" app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
) )
except services.errors.conversation.ConversationNotExistsError: except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.") raise NotFound("Conversation Not Exists.")
@ -67,10 +66,17 @@ class MessageFeedbackApi(InstalledAppResource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json") parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
parser.add_argument("content", type=str, location="json")
args = parser.parse_args() args = parser.parse_args()
try: try:
MessageService.create_feedback(app_model, message_id, current_user, args["rating"]) MessageService.create_feedback(
app_model=app_model,
message_id=message_id,
user=current_user,
rating=args.get("rating"),
content=args.get("content"),
)
except services.errors.message.MessageNotExistsError: except services.errors.message.MessageNotExistsError:
raise NotFound("Message Not Exists.") raise NotFound("Message Not Exists.")
@ -153,21 +159,3 @@ class MessageSuggestedQuestionApi(InstalledAppResource):
raise InternalServerError() raise InternalServerError()
return {"data": questions} return {"data": questions}
api.add_resource(MessageListApi, "/installed-apps/<uuid:installed_app_id>/messages", endpoint="installed_app_messages")
api.add_resource(
MessageFeedbackApi,
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/feedbacks",
endpoint="installed_app_message_feedback",
)
api.add_resource(
MessageMoreLikeThisApi,
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/more-like-this",
endpoint="installed_app_more_like_this",
)
api.add_resource(
MessageSuggestedQuestionApi,
"/installed-apps/<uuid:installed_app_id>/messages/<uuid:message_id>/suggested-questions",
endpoint="installed_app_suggested_question",
)

View File

@ -1,4 +1,4 @@
from flask_restful import marshal_with from flask_restful import marshal_with # type: ignore
from controllers.common import fields from controllers.common import fields
from controllers.common import helpers as controller_helpers from controllers.common import helpers as controller_helpers

View File

@ -1,5 +1,5 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from constants.languages import languages from constants.languages import languages
from controllers.console import api from controllers.console import api

View File

@ -1,6 +1,6 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import fields, marshal_with, reqparse from flask_restful import fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.console import api from controllers.console import api

View File

@ -1,9 +1,8 @@
import logging import logging
from flask_restful import reqparse from flask_restful import reqparse # type: ignore
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
from controllers.console import api
from controllers.console.app.error import ( from controllers.console.app.error import (
CompletionRequestError, CompletionRequestError,
ProviderModelCurrentlyNotSupportError, ProviderModelCurrentlyNotSupportError,
@ -14,7 +13,11 @@ from controllers.console.explore.error import NotWorkflowAppError
from controllers.console.explore.wraps import InstalledAppResource from controllers.console.explore.wraps import InstalledAppResource
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 ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.errors.error import (
ModelCurrentlyNotSupportError,
ProviderTokenNotInitError,
QuotaExceededError,
)
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from libs import helper from libs import helper
from libs.login import current_user from libs.login import current_user
@ -73,9 +76,3 @@ class InstalledAppWorkflowTaskStopApi(InstalledAppResource):
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id) AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)
return {"result": "success"} return {"result": "success"}
api.add_resource(InstalledAppWorkflowRunApi, "/installed-apps/<uuid:installed_app_id>/workflows/run")
api.add_resource(
InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop"
)

View File

@ -1,7 +1,7 @@
from functools import wraps from functools import wraps
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource from flask_restful import Resource # type: ignore
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required

View File

@ -1,5 +1,5 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from constants import HIDDEN_VALUE from constants import HIDDEN_VALUE
from controllers.console import api from controllers.console import api

View File

@ -1,5 +1,5 @@
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource from flask_restful import Resource # type: ignore
from libs.login import login_required from libs.login import login_required
from services.feature_service import FeatureService from services.feature_service import FeatureService

View File

@ -1,6 +1,8 @@
from typing import Literal
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with from flask_restful import Resource, marshal_with # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
import services import services
@ -48,7 +50,8 @@ class FileApi(Resource):
@cloud_edition_billing_resource_check("documents") @cloud_edition_billing_resource_check("documents")
def post(self): def post(self):
file = request.files["file"] file = request.files["file"]
source = request.form.get("source") source_str = request.form.get("source")
source: Literal["datasets"] | None = "datasets" if source_str == "datasets" else None
if "file" not in request.files: if "file" not in request.files:
raise NoFileUploadedError() raise NoFileUploadedError()

View File

@ -1,7 +1,7 @@
import os import os
from flask import session from flask import session
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from configs import dify_config from configs import dify_config
from libs.helper import StrLen from libs.helper import StrLen

View File

@ -1,4 +1,4 @@
from flask_restful import Resource from flask_restful import Resource # type: ignore
from controllers.console import api from controllers.console import api

View File

@ -2,11 +2,12 @@ import urllib.parse
from typing import cast from typing import cast
import httpx import httpx
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
import services import services
from controllers.common import helpers from controllers.common import helpers
from controllers.common.errors import RemoteFileUploadError
from core.file import helpers as file_helpers from core.file import helpers as file_helpers
from core.helper import ssrf_proxy from core.helper import ssrf_proxy
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
@ -43,10 +44,14 @@ class RemoteFileUploadApi(Resource):
url = args["url"] url = args["url"]
resp = ssrf_proxy.head(url=url) try:
if resp.status_code != httpx.codes.OK: resp = ssrf_proxy.head(url=url)
resp = ssrf_proxy.get(url=url, timeout=3, follow_redirects=True) if resp.status_code != httpx.codes.OK:
resp.raise_for_status() resp = ssrf_proxy.get(url=url, timeout=3, follow_redirects=True)
if resp.status_code != httpx.codes.OK:
raise RemoteFileUploadError(f"Failed to fetch file from {url}: {resp.text}")
except httpx.RequestError as e:
raise RemoteFileUploadError(f"Failed to fetch file from {url}: {str(e)}")
file_info = helpers.guess_file_info_from_response(resp) file_info = helpers.guess_file_info_from_response(resp)

View File

@ -1,5 +1,5 @@
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from configs import dify_config from configs import dify_config
from libs.helper import StrLen, email, extract_remote_ip from libs.helper import StrLen, email, extract_remote_ip

View File

@ -1,6 +1,6 @@
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.console import api from controllers.console import api
@ -23,7 +23,7 @@ class TagListApi(Resource):
@account_initialization_required @account_initialization_required
@marshal_with(tag_fields) @marshal_with(tag_fields)
def get(self): def get(self):
tag_type = request.args.get("type", type=str) tag_type = request.args.get("type", type=str, default="")
keyword = request.args.get("keyword", default=None, type=str) keyword = request.args.get("keyword", default=None, type=str)
tags = TagService.get_tags(tag_type, current_user.current_tenant_id, keyword) tags = TagService.get_tags(tag_type, current_user.current_tenant_id, keyword)

View File

@ -2,7 +2,7 @@ import json
import logging import logging
import requests import requests
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from packaging import version from packaging import version
from configs import dify_config from configs import dify_config

View File

@ -2,8 +2,8 @@ import datetime
import pytz import pytz
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from configs import dify_config from configs import dify_config
from constants.languages import supported_language from constants.languages import supported_language
@ -11,6 +11,7 @@ from controllers.console import api
from controllers.console.workspace.error import ( from controllers.console.workspace.error import (
AccountAlreadyInitedError, AccountAlreadyInitedError,
CurrentPasswordIncorrectError, CurrentPasswordIncorrectError,
InvalidAccountDeletionCodeError,
InvalidInvitationCodeError, InvalidInvitationCodeError,
RepeatPasswordNotMatchError, RepeatPasswordNotMatchError,
) )
@ -21,6 +22,7 @@ from libs.helper import TimestampField, timezone
from libs.login import login_required from libs.login import login_required
from models import AccountIntegrate, InvitationCode from models import AccountIntegrate, InvitationCode
from services.account_service import AccountService from services.account_service import AccountService
from services.billing_service import BillingService
from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError from services.errors.account import CurrentPasswordIncorrectError as ServiceCurrentPasswordIncorrectError
@ -242,6 +244,54 @@ class AccountIntegrateApi(Resource):
return {"data": integrate_data} return {"data": integrate_data}
class AccountDeleteVerifyApi(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
account = current_user
token, code = AccountService.generate_account_deletion_verification_code(account)
AccountService.send_account_deletion_verification_email(account, code)
return {"result": "success", "data": token}
class AccountDeleteApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
account = current_user
parser = reqparse.RequestParser()
parser.add_argument("token", type=str, required=True, location="json")
parser.add_argument("code", type=str, required=True, location="json")
args = parser.parse_args()
if not AccountService.verify_account_deletion_code(args["token"], args["code"]):
raise InvalidAccountDeletionCodeError()
AccountService.delete_account(account)
return {"result": "success"}
class AccountDeleteUpdateFeedbackApi(Resource):
@setup_required
def post(self):
account = current_user
parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json")
parser.add_argument("feedback", type=str, required=True, location="json")
args = parser.parse_args()
BillingService.update_account_deletion_feedback(args["email"], args["feedback"])
return {"result": "success"}
# Register API resources # Register API resources
api.add_resource(AccountInitApi, "/account/init") api.add_resource(AccountInitApi, "/account/init")
api.add_resource(AccountProfileApi, "/account/profile") api.add_resource(AccountProfileApi, "/account/profile")
@ -252,5 +302,8 @@ api.add_resource(AccountInterfaceThemeApi, "/account/interface-theme")
api.add_resource(AccountTimezoneApi, "/account/timezone") api.add_resource(AccountTimezoneApi, "/account/timezone")
api.add_resource(AccountPasswordApi, "/account/password") api.add_resource(AccountPasswordApi, "/account/password")
api.add_resource(AccountIntegrateApi, "/account/integrates") api.add_resource(AccountIntegrateApi, "/account/integrates")
api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify")
api.add_resource(AccountDeleteApi, "/account/delete")
api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback")
# api.add_resource(AccountEmailApi, '/account/email') # api.add_resource(AccountEmailApi, '/account/email')
# api.add_resource(AccountEmailVerifyApi, '/account/email-verify') # api.add_resource(AccountEmailVerifyApi, '/account/email-verify')

View File

@ -35,3 +35,9 @@ class AccountNotInitializedError(BaseHTTPException):
error_code = "account_not_initialized" error_code = "account_not_initialized"
description = "The account has not been initialized yet. Please proceed with the initialization process first." description = "The account has not been initialized yet. Please proceed with the initialization process first."
code = 400 code = 400
class InvalidAccountDeletionCodeError(BaseHTTPException):
error_code = "invalid_account_deletion_code"
description = "Invalid account deletion code."
code = 400

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.console import api from controllers.console import api
@ -37,7 +37,7 @@ class LoadBalancingCredentialsValidateApi(Resource):
model_load_balancing_service = ModelLoadBalancingService() model_load_balancing_service = ModelLoadBalancingService()
result = True result = True
error = None error = ""
try: try:
model_load_balancing_service.validate_load_balancing_credentials( model_load_balancing_service.validate_load_balancing_credentials(
@ -86,7 +86,7 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
model_load_balancing_service = ModelLoadBalancingService() model_load_balancing_service = ModelLoadBalancingService()
result = True result = True
error = None error = ""
try: try:
model_load_balancing_service.validate_load_balancing_credentials( model_load_balancing_service.validate_load_balancing_credentials(

View File

@ -1,7 +1,7 @@
from urllib import parse from urllib import parse
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, abort, marshal_with, reqparse from flask_restful import Resource, abort, marshal_with, reqparse # type: ignore
import services import services
from configs import dify_config from configs import dify_config
@ -89,19 +89,19 @@ class MemberCancelInviteApi(Resource):
@account_initialization_required @account_initialization_required
def delete(self, member_id): def delete(self, member_id):
member = db.session.query(Account).filter(Account.id == str(member_id)).first() member = db.session.query(Account).filter(Account.id == str(member_id)).first()
if not member: if member is None:
abort(404) abort(404)
else:
try: try:
TenantService.remove_member_from_tenant(current_user.current_tenant, member, current_user) TenantService.remove_member_from_tenant(current_user.current_tenant, member, current_user)
except services.errors.account.CannotOperateSelfError as e: except services.errors.account.CannotOperateSelfError as e:
return {"code": "cannot-operate-self", "message": str(e)}, 400 return {"code": "cannot-operate-self", "message": str(e)}, 400
except services.errors.account.NoPermissionError as e: except services.errors.account.NoPermissionError as e:
return {"code": "forbidden", "message": str(e)}, 403 return {"code": "forbidden", "message": str(e)}, 403
except services.errors.account.MemberNotInTenantError as e: except services.errors.account.MemberNotInTenantError as e:
return {"code": "member-not-found", "message": str(e)}, 404 return {"code": "member-not-found", "message": str(e)}, 404
except Exception as e: except Exception as e:
raise ValueError(str(e)) raise ValueError(str(e))
return {"result": "success"}, 204 return {"result": "success"}, 204
@ -126,6 +126,7 @@ class MemberUpdateRoleApi(Resource):
abort(404) abort(404)
try: try:
assert member is not None, "Member not found"
TenantService.update_member_role(current_user.current_tenant, member, new_role, current_user) TenantService.update_member_role(current_user.current_tenant, member, new_role, current_user)
except Exception as e: except Exception as e:
raise ValueError(str(e)) raise ValueError(str(e))

View File

@ -1,8 +1,8 @@
import io import io
from flask import send_file from flask import send_file
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.console import api from controllers.console import api
@ -66,7 +66,7 @@ class ModelProviderValidateApi(Resource):
model_provider_service = ModelProviderService() model_provider_service = ModelProviderService()
result = True result = True
error = None error = ""
try: try:
model_provider_service.provider_credentials_validate( model_provider_service.provider_credentials_validate(
@ -132,7 +132,8 @@ class ModelProviderIconApi(Resource):
icon_type=icon_type, icon_type=icon_type,
lang=lang, lang=lang,
) )
if icon is None:
raise ValueError(f"icon not found for provider {provider}, icon_type {icon_type}, lang {lang}")
return send_file(io.BytesIO(icon), mimetype=mimetype) return send_file(io.BytesIO(icon), mimetype=mimetype)

View File

@ -1,7 +1,7 @@
import logging import logging
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from controllers.console import api from controllers.console import api
@ -308,7 +308,7 @@ class ModelProviderModelValidateApi(Resource):
model_provider_service = ModelProviderService() model_provider_service = ModelProviderService()
result = True result = True
error = None error = ""
try: try:
model_provider_service.model_credentials_validate( model_provider_service.model_credentials_validate(

View File

@ -1,14 +1,16 @@
import io import io
from flask import send_file from flask import send_file
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
from configs import dify_config from configs import dify_config
from controllers.console import api from controllers.console import api
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from extensions.ext_database import db
from libs.helper import alphanumeric, uuid_value from libs.helper import alphanumeric, uuid_value
from libs.login import login_required from libs.login import login_required
from services.tools.api_tools_manage_service import ApiToolManageService from services.tools.api_tools_manage_service import ApiToolManageService
@ -91,12 +93,16 @@ class ToolBuiltinProviderUpdateApi(Resource):
args = parser.parse_args() args = parser.parse_args()
return BuiltinToolManageService.update_builtin_tool_provider( with Session(db.engine) as session:
user_id, result = BuiltinToolManageService.update_builtin_tool_provider(
tenant_id, session=session,
provider, user_id=user_id,
args["credentials"], tenant_id=tenant_id,
) provider_name=provider,
credentials=args["credentials"],
)
session.commit()
return result
class ToolBuiltinProviderGetCredentialsApi(Resource): class ToolBuiltinProviderGetCredentialsApi(Resource):
@ -104,13 +110,11 @@ class ToolBuiltinProviderGetCredentialsApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
def get(self, provider): def get(self, provider):
user_id = current_user.id
tenant_id = current_user.current_tenant_id tenant_id = current_user.current_tenant_id
return BuiltinToolManageService.get_builtin_tool_provider_credentials( return BuiltinToolManageService.get_builtin_tool_provider_credentials(
user_id, tenant_id=tenant_id,
tenant_id, provider_name=provider,
provider,
) )

View File

@ -1,8 +1,8 @@
import logging import logging
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user # type: ignore
from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse # type: ignore
from werkzeug.exceptions import Unauthorized from werkzeug.exceptions import Unauthorized
import services import services
@ -82,11 +82,7 @@ class WorkspaceListApi(Resource):
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args") parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
args = parser.parse_args() args = parser.parse_args()
tenants = ( tenants = Tenant.query.order_by(Tenant.created_at.desc()).paginate(page=args["page"], per_page=args["limit"])
db.session.query(Tenant)
.order_by(Tenant.created_at.desc())
.paginate(page=args["page"], per_page=args["limit"])
)
has_more = False has_more = False
if len(tenants.items) == args["limit"]: if len(tenants.items) == args["limit"]:
@ -151,6 +147,8 @@ class SwitchWorkspaceApi(Resource):
raise AccountNotLinkTenantError("Account not link tenant") raise AccountNotLinkTenantError("Account not link tenant")
new_tenant = db.session.query(Tenant).get(args["tenant_id"]) # Get new tenant new_tenant = db.session.query(Tenant).get(args["tenant_id"]) # Get new tenant
if new_tenant is None:
raise ValueError("Tenant not found")
return {"result": "success", "new_tenant": marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)} return {"result": "success", "new_tenant": marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)}
@ -166,7 +164,7 @@ class CustomConfigWorkspaceApi(Resource):
parser.add_argument("replace_webapp_logo", type=str, location="json") parser.add_argument("replace_webapp_logo", type=str, location="json")
args = parser.parse_args() args = parser.parse_args()
tenant = db.session.query(Tenant).filter(Tenant.id == current_user.current_tenant_id).one_or_404() tenant = Tenant.query.filter(Tenant.id == current_user.current_tenant_id).one_or_404()
custom_config_dict = { custom_config_dict = {
"remove_webapp_brand": args["remove_webapp_brand"], "remove_webapp_brand": args["remove_webapp_brand"],

View File

@ -3,7 +3,7 @@ import os
from functools import wraps from functools import wraps
from flask import abort, request from flask import abort, request
from flask_login import current_user from flask_login import current_user # type: ignore
from configs import dify_config from configs import dify_config
from controllers.console.workspace.error import AccountNotInitializedError from controllers.console.workspace.error import AccountNotInitializedError
@ -121,8 +121,8 @@ def cloud_utm_record(view):
utm_info = request.cookies.get("utm_info") utm_info = request.cookies.get("utm_info")
if utm_info: if utm_info:
utm_info = json.loads(utm_info) utm_info_dict: dict = json.loads(utm_info)
OperationService.record_utm(current_user.current_tenant_id, utm_info) OperationService.record_utm(current_user.current_tenant_id, utm_info_dict)
except Exception as e: except Exception as e:
pass pass
return view(*args, **kwargs) return view(*args, **kwargs)

View File

@ -1,5 +1,5 @@
from flask import Response, request from flask import Response, request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
import services import services

View File

@ -1,5 +1,5 @@
from flask import Response from flask import Response
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
from controllers.files import api from controllers.files import api

View File

@ -1,4 +1,6 @@
from flask_restful import Resource, reqparse import json
from flask_restful import Resource, reqparse # type: ignore
from controllers.console.wraps import setup_required from controllers.console.wraps import setup_required
from controllers.inner_api import api from controllers.inner_api import api
@ -29,4 +31,34 @@ class EnterpriseWorkspace(Resource):
return {"message": "enterprise workspace created."} return {"message": "enterprise workspace created."}
class EnterpriseWorkspaceNoOwnerEmail(Resource):
@setup_required
@inner_api_only
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("name", type=str, required=True, location="json")
args = parser.parse_args()
tenant = TenantService.create_tenant(args["name"], is_from_dashboard=True)
tenant_was_created.send(tenant)
resp = {
"id": tenant.id,
"name": tenant.name,
"encrypt_public_key": tenant.encrypt_public_key,
"plan": tenant.plan,
"status": tenant.status,
"custom_config": json.loads(tenant.custom_config) if tenant.custom_config else {},
"created_at": tenant.created_at.isoformat() if tenant.created_at else None,
"updated_at": tenant.updated_at.isoformat() if tenant.updated_at else None,
}
return {
"message": "enterprise workspace created.",
"tenant": resp,
}
api.add_resource(EnterpriseWorkspace, "/enterprise/workspace") api.add_resource(EnterpriseWorkspace, "/enterprise/workspace")
api.add_resource(EnterpriseWorkspaceNoOwnerEmail, "/enterprise/workspace/ownerless")

View File

@ -45,14 +45,14 @@ def inner_api_user_auth(view):
if " " in user_id: if " " in user_id:
user_id = user_id.split(" ")[1] user_id = user_id.split(" ")[1]
inner_api_key = request.headers.get("X-Inner-Api-Key") inner_api_key = request.headers.get("X-Inner-Api-Key", "")
data_to_sign = f"DIFY {user_id}" data_to_sign = f"DIFY {user_id}"
signature = hmac_new(inner_api_key.encode("utf-8"), data_to_sign.encode("utf-8"), sha1) signature = hmac_new(inner_api_key.encode("utf-8"), data_to_sign.encode("utf-8"), sha1)
signature = b64encode(signature.digest()).decode("utf-8") signature_base64 = b64encode(signature.digest()).decode("utf-8")
if signature != token: if signature_base64 != token:
return view(*args, **kwargs) return view(*args, **kwargs)
kwargs["user"] = db.session.query(EndUser).filter(EndUser.id == user_id).first() kwargs["user"] = db.session.query(EndUser).filter(EndUser.id == user_id).first()

View File

@ -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 from .dataset import dataset, document, hit_testing, segment, upload_file

View File

@ -1,4 +1,4 @@
from flask_restful import Resource, marshal_with from flask_restful import Resource, marshal_with # type: ignore
from controllers.common import fields from controllers.common import fields
from controllers.common import helpers as controller_helpers from controllers.common import helpers as controller_helpers

View File

@ -1,7 +1,7 @@
import logging import logging
from flask import request from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
import services import services
@ -83,7 +83,7 @@ class TextApi(Resource):
and app_model.workflow and app_model.workflow
and app_model.workflow.features_dict and app_model.workflow.features_dict
): ):
text_to_speech = app_model.workflow.features_dict.get("text_to_speech") text_to_speech = app_model.workflow.features_dict.get("text_to_speech", {})
voice = args.get("voice") or text_to_speech.get("voice") voice = args.get("voice") or text_to_speech.get("voice")
else: else:
try: try:

View File

@ -1,6 +1,6 @@
import logging import logging
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse # type: ignore
from werkzeug.exceptions import InternalServerError, NotFound from werkzeug.exceptions import InternalServerError, NotFound
import services import services
@ -18,7 +18,6 @@ from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate
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 (
AppInvokeQuotaExceededError,
ModelCurrentlyNotSupportError, ModelCurrentlyNotSupportError,
ProviderTokenNotInitError, ProviderTokenNotInitError,
QuotaExceededError, QuotaExceededError,
@ -74,7 +73,7 @@ class CompletionApi(Resource):
raise ProviderModelCurrentlyNotSupportError() raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e: except InvokeError as e:
raise CompletionRequestError(e.description) raise CompletionRequestError(e.description)
except (ValueError, AppInvokeQuotaExceededError) as e: except ValueError as e:
raise e raise e
except Exception as e: except Exception as e:
logging.exception("internal server error.") logging.exception("internal server error.")
@ -133,7 +132,7 @@ class ChatApi(Resource):
raise ProviderModelCurrentlyNotSupportError() raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e: except InvokeError as e:
raise CompletionRequestError(e.description) raise CompletionRequestError(e.description)
except (ValueError, AppInvokeQuotaExceededError) as e: except ValueError as e:
raise e raise e
except Exception as e: except Exception as e:
logging.exception("internal server error.") logging.exception("internal server error.")

View File

@ -1,5 +1,6 @@
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
import services import services
@ -7,6 +8,7 @@ from controllers.service_api import api
from controllers.service_api.app.error import NotChatAppError from controllers.service_api.app.error import NotChatAppError
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
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 fields.conversation_fields import ( from fields.conversation_fields import (
conversation_delete_fields, conversation_delete_fields,
conversation_infinite_scroll_pagination_fields, conversation_infinite_scroll_pagination_fields,
@ -39,14 +41,16 @@ class ConversationApi(Resource):
args = parser.parse_args() args = parser.parse_args()
try: try:
return ConversationService.pagination_by_last_id( with Session(db.engine) as session:
app_model=app_model, return ConversationService.pagination_by_last_id(
user=end_user, session=session,
last_id=args["last_id"], app_model=app_model,
limit=args["limit"], user=end_user,
invoke_from=InvokeFrom.SERVICE_API, last_id=args["last_id"],
sort_by=args["sort_by"], limit=args["limit"],
) invoke_from=InvokeFrom.SERVICE_API,
sort_by=args["sort_by"],
)
except services.errors.conversation.LastConversationNotExistsError: except services.errors.conversation.LastConversationNotExistsError:
raise NotFound("Last Conversation Not Exists.") raise NotFound("Last Conversation Not Exists.")

View File

@ -1,5 +1,5 @@
from flask import request from flask import request
from flask_restful import Resource, marshal_with from flask_restful import Resource, marshal_with # type: ignore
import services import services
from controllers.common.errors import FilenameNotExistsError from controllers.common.errors import FilenameNotExistsError

View File

@ -1,7 +1,7 @@
import logging import logging
from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
from flask_restful.inputs import int_range from flask_restful.inputs import int_range # type: ignore
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services import services
@ -104,10 +104,17 @@ class MessageFeedbackApi(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json") parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
parser.add_argument("content", type=str, location="json")
args = parser.parse_args() args = parser.parse_args()
try: try:
MessageService.create_feedback(app_model, message_id, end_user, args["rating"]) MessageService.create_feedback(
app_model=app_model,
message_id=message_id,
user=end_user,
rating=args.get("rating"),
content=args.get("content"),
)
except services.errors.message.MessageNotExistsError: except services.errors.message.MessageNotExistsError:
raise NotFound("Message Not Exists.") raise NotFound("Message Not Exists.")

Some files were not shown because too many files have changed in this diff Show More