Compare commits

..
317 Commits
Author SHA1 Message Date
-LAN- 24d06eb741 Merge branch 'feat/enhance-multi-modal-support' into release/0.10.0-beta 2024-10-15 15:34:42 +08:00
-LAN- a36ef8430e refactor(core): improve type annotations and file handling consistency
- Use more precise type annotations with Sequence and Mapping for task entities.
- Ensure raw_prompt is assigned properly after replacement in advanced prompt transform.
- Remove unused generator return type from _fetch_context method.
- Refactor tool node file handling to retrieve more comprehensive file attributes, ensuring file existence validation in the database.
2024-10-15 15:34:14 +08:00
-LAN- 6e9129a333 Update build-push.yml to include release/0.10.0-beta branch in the workflow trigger 2024-10-15 14:24:27 +08:00
-LAN- 86427468fd Update version to 0.10.0-beta3 in packaging, docker, and web 2024-10-15 14:23:02 +08:00
-LAN- 638d9250e4 Merge branch 'feat/enhance-multi-modal-support' into release/0.10.0-beta 2024-10-15 12:01:16 +08:00
-LAN- dba67cd87a fix(memory): filter non-image file types in prompt message content
- Skip non-image files when converting file objects to prompt message content.
- Ensures only image files are processed, improving the accuracy and relevance of prompt messages.
2024-10-15 12:01:00 +08:00
-LAN- c3811c4c23 Merge branch 'feat/attachments' into release/0.10.0-beta 2024-10-15 11:29:54 +08:00
Joel ff7a467a1a fix: iteration not support file item 2024-10-15 11:20:52 +08:00
-LAN- c73b5e2410 Merge branch 'feat/attachments' into release/0.10.0-beta 2024-10-15 11:05:45 +08:00
-LAN- 421fde0e85 Merge branch 'feat/enhance-multi-modal-support' into release/0.10.0-beta 2024-10-15 11:05:41 +08:00
-LAN- b3fdd618a1 refactor(core): simplify role handling and improve usability
- Replaced explicit string usage with `CreatedByRole` enum for better maintainability.
- Removed duplicate `CreatedByRole` class definition, improving codebase consistency.
- Increased file number limits from 6 to 10 to allow more file uploads.
- Transitioned `AppMode` to a string enum for consistent type usage.
- Refactored `extract_thread_messages` function argument for flexibility.
- Removed file extension limitation in file service to support custom extensions.
- Improved enum import statements across multiple modules for clarity and consistency.
2024-10-15 11:04:28 +08:00
StyleZhang 81e57bcc73 fix: remove image item 2024-10-15 10:53:27 +08:00
-LAN- 7156753a99 Merge branch 'feat/attachments' into release/0.10.0-beta 2024-10-14 23:57:44 +08:00
-LAN- 0d310b503b refactor(prompt): improve handling of variable templates in advanced prompt transform 2024-10-14 16:47:03 +08:00
-LAN- 03018823d8 fix(workflow): handle special values for process data consistently
- Apply `handle_special_values` to `process_data` in workflow cycle management.
- Improve template processing in `AdvancedPromptTransform` with `VariablePool`.
- Make `system_variables` and `user_inputs` optional in `VariablePool` initialization.
2024-10-14 16:47:03 +08:00
-LAN- 885192db38 feat(podcast_generator): add new podcast generation tools
- Introduced podcast generator with text-to-speech functionality using OpenAI's API.
- Implemented credential validation for TTS services and API keys.
- Added support for generating podcast audio with alternating host voices.
- Included user-friendly setup with internationalized YAML configuration.
- Added SVG icon to enhance visual identification.
2024-10-14 16:47:03 +08:00
-LAN- ea18dd1571 feat(api): Enhance multi modal support. 2024-10-14 16:47:03 +08:00
StyleZhang 8fe5028f74 Merge branch 'main' into feat/attachments 2024-10-14 16:09:05 +08:00
StyleZhang 9f492d527a fix: chat input file list remove button style 2024-10-11 10:33:19 +08:00
StyleZhang faec1c50f9 Merge branch 'main' into feat/attachments 2024-10-10 16:36:25 +08:00
StyleZhang 2888c068c1 fix: chat log 2024-10-10 16:07:35 +08:00
StyleZhang d77521e65f Merge branch 'main' into feat/attachments 2024-10-10 10:28:56 +08:00
StyleZhang b463468a14 Merge branch 'main' into feat/attachments 2024-10-10 10:02:35 +08:00
StyleZhang 820076beec Merge branch 'main' into feat/attachments 2024-10-09 17:48:05 +08:00
StyleZhang fd6476b941 fix: start chat check & chat send button theme 2024-10-09 17:45:38 +08:00
StyleZhang 7abe76f07e fix: basic chat 2024-10-09 17:24:43 +08:00
StyleZhang dba24766e8 fix: file download 2024-10-09 14:22:46 +08:00
Joel 12492c0d5d fix: answer node support choose file 2024-10-09 10:31:59 +08:00
Joel 2740d68cd1 chore: prompt not support file var 2024-10-08 18:32:30 +08:00
Joel 7218f718ad fix: value check 2024-10-08 18:05:55 +08:00
Joel 2ae0aceef8 chore: fix doc options 2024-10-08 17:23:57 +08:00
Joel a5c1132087 chore: prompt support array string number 2024-10-08 17:13:51 +08:00
Joel 691a7f727a fix: iteration output var type 2024-10-08 14:54:21 +08:00
Joel 7e2984b6e2 fix: array operator render 2024-10-08 14:45:28 +08:00
JzoNg 44e81dbbc8 Merge branch 'main' into jzh 2024-09-30 17:19:35 +08:00
Joel 944cfd2b68 chore: merge main 2024-09-30 16:43:06 +08:00
Joel 6d2682c751 fix: help link url 2024-09-30 16:36:16 +08:00
StyleZhang d2971e84bb fix: node handle tooltip 2024-09-30 15:23:04 +08:00
StyleZhang b67b81bf8f fix: file upload tip 2024-09-30 14:52:16 +08:00
Joel c05902404d chore: add help link 2024-09-30 14:00:51 +08:00
Joel 1e6d5f2c48 feat: add support doc extract file types 2024-09-30 13:56:24 +08:00
JzoNg e2b1464db2 fix features update by DSL import 2024-09-29 17:15:39 +08:00
JzoNg f0285a53d2 fix style of textarea in retrivel test 2024-09-29 15:53:10 +08:00
Joel 00f91b5dc4 chore: list fiter i18n 2024-09-29 14:57:38 +08:00
StyleZhang dc3e86b82a fix: embedded chat file form 2024-09-29 14:56:13 +08:00
Joel d239c5b54d fix: list filter first name 2024-09-29 14:47:02 +08:00
StyleZhang 23abccd3a6 fix: lint 2024-09-29 14:43:17 +08:00
JzoNg 2520e40059 fix: feature panel content height in safari 2024-09-29 14:17:27 +08:00
JzoNg 1d4ed3d9e7 fix: feature panel content height in safari 2024-09-29 14:06:07 +08:00
JzoNg eed8ab9348 fix style of conversation variable 2024-09-29 11:58:23 +08:00
JzoNg 112aaf6e1b fix completion 2024-09-29 11:26:35 +08:00
StyleZhang 094a1a1458 remove chat thumb 2024-09-29 10:49:00 +08:00
JzoNg 955fa4345a fix output of run 2024-09-27 17:37:26 +08:00
Joel ac5e381a1a chore: change list filter writing 2024-09-27 17:09:12 +08:00
Joel ae9b9f867a chore: add new node description 2024-09-27 16:37:14 +08:00
JzoNg 8fd04e5313 merge main 2024-09-27 16:27:00 +08:00
Joel 3904782647 fix: add upload i18n 2024-09-27 11:16:02 +08:00
StyleZhang 288be3fbd8 fix: chat message end 2024-09-27 10:30:36 +08:00
JzoNg f7f836d6f1 fix workflow output 2024-09-26 17:16:08 +08:00
JzoNg 5dedcb74a5 fix form of chat in webapp 2024-09-26 17:01:17 +08:00
StyleZhang b95d0fa9a9 fix: file upload limits in web app 2024-09-26 16:41:44 +08:00
StyleZhang 543503c398 fix: file progress 2024-09-26 16:33:37 +08:00
JzoNg 3f16caf244 show file list in generation result 2024-09-26 16:10:28 +08:00
JzoNg 54133dfbde files in log 2024-09-26 15:50:19 +08:00
StyleZhang b491c93b1c image download 2024-09-26 15:49:53 +08:00
StyleZhang 2a6d9c3211 fix: chat send 2024-09-26 15:33:01 +08:00
StyleZhang c6691bd297 fix: webapp chat embedded chat 2024-09-26 15:26:53 +08:00
StyleZhang 2a0b30de5c fix: image.enable 2024-09-26 14:39:56 +08:00
StyleZhang a7d53abba9 webapp chat embedded chat 2024-09-26 13:47:36 +08:00
StyleZhang 296253a365 debug chat 2024-09-26 11:54:42 +08:00
Joel c89cefe526 chore: remove log 2024-09-26 11:50:52 +08:00
StyleZhang 1d027fa065 fix: chat check inputs form 2024-09-26 11:07:08 +08:00
Joel 9ce9a52a86 fix: text not string 2024-09-26 10:15:20 +08:00
JzoNg c74424ed85 fix text-generation files 2024-09-26 08:45:50 +08:00
JzoNg 719ef9cef9 text generation run 2024-09-25 20:34:48 +08:00
StyleZhang 0ab525a691 fix: file size 2024-09-25 16:38:20 +08:00
StyleZhang 6fdcf6ee21 file-uploader 2024-09-25 16:24:02 +08:00
Joel d01e97c1fc fix: tiny select ui 2024-09-25 15:44:02 +08:00
Joel 87e560de8a chore: type value change to array 2024-09-25 15:19:34 +08:00
JzoNg f8d26e46ac text-generation support file type 2024-09-25 15:01:56 +08:00
Joel 195ac19774 chore: list filter transfer value to array 2024-09-25 13:41:56 +08:00
Joel 0281eb796d chore: transfer value to string array 2024-09-25 12:59:19 +08:00
StyleZhang 9fe2f321ae fix: chatflow check start node form 2024-09-25 12:21:53 +08:00
StyleZhang 5f76e665a1 fix: file extension 2024-09-25 11:24:14 +08:00
StyleZhang 81568752c0 fix: file from link 2024-09-24 17:47:01 +08:00
JzoNg ceb1dde714 Merge branch 'main' into jzh 2024-09-24 17:30:56 +08:00
JzoNg 3209fdca53 legacy for sys.files 2024-09-24 17:08:23 +08:00
JzoNg dc5010d833 fix step run of file type 2024-09-24 16:24:46 +08:00
Joel 8b26ae6532 fix: http file node not added 2024-09-24 15:18:39 +08:00
Joel 66953d57a2 chore: use new add sub variables 2024-09-24 13:56:06 +08:00
StyleZhang afc9630cd0 merge main 2024-09-24 11:44:38 +08:00
Joel 7e8bafe186 feat: support file size unit 2024-09-24 11:12:41 +08:00
Joel 6c5fcd1ffc fix: all of not show the right place 2024-09-24 10:55:37 +08:00
Joel 7602d22133 chore: files name 2024-09-23 18:33:14 +08:00
Joel 5ec91e8507 feat: if node render files sub vars 2024-09-23 18:25:11 +08:00
StyleZhang 466966f027 file input error tip 2024-09-23 17:27:06 +08:00
JzoNg 212d04ea27 form inputs hide handle 2024-09-23 10:25:35 +08:00
StyleZhang 0cb50dd4a5 fix: workflow inputs panel 2024-09-20 18:00:35 +08:00
StyleZhang ab19fccf3d single file 2024-09-20 17:20:28 +08:00
StyleZhang 4ed46e3fed fix: uploader 2024-09-20 15:44:49 +08:00
Joel 9fd2f798ff feat: add type palcehloder in picker 2024-09-20 15:19:38 +08:00
Joel 146be41b1d fix: if show var types 2024-09-20 14:59:46 +08:00
Joel ce6ae5732a fix: not show the right output type 2024-09-20 14:42:53 +08:00
StyleZhang edf462c640 fix: uploader 2024-09-20 12:08:03 +08:00
JzoNg d580fc1e9d fix features initilization 2024-09-20 11:03:45 +08:00
Joel 5544791031 feat: doc extract support both file and file array 2024-09-19 17:55:58 +08:00
JzoNg 099746dd59 fix style of tracing 2024-09-19 17:40:27 +08:00
StyleZhang c6f53c9030 Merge branch 'main' into feat/attachments 2024-09-19 17:11:28 +08:00
StyleZhang 8236f8fed8 fix: file uploader 2024-09-19 17:08:02 +08:00
StyleZhang 2b0c39ed3f file in chat question 2024-09-19 16:55:14 +08:00
JzoNg 396a240e68 test run 2024-09-19 16:49:30 +08:00
JzoNg 8bd9d8f6ba remove chat input 2024-09-19 15:45:30 +08:00
JzoNg aa7ae4c5f1 chore: remove console 2024-09-19 15:45:30 +08:00
StyleZhang 49b7acf52e fix: file uploader 2024-09-19 14:52:22 +08:00
StyleZhang 466ac987f5 fix: file type icon 2024-09-19 11:12:18 +08:00
StyleZhang 49972939a9 file icon 2024-09-19 11:06:38 +08:00
StyleZhang 80f167ca02 file upload limit 2024-09-18 18:12:03 +08:00
JzoNg f652ae0d98 step run 2024-09-18 18:07:03 +08:00
Joel 4dbf56675a fix: change to backend doc extractor 2024-09-18 17:35:14 +08:00
StyleZhang f5d1f5a20a file uploader 2024-09-18 16:50:53 +08:00
StyleZhang fd9b71c4d7 file uploader 2024-09-18 16:36:55 +08:00
Joel 1df41cef4c fix: doc extract not export text 2024-09-18 15:23:53 +08:00
Joel 602d2486bd fix: iteration file arry type 2024-09-18 15:03:02 +08:00
JzoNg 403fede432 fix basic app publish 2024-09-18 14:01:20 +08:00
JzoNg 9f66e6e357 fix feature bar in basic chabot 2024-09-18 13:36:11 +08:00
JzoNg affb2e38a1 fix typo of icon path 2024-09-18 13:14:18 +08:00
Joel 31d87f85b8 merge 2024-09-18 11:57:48 +08:00
JzoNg 54105e85ff fix icon 2024-09-18 11:21:38 +08:00
Joel 5ec604500c chore: change field to backend 2024-09-18 11:15:36 +08:00
JzoNg 96d2582d89 file var in form 2024-09-18 10:44:59 +08:00
JzoNg a10b0db102 vision setting 2024-09-18 10:44:59 +08:00
StyleZhang 5dd556b4c8 file uploader 2024-09-13 17:31:10 +08:00
StyleZhang a4c6d0b94b file uploader 2024-09-13 16:46:16 +08:00
StyleZhang 323a835de9 Merge branch 'main' into feat/attachments 2024-09-12 13:53:41 +08:00
StyleZhang 0076577764 file uploader 2024-09-11 18:25:49 +08:00
Joel 9a3b7345c4 fix: split line too long 2024-09-11 14:40:38 +08:00
StyleZhang 2ebf5f5ffa merge main 2024-09-11 13:40:36 +08:00
StyleZhang 02f494c0de merge main 2024-09-10 16:38:32 +08:00
Joel f0e81e3918 fix: doc extract and node isconversation item 2024-09-10 15:22:44 +08:00
Joel aa8499efac fix: doc extract var type 2024-09-10 15:16:27 +08:00
Joel ea40b1dcb2 fix: two scrollbar 2024-09-10 15:05:54 +08:00
Joel a689cd6fd4 chore: var refernece support theme 2024-09-10 14:49:27 +08:00
StyleZhang 32b6c7063a file uploader 2024-09-10 14:17:56 +08:00
Joel 97056dad30 fix: file type var match page crash 2024-09-10 11:54:34 +08:00
Joel 264f7c2139 fix: file show type error 2024-09-10 10:59:13 +08:00
Joel 007a6fd14a chore: other file types placeholder add + 2024-09-09 15:24:50 +08:00
Joel c159b7a781 chore: transform field type css to tailwind and multi theme 2024-09-09 15:15:08 +08:00
Joel 6c9c3faf78 fix: allow file extensions remove . 2024-09-09 14:36:56 +08:00
StyleZhang d933ebb845 file input 2024-09-09 11:27:35 +08:00
JzoNg b60c7a5826 vision config 2024-09-04 15:45:33 +08:00
JzoNg 0b94218378 remove unused components 2024-09-04 11:47:29 +08:00
Joel 97cc9a5615 feat: check file item key not set 2024-09-04 11:28:36 +08:00
Joel f6d0fd9848 feat: add check list filter value 2024-09-04 11:19:09 +08:00
Joel b863dd7de2 fix: list filter init value 2024-09-04 10:47:26 +08:00
JzoNg b0e7a22a27 annotation reply 2024-09-03 17:19:11 +08:00
JzoNg 565a835947 conversation opener 2024-09-01 15:00:23 +08:00
JzoNg fe94c876fb multiple model message sending 2024-09-01 13:37:21 +08:00
JzoNg 67a34bdd7a app publish with new features 2024-09-01 13:10:40 +08:00
JzoNg 8c785e268b completion debug & preview 2024-09-01 11:24:54 +08:00
JzoNg 65a6265ff6 new features in chat app configuration 2024-08-30 18:58:08 +08:00
Joel 08d3cb1912 fix: filter file and file sub variable 2024-08-30 17:25:32 +08:00
Joel 48d8b01d81 fix: http node value var rename 2024-08-30 15:30:12 +08:00
Joel 38edb06897 feat: list filter output 2024-08-30 15:21:37 +08:00
Joel dc919c2a6c feat: output item var type and filter condition triger 2024-08-30 15:06:57 +08:00
Joel e7a6a0ab01 chore: list filter operate ui 2024-08-30 14:44:16 +08:00
Joel 61d989f413 feat: support order change 2024-08-30 14:34:46 +08:00
Joel 976efd93a1 feat: support filter variable var data sync 2024-08-30 14:14:42 +08:00
JzoNg 0e2f78b3a6 features in workflow 2024-08-29 22:54:36 +08:00
JzoNg b3529d3ccc file upload 2024-08-29 20:20:28 +08:00
JzoNg d69b453729 conversation opener 2024-08-29 20:20:28 +08:00
JzoNg 2f658de155 moderation 2024-08-29 20:20:28 +08:00
JzoNg a691700b48 text2speech 2024-08-29 20:20:28 +08:00
JzoNg c5317d8f58 feature card 2024-08-29 20:20:28 +08:00
JzoNg 822f03f3cd text to speech card 2024-08-29 20:20:28 +08:00
JzoNg 101e56baaa follow up & citations & speech-to-text 2024-08-29 20:20:28 +08:00
JzoNg 3a8f516dfc more like this 2024-08-29 20:20:28 +08:00
JzoNg 912030c9a1 update style 2024-08-29 20:20:28 +08:00
JzoNg 687661eef7 new style of feature panel 2024-08-29 20:20:28 +08:00
Joel 8efc63a705 feat: handle value picker in body file selector 2024-08-29 16:36:18 +08:00
Joel dca4f9fe9c feat: support file values in body 2024-08-29 16:18:16 +08:00
Joel 51597629b1 fix: http binary node not valid 2024-08-29 14:32:36 +08:00
Joel 76a07513ba fix: prompt editor not update data 2024-08-29 14:25:04 +08:00
Joel dae62bef78 fix: change to key value type not show init key values 2024-08-29 11:55:18 +08:00
Joel 2a6629d435 feat: binary files 2024-08-29 11:50:42 +08:00
Joel 41f0ce1012 feat: support http body to new data struct 2024-08-28 16:56:31 +08:00
Joel e90b055c47 fix: ts problems 2024-08-28 14:40:13 +08:00
Joel 94e40d4ed9 feat: default set vision var value 2024-08-28 14:35:49 +08:00
Joel c34fc071e0 feat: vison file to api define 2024-08-28 10:57:26 +08:00
Joel c014ae43e1 feat: if support file exist 2024-08-27 17:37:39 +08:00
Joel 9851153d38 chore: file type checkbox 2024-08-27 17:15:02 +08:00
Joel cfbabb8383 feat: file valid 2024-08-27 17:09:34 +08:00
Joel b78e90679d fix: choose file type problems 2024-08-27 16:55:24 +08:00
Joel ec1bfdc723 feat: change to new start file struct 2024-08-27 16:37:15 +08:00
Joel e20019f6e9 chore: merge main 2024-08-27 14:23:35 +08:00
Joel 2122cfb152 chore: list filter field 2024-08-27 10:43:03 +08:00
Joel c2b8beffac feat: global variables 2024-08-26 17:32:46 +08:00
StyleZhang 985651454a progress circle 2024-08-26 10:30:26 +08:00
Joel f9c1d06e91 chore: tools node 2024-08-23 18:08:54 +08:00
Joel 657f1d2de8 chore: http request 2024-08-23 17:56:34 +08:00
Joel 6e2192c1e0 chore: variable aggregator 2024-08-23 16:40:39 +08:00
Joel e05b20eb91 chore:paramter extrctor ui 2024-08-23 16:03:01 +08:00
Joel 5117e08def chore: question classify 2024-08-22 17:00:20 +08:00
Joel 34691ca6c9 chore: knownledge node ui 2024-08-22 16:51:44 +08:00
Joel aa40047b08 chore: knowledge 2024-08-22 15:10:49 +08:00
StyleZhang eca17767fe chat style 2024-08-22 15:10:09 +08:00
Joel 51cec1b9ba chore: llm upgrade 2024-08-22 14:34:58 +08:00
Joel 651547c3ef fix: number var picker and other tiny css problem 2024-08-22 10:49:56 +08:00
Joel 8fbdaa604c feat: file array variable choose vars 2024-08-21 17:29:03 +08:00
Joel 1bcb30647f chore: select ui 2024-08-21 17:29:03 +08:00
JzoNg bc245a25bf new style of tables 2024-08-21 17:07:14 +08:00
Joel 85b25ebe1b chore: file selct trigger 2024-08-21 16:45:18 +08:00
Joel b50e94d681 feat: file arrary sub variable select 2024-08-21 16:22:58 +08:00
Joel 91c0657cf6 fix: select default trigger problem 2024-08-21 15:35:28 +08:00
StyleZhang 0da06128e3 agent tool in chat 2024-08-21 15:32:13 +08:00
Joel 0c4af3a1d2 feat: support sub variable operate changes with key and value support 2024-08-21 15:27:08 +08:00
Joel 5628b293f8 feat: sub var if condindion postion 2024-08-21 14:54:06 +08:00
JzoNg fff40aae58 Merge branch 'main' into jzh 2024-08-21 13:40:04 +08:00
Joel b3b87b3e4c chore: sub variable trigger 2024-08-21 11:07:13 +08:00
Joel 9a23cd08d8 fix: sub varibale select trigger 2024-08-19 16:49:52 +08:00
JzoNg cf61ca24e3 new style of table 2024-08-19 16:05:37 +08:00
Joel 58a56add9c feat: can support value 2024-08-19 15:15:44 +08:00
JzoNg b362031baf chip 2024-08-19 14:11:25 +08:00
Joel 7ad409b3d9 fix: update operate and value 2024-08-19 13:53:23 +08:00
Joel 876ea90fe9 feat: support update sub variable value 2024-08-19 11:43:52 +08:00
JzoNg 0eb442f954 new style of user inputs 2024-08-16 17:48:31 +08:00
Joel 4554ac3ef8 feat: can add sub variable 2024-08-16 17:12:44 +08:00
Joel eaa7d114dc feat: file array not sub vars 2024-08-16 11:39:23 +08:00
Joel 581228be74 feat: abstract condition logic to components 2024-08-16 10:14:43 +08:00
StyleZhang 02da0219ff workflow debug and preview panel style 2024-08-15 17:30:17 +08:00
JzoNg d0bbe43dab chore: fix type 2024-08-15 16:36:47 +08:00
JzoNg 16acdc9be4 new style of workflow process 2024-08-15 16:33:33 +08:00
Joel a6999b5d02 fix: old not set vision data 2024-08-15 11:37:02 +08:00
StyleZhang 33bfa4758e Merge branch 'main' into feat/attachments 2024-08-15 10:57:59 +08:00
JzoNg db63c2c219 new style of status 2024-08-14 18:40:11 +08:00
JzoNg bea4ec5998 style update of log 2024-08-13 17:11:17 +08:00
JzoNg 74333db4c8 update input in env & conversation var 2024-08-13 16:12:12 +08:00
JzoNg 0019fb9f8b Merge branch 'main' into jzh 2024-08-13 15:59:19 +08:00
JzoNg 47615ac8fb meta data style update 2024-08-13 15:25:02 +08:00
StyleZhang d7c8bced9b file uploader 2024-08-13 15:24:32 +08:00
Joel 57f178902f feat: if node select value 2024-08-13 14:14:38 +08:00
Joel 4586de48d6 feat: default var type 2024-08-13 14:04:43 +08:00
Joel 6549519fa5 feat: files attr select 2024-08-13 13:59:19 +08:00
Joel ae098ad121 feat: condition operation 2024-08-13 11:16:27 +08:00
Joel 20922fde1c feat: detect file type 2024-08-12 18:22:07 +08:00
StyleZhang 079c802b5c file uploader 2024-08-12 16:24:13 +08:00
JzoNg efcd462a69 fix style of switch 2024-08-12 13:15:36 +08:00
Joel 843c8ad306 feat: file obj 2024-08-09 17:46:33 +08:00
StyleZhang 594bf96922 file uploader hooks 2024-08-09 16:48:58 +08:00
JzoNg ade385c9c1 replace input in workflow blocks 2024-08-09 16:19:10 +08:00
JzoNg baed068231 replace form input 2024-08-09 12:35:01 +08:00
Joel 42f5334ae4 feat: iteration file array input 2024-08-09 11:45:14 +08:00
Joel 3c4ab0632d feat: tool support file type 2024-08-09 11:38:40 +08:00
Joel bc5f109308 feat: http support body binary 2024-08-09 10:53:59 +08:00
Joel 97b2a42cc3 feat: form data support file type 2024-08-09 10:29:29 +08:00
JzoNg 939df16655 refactor input and replace search input 2024-08-08 17:39:09 +08:00
JzoNg 9362ae045c fix textarea onchange 2024-08-08 17:39:09 +08:00
Joel 257c515178 fix: old no vision data 2024-08-08 15:47:55 +08:00
Joel 6b7520ccc2 fix: old llm code 2024-08-08 15:44:39 +08:00
Joel 85eeaee95a feat: vision valid 2024-08-08 14:40:08 +08:00
Joel 99bf3ff565 feat: params support vison 2024-08-08 14:22:54 +08:00
Joel 36ae154ca2 feat: classify support vision 2024-08-08 11:57:07 +08:00
Joel ef93d60534 chore: vision logic hooks 2024-08-08 11:36:44 +08:00
JzoNg 6c9a6b99e0 refactor textarea 2024-08-08 11:14:52 +08:00
JzoNg b73f05fdf0 new style of textarea 2024-08-08 11:14:52 +08:00
StyleZhang 26bca75884 file uploader 2024-08-08 10:27:43 +08:00
Joel e2962da1b8 chore: add visioin disabled tip check 2024-08-07 11:33:19 +08:00
NFishandJoel 1b9ebb8037 feat: add disabled support to tooltip-plus component (#7036) 2024-08-07 11:33:19 +08:00
crazywoolaandJoel a945a45b06 doc: correct typos in mdx files (#7029) 2024-08-07 11:33:19 +08:00
AchimandJoel be829a8103 Provide output data also in json property of workflow tool (#6924) (#7027) 2024-08-07 11:33:19 +08:00
crazywoolaandJoel 9432d41e60 fix: typos in wenxin llm (#7021) 2024-08-07 11:33:19 +08:00
Sa ZhangandJoel 0beeb4ab3e fix: Fix incorrect context size for jina-reranker-v2 model (#7006) 2024-08-07 11:33:19 +08:00
d7e057be44 fix: tran list issue (#7009)
Co-authored-by: libing <libing@healink.cn>
2024-08-07 11:33:19 +08:00
JyongandJoel 81b11c08d0 Fix/reranking mode is null (#7012) 2024-08-07 11:33:19 +08:00
Joel 83a5cdfff9 feat: agent app support generate prompt (#7007) 2024-08-07 11:33:19 +08:00
yanghxandJoel c837218bc9 fix #6902 .docx handles images within tables and handles cross-column tables (#6951) 2024-08-07 11:33:19 +08:00
crazywoolaandJoel 68552893ef fix: code-block-missing-checks (#7002) 2024-08-07 11:33:19 +08:00
灰灰andJoel 5ba93ed064 fix: code tool fails when null property exists in object (#6988) 2024-08-07 11:33:19 +08:00
Yi XiaoandJoel 959107f553 Feat/new confirm (#6984) 2024-08-07 11:33:19 +08:00
YeforiandJoel 443d929137 feat: add function calling for deepseek models (#6990) 2024-08-07 11:33:19 +08:00
Vico ChuandJoel 1e04418023 Chores: fix name typo (#6987) 2024-08-07 11:33:19 +08:00
小羽andJoel aeda8869bc feat:nvidia add nemotron4-340b and microsoft/phi-3 (#6973) 2024-08-07 11:33:19 +08:00
非法操作andJoel 10eed02ec4 chore: update duckduckgo tool (#6983) 2024-08-07 11:33:19 +08:00
Dr. Artificial曾小健andJoel 2472c4f890 fix doc (#6974) 2024-08-07 11:33:19 +08:00
Joel 0455e4e1a5 feat: llm support vision 2024-08-07 11:33:19 +08:00
StyleZhang 251ab5418f file-uploader i18n 2024-08-06 18:03:38 +08:00
Joel 38e6e40900 feat: config vision comp 2024-08-06 17:33:02 +08:00
Joel b3a3672857 chore: new required field 2024-08-06 15:42:22 +08:00
Joel 53a3c199ec chore: input slider 2024-08-06 15:33:38 +08:00
Joel fca5af5073 feat: max number with slider 2024-08-06 15:25:53 +08:00
Joel 77d0aac1d3 feat: support custom file type 2024-08-06 14:59:26 +08:00
StyleZhang fd0f8f33b5 Merge branch 'main' into feat/attachments 2024-08-06 09:58:53 +08:00
Joel 0be99ad01c feat: select file types 2024-08-02 18:17:13 +08:00
StyleZhang a05d16375e Merge branch 'main' into feat/attachments 2024-08-02 16:30:10 +08:00
Joel 0480bb03c3 feat: new input types 2024-08-02 11:43:01 +08:00
StyleZhang 19dfc6d9a8 file uploader 2024-08-02 10:21:20 +08:00
Joel d361675159 chore: some select style 2024-08-01 17:54:33 +08:00
Joel 23ae150298 feat: fiter condion 2024-08-01 17:10:02 +08:00
Joel 81383d7c74 feat: sub var picker 2024-08-01 14:43:17 +08:00
Joel 573f653789 feat: order by 2024-08-01 11:40:55 +08:00
Joel f1b61861b6 Merge branch 'main' into feat/attachments 2024-07-31 17:59:10 +08:00
Joel 8ecee8abce fix: ts problem 2024-07-31 17:02:11 +08:00
Joel e9ce9c1f47 feat: limit config 2024-07-31 17:01:26 +08:00
Joel 944fea4cc9 feat: list filter type and outpt var 2024-07-31 16:31:51 +08:00
StyleZhang 25c029877a progress circle 2024-07-31 15:15:26 +08:00
StyleZhang 9c31c56115 file uploader 2024-07-30 16:19:20 +08:00
StyleZhang 56507c9f7a chat input area 2024-07-30 13:48:39 +08:00
StyleZhang b322dda3f6 Merge branch 'main' into feat/attachments 2024-07-30 10:06:40 +08:00
StyleZhang 52d69dd55b file uploader 2024-07-29 17:22:11 +08:00
StyleZhang 0451c5590c Merge branch 'main' into feat/attachments 2024-07-29 10:19:11 +08:00
StyleZhang 2498c238b2 file-uploader 2024-07-26 16:54:45 +08:00
Joel 6e15d7f777 feat: doc extract inputs 2024-07-26 15:00:29 +08:00
Joel f6caf0915b chore: block bg to utils color 2024-07-26 14:23:34 +08:00
Joel 09aa14ca82 feat: node icons 2024-07-26 14:11:03 +08:00
Joel 394f06a27a feat: list filter 2024-07-26 11:55:09 +08:00
Joel 6fafd410d2 feat: doc extract struct 2024-07-26 11:21:17 +08:00
StyleZhang 1668df104f Merge branch 'main' into feat/attachments 2024-07-26 08:49:17 +08:00
StyleZhang d376b8540e add file-uploader 2024-07-25 16:41:09 +08:00
877 changed files with 6822 additions and 27819 deletions
+4 -3
View File
@@ -27,17 +27,18 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Poetry
uses: abatilo/actions-poetry@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
cache-dependency-path: |
api/pyproject.toml
api/poetry.lock
- name: Install Poetry
uses: abatilo/actions-poetry@v3
- name: Check Poetry lockfile
run: |
poetry check -C api --lock
+1
View File
@@ -5,6 +5,7 @@ on:
branches:
- "main"
- "deploy/dev"
- "release/0.10.0-beta"
release:
types: [published]
+4 -3
View File
@@ -23,17 +23,18 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Poetry
uses: abatilo/actions-poetry@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
cache-dependency-path: |
api/pyproject.toml
api/poetry.lock
- name: Install Poetry
uses: abatilo/actions-poetry@v3
- name: Install dependencies
run: poetry install -C api
+3 -4
View File
@@ -24,16 +24,15 @@ jobs:
with:
files: api/**
- name: Install Poetry
uses: abatilo/actions-poetry@v3
- name: Set up Python
uses: actions/setup-python@v5
if: steps.changed-files.outputs.any_changed == 'true'
with:
python-version: '3.10'
- name: Install Poetry
if: steps.changed-files.outputs.any_changed == 'true'
uses: abatilo/actions-poetry@v3
- name: Python dependencies
if: steps.changed-files.outputs.any_changed == 'true'
run: poetry install -C api --only lint
-2
View File
@@ -175,8 +175,6 @@ docker/volumes/pgvector/data/*
docker/volumes/pgvecto_rs/data/*
docker/nginx/conf.d/default.conf
docker/nginx/ssl/*
!docker/nginx/ssl/.gitkeep
docker/middleware.env
sdks/python-client/build
+2 -3
View File
@@ -6,9 +6,8 @@ Dify is licensed under the Apache License 2.0, with the following additional con
a. Multi-tenant service: Unless explicitly authorized by Dify in writing, you may not use the Dify source code to operate a multi-tenant environment.
- Tenant Definition: Within the context of Dify, one tenant corresponds to one workspace. The workspace provides a separated area for each tenant's data and configurations.
b. LOGO and copyright information: In the process of using Dify's frontend, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend.
- Frontend Definition: For the purposes of this license, the "frontend" of Dify includes all components located in the `web/` directory when running Dify from the raw source code, or the "web" image when running Dify with Docker.
b. LOGO and copyright information: In the process of using Dify's frontend components, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend components.
Please contact business@dify.ai by email to inquire about licensing matters.
+1 -5
View File
@@ -1,9 +1,5 @@
![cover-v5-optimized](https://github.com/langgenius/dify/assets/13230914/f9e19af5-61ba-4119-b926-d10c4c06ebab)
<p align="center">
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast</a>
</p>
<p align="center">
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Self-hosting</a> ·
@@ -172,7 +168,7 @@ Star Dify on GitHub and be instantly notified of new releases.
> Before installing Dify, make sure your machine meets the following minimum system requirements:
>
>- CPU >= 2 Core
>- RAM >= 4 GiB
>- RAM >= 4GB
</br>
+2 -2
View File
@@ -154,7 +154,7 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI
我们提供[ Dify 云服务](https://dify.ai),任何人都可以零设置尝试。它提供了自部署版本的所有功能,并在沙盒计划中包含 200 次免费的 GPT-4 调用。
- **自托管 Dify 社区版</br>**
使用这个[入门指南](#快速启动)快速在您的环境中运行 Dify。
使用这个[入门指南](#quick-start)快速在您的环境中运行 Dify。
使用我们的[文档](https://docs.dify.ai)进行进一步的参考和更深入的说明。
- **面向企业/组织的 Dify</br>**
@@ -174,7 +174,7 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI
在安装 Dify 之前,请确保您的机器满足以下最低系统要求:
- CPU >= 2 Core
- RAM >= 4 GiB
- RAM >= 4GB
### 快速启动
+2 -23
View File
@@ -31,17 +31,8 @@ REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=difyai123456
REDIS_USE_SSL=false
REDIS_DB=0
# redis Sentinel configuration.
REDIS_USE_SENTINEL=false
REDIS_SENTINELS=
REDIS_SENTINEL_SERVICE_NAME=
REDIS_SENTINEL_USERNAME=
REDIS_SENTINEL_PASSWORD=
REDIS_SENTINEL_SOCKET_TIMEOUT=0.1
# PostgreSQL database configuration
DB_USERNAME=postgres
DB_PASSWORD=difyai123456
@@ -51,7 +42,7 @@ DB_DATABASE=dify
# Storage configuration
# use for store upload files, private keys...
# storage type: local, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase
# storage type: local, s3, azure-blob, google-storage, tencent-cos, huawei-obs, volcengine-tos, baidu-obs, supabase
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=storage
S3_USE_AWS_MANAGED_IAM=false
@@ -120,7 +111,7 @@ SUPABASE_URL=your-server-url
WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
# Vector database configuration, support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, vikingdb, upstash
# Vector database configuration, support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, vikingdb
VECTOR_STORE=weaviate
# Weaviate configuration
@@ -229,10 +220,6 @@ BAIDU_VECTOR_DB_DATABASE=dify
BAIDU_VECTOR_DB_SHARD=1
BAIDU_VECTOR_DB_REPLICAS=3
# Upstash configuration
UPSTASH_VECTOR_URL=your-server-url
UPSTASH_VECTOR_TOKEN=your-access-token
# ViKingDB configuration
VIKINGDB_ACCESS_KEY=your-ak
VIKINGDB_SECRET_KEY=your-sk
@@ -252,7 +239,6 @@ UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
# Model Configuration
MULTIMODAL_SEND_IMAGE_FORMAT=base64
PROMPT_GENERATION_MAX_TOKENS=512
CODE_GENERATION_MAX_TOKENS=1024
# Mail configuration, support: resend, smtp
MAIL_TYPE=
@@ -318,10 +304,6 @@ RESPECT_XFORWARD_HEADERS_ENABLED=false
# Log file path
LOG_FILE=
# Log file max size, the unit is MB
LOG_FILE_MAX_SIZE=20
# Log file max backup count
LOG_FILE_BACKUP_COUNT=5
# Indexing configuration
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=1000
@@ -348,6 +330,3 @@ POSITION_TOOL_EXCLUDES=
POSITION_PROVIDER_PINS=
POSITION_PROVIDER_INCLUDES=
POSITION_PROVIDER_EXCLUDES=
# Reset password token expiry minutes
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
+1 -3
View File
@@ -55,9 +55,7 @@ RUN apt-get update \
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
&& apt-get update \
# For Security
&& apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1 expat=2.6.3-1 libldap-2.5-0=2.5.18+dfsg-3+b1 perl=5.40.0-6 libsqlite3-0=3.46.1-1 \
# install a chinese font to support the use of tools like matplotlib
&& apt-get install -y fonts-noto-cjk \
&& apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1 expat=2.6.3-1 libldap-2.5-0=2.5.18+dfsg-3 perl=5.38.2-5 libsqlite3-0=3.46.0-1 \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
-1
View File
@@ -85,4 +85,3 @@
cd ../
poetry run -C api bash dev/pytest/pytest_all_tests.sh
```
+213 -8
View File
@@ -1,7 +1,5 @@
import os
from configs import dify_config
if os.environ.get("DEBUG", "false").lower() != "true":
from gevent import monkey
@@ -12,20 +10,44 @@ if os.environ.get("DEBUG", "false").lower() != "true":
grpc.experimental.gevent.init_gevent()
import json
import logging
import sys
import threading
import time
import warnings
from logging.handlers import RotatingFileHandler
from flask import Response
from flask import Flask, Response, request
from flask_cors import CORS
from werkzeug.exceptions import Unauthorized
from app_factory import create_app
import contexts
from commands import register_commands
from configs import dify_config
# DO NOT REMOVE BELOW
from events import event_handlers # noqa: F401
from extensions import (
ext_celery,
ext_code_based_extension,
ext_compress,
ext_database,
ext_hosting_provider,
ext_login,
ext_mail,
ext_migrate,
ext_proxy_fix,
ext_redis,
ext_sentry,
ext_storage,
)
from extensions.ext_database import db
from extensions.ext_login import login_manager
from libs.passport import PassportService
# TODO: Find a way to avoid importing models here
from models import account, dataset, model, source, task, tool, tools, web # noqa: F401
from services.account_service import AccountService
# DO NOT REMOVE ABOVE
@@ -38,11 +60,194 @@ if hasattr(time, "tzset"):
time.tzset()
class DifyApp(Flask):
pass
# -------------
# Configuration
# -------------
config_type = os.getenv("EDITION", default="SELF_HOSTED") # ce edition first
# ----------------------------
# Application Factory Function
# ----------------------------
def create_flask_app_with_configs() -> Flask:
"""
create a raw flask app
with configs loaded from .env file
"""
dify_app = DifyApp(__name__)
dify_app.config.from_mapping(dify_config.model_dump())
# populate configs into system environment variables
for key, value in dify_app.config.items():
if isinstance(value, str):
os.environ[key] = value
elif isinstance(value, int | float | bool):
os.environ[key] = str(value)
elif value is None:
os.environ[key] = ""
return dify_app
def create_app() -> Flask:
app = create_flask_app_with_configs()
app.secret_key = app.config["SECRET_KEY"]
log_handlers = None
log_file = app.config.get("LOG_FILE")
if log_file:
log_dir = os.path.dirname(log_file)
os.makedirs(log_dir, exist_ok=True)
log_handlers = [
RotatingFileHandler(
filename=log_file,
maxBytes=1024 * 1024 * 1024,
backupCount=5,
),
logging.StreamHandler(sys.stdout),
]
logging.basicConfig(
level=app.config.get("LOG_LEVEL"),
format=app.config["LOG_FORMAT"],
datefmt=app.config.get("LOG_DATEFORMAT"),
handlers=log_handlers,
force=True,
)
log_tz = app.config.get("LOG_TZ")
if log_tz:
from datetime import datetime
import pytz
timezone = pytz.timezone(log_tz)
def time_converter(seconds):
return datetime.utcfromtimestamp(seconds).astimezone(timezone).timetuple()
for handler in logging.root.handlers:
assert handler.formatter
handler.formatter.converter = time_converter
initialize_extensions(app)
register_blueprints(app)
register_commands(app)
return app
def initialize_extensions(app):
# Since the application instance is now created, pass it to each Flask
# extension instance to bind it to the Flask application instance (app)
ext_compress.init_app(app)
ext_code_based_extension.init()
ext_database.init_app(app)
ext_migrate.init(app, db)
ext_redis.init_app(app)
ext_storage.init_app(app)
ext_celery.init_app(app)
ext_login.init_app(app)
ext_mail.init_app(app)
ext_hosting_provider.init_app(app)
ext_sentry.init_app(app)
ext_proxy_fix.init_app(app)
# Flask-Login configuration
@login_manager.request_loader
def load_user_from_request(request_from_flask_login):
"""Load user based on the request."""
if request.blueprint not in {"console", "inner_api"}:
return None
# Check if the user_id contains a dot, indicating the old format
auth_header = request.headers.get("Authorization", "")
if not auth_header:
auth_token = request.args.get("_token")
if not auth_token:
raise Unauthorized("Invalid Authorization token.")
else:
if " " not in auth_header:
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != "bearer":
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
decoded = PassportService().verify(auth_token)
user_id = decoded.get("user_id")
logged_in_account = AccountService.load_logged_in_account(account_id=user_id)
if logged_in_account:
contexts.tenant_id.set(logged_in_account.current_tenant_id)
return logged_in_account
@login_manager.unauthorized_handler
def unauthorized_handler():
"""Handle unauthorized requests."""
return Response(
json.dumps({"code": "unauthorized", "message": "Unauthorized."}),
status=401,
content_type="application/json",
)
# register blueprint routers
def register_blueprints(app):
from controllers.console import bp as console_app_bp
from controllers.files import bp as files_bp
from controllers.inner_api import bp as inner_api_bp
from controllers.service_api import bp as service_api_bp
from controllers.web import bp as web_bp
CORS(
service_api_bp,
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
)
app.register_blueprint(service_api_bp)
CORS(
web_bp,
resources={r"/*": {"origins": app.config["WEB_API_CORS_ALLOW_ORIGINS"]}},
supports_credentials=True,
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
expose_headers=["X-Version", "X-Env"],
)
app.register_blueprint(web_bp)
CORS(
console_app_bp,
resources={r"/*": {"origins": app.config["CONSOLE_CORS_ALLOW_ORIGINS"]}},
supports_credentials=True,
allow_headers=["Content-Type", "Authorization"],
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
expose_headers=["X-Version", "X-Env"],
)
app.register_blueprint(console_app_bp)
CORS(files_bp, allow_headers=["Content-Type"], methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"])
app.register_blueprint(files_bp)
app.register_blueprint(inner_api_bp)
# create app
app = create_app()
celery = app.extensions["celery"]
if dify_config.TESTING:
if app.config.get("TESTING"):
print("App is running in TESTING mode")
@@ -50,15 +255,15 @@ if dify_config.TESTING:
def after_request(response):
"""Add Version headers to the response."""
response.set_cookie("remember_token", "", expires=0)
response.headers.add("X-Version", dify_config.CURRENT_VERSION)
response.headers.add("X-Env", dify_config.DEPLOY_ENV)
response.headers.add("X-Version", app.config["CURRENT_VERSION"])
response.headers.add("X-Env", app.config["DEPLOY_ENV"])
return response
@app.route("/health")
def health():
return Response(
json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}),
json.dumps({"pid": os.getpid(), "status": "ok", "version": app.config["CURRENT_VERSION"]}),
status=200,
content_type="application/json",
)
-176
View File
@@ -1,176 +0,0 @@
import os
if os.environ.get("DEBUG", "false").lower() != "true":
from gevent import monkey
monkey.patch_all()
import grpc.experimental.gevent
grpc.experimental.gevent.init_gevent()
import json
from flask import Flask, Response, request
from flask_cors import CORS
from werkzeug.exceptions import Unauthorized
import contexts
from commands import register_commands
from configs import dify_config
from extensions import (
ext_celery,
ext_code_based_extension,
ext_compress,
ext_database,
ext_hosting_provider,
ext_logging,
ext_login,
ext_mail,
ext_migrate,
ext_proxy_fix,
ext_redis,
ext_sentry,
ext_storage,
)
from extensions.ext_database import db
from extensions.ext_login import login_manager
from libs.passport import PassportService
from services.account_service import AccountService
class DifyApp(Flask):
pass
# ----------------------------
# Application Factory Function
# ----------------------------
def create_flask_app_with_configs() -> Flask:
"""
create a raw flask app
with configs loaded from .env file
"""
dify_app = DifyApp(__name__)
dify_app.config.from_mapping(dify_config.model_dump())
# populate configs into system environment variables
for key, value in dify_app.config.items():
if isinstance(value, str):
os.environ[key] = value
elif isinstance(value, int | float | bool):
os.environ[key] = str(value)
elif value is None:
os.environ[key] = ""
return dify_app
def create_app() -> Flask:
app = create_flask_app_with_configs()
app.secret_key = dify_config.SECRET_KEY
initialize_extensions(app)
register_blueprints(app)
register_commands(app)
return app
def initialize_extensions(app):
# Since the application instance is now created, pass it to each Flask
# extension instance to bind it to the Flask application instance (app)
ext_logging.init_app(app)
ext_compress.init_app(app)
ext_code_based_extension.init()
ext_database.init_app(app)
ext_migrate.init(app, db)
ext_redis.init_app(app)
ext_storage.init_app(app)
ext_celery.init_app(app)
ext_login.init_app(app)
ext_mail.init_app(app)
ext_hosting_provider.init_app(app)
ext_sentry.init_app(app)
ext_proxy_fix.init_app(app)
# Flask-Login configuration
@login_manager.request_loader
def load_user_from_request(request_from_flask_login):
"""Load user based on the request."""
if request.blueprint not in {"console", "inner_api"}:
return None
# Check if the user_id contains a dot, indicating the old format
auth_header = request.headers.get("Authorization", "")
if not auth_header:
auth_token = request.args.get("_token")
if not auth_token:
raise Unauthorized("Invalid Authorization token.")
else:
if " " not in auth_header:
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
auth_scheme, auth_token = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != "bearer":
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
decoded = PassportService().verify(auth_token)
user_id = decoded.get("user_id")
logged_in_account = AccountService.load_logged_in_account(account_id=user_id)
if logged_in_account:
contexts.tenant_id.set(logged_in_account.current_tenant_id)
return logged_in_account
@login_manager.unauthorized_handler
def unauthorized_handler():
"""Handle unauthorized requests."""
return Response(
json.dumps({"code": "unauthorized", "message": "Unauthorized."}),
status=401,
content_type="application/json",
)
# register blueprint routers
def register_blueprints(app):
from controllers.console import bp as console_app_bp
from controllers.files import bp as files_bp
from controllers.inner_api import bp as inner_api_bp
from controllers.service_api import bp as service_api_bp
from controllers.web import bp as web_bp
CORS(
service_api_bp,
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
)
app.register_blueprint(service_api_bp)
CORS(
web_bp,
resources={r"/*": {"origins": dify_config.WEB_API_CORS_ALLOW_ORIGINS}},
supports_credentials=True,
allow_headers=["Content-Type", "Authorization", "X-App-Code"],
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
expose_headers=["X-Version", "X-Env"],
)
app.register_blueprint(web_bp)
CORS(
console_app_bp,
resources={r"/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}},
supports_credentials=True,
allow_headers=["Content-Type", "Authorization"],
methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
expose_headers=["X-Version", "X-Env"],
)
app.register_blueprint(console_app_bp)
CORS(files_bp, allow_headers=["Content-Type"], methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"])
app.register_blueprint(files_bp)
app.register_blueprint(inner_api_bp)
+56 -26
View File
@@ -259,26 +259,6 @@ def migrate_knowledge_vector_database():
skipped_count = 0
total_count = 0
vector_type = dify_config.VECTOR_STORE
upper_colletion_vector_types = {
VectorType.MILVUS,
VectorType.PGVECTOR,
VectorType.RELYT,
VectorType.WEAVIATE,
VectorType.ORACLE,
VectorType.ELASTICSEARCH,
}
lower_colletion_vector_types = {
VectorType.ANALYTICDB,
VectorType.CHROMA,
VectorType.MYSCALE,
VectorType.PGVECTO_RS,
VectorType.TIDB_VECTOR,
VectorType.OPENSEARCH,
VectorType.TENCENT,
VectorType.BAIDU,
VectorType.VIKINGDB,
VectorType.UPSTASH,
}
page = 1
while True:
try:
@@ -304,9 +284,11 @@ def migrate_knowledge_vector_database():
skipped_count = skipped_count + 1
continue
collection_name = ""
dataset_id = dataset.id
if vector_type in upper_colletion_vector_types:
if vector_type == VectorType.WEAVIATE:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": VectorType.WEAVIATE, "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.QDRANT:
if dataset.collection_binding_id:
dataset_collection_binding = (
@@ -319,15 +301,63 @@ def migrate_knowledge_vector_database():
else:
raise ValueError("Dataset Collection Binding not found")
else:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": VectorType.QDRANT, "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type in lower_colletion_vector_types:
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
elif vector_type == VectorType.MILVUS:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": VectorType.MILVUS, "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.RELYT:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": "relyt", "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.TENCENT:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": VectorType.TENCENT, "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.PGVECTOR:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": VectorType.PGVECTOR, "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.OPENSEARCH:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {
"type": VectorType.OPENSEARCH,
"vector_store": {"class_prefix": collection_name},
}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.ANALYTICDB:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {
"type": VectorType.ANALYTICDB,
"vector_store": {"class_prefix": collection_name},
}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.ELASTICSEARCH:
dataset_id = dataset.id
index_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {"type": "elasticsearch", "vector_store": {"class_prefix": index_name}}
dataset.index_struct = json.dumps(index_struct_dict)
elif vector_type == VectorType.BAIDU:
dataset_id = dataset.id
collection_name = Dataset.gen_collection_name_by_id(dataset_id)
index_struct_dict = {
"type": VectorType.BAIDU,
"vector_store": {"class_prefix": collection_name},
}
dataset.index_struct = json.dumps(index_struct_dict)
else:
raise ValueError(f"Vector store {vector_type} is not supported.")
index_struct_dict = {"type": vector_type, "vector_store": {"class_prefix": collection_name}}
dataset.index_struct = json.dumps(index_struct_dict)
vector = Vector(dataset)
click.echo(f"Migrating dataset {dataset.id}.")
+6 -83
View File
@@ -1,15 +1,6 @@
from typing import Annotated, Literal, Optional
from pydantic import (
AliasChoices,
Field,
HttpUrl,
NegativeInt,
NonNegativeInt,
PositiveFloat,
PositiveInt,
computed_field,
)
from pydantic import AliasChoices, Field, HttpUrl, NegativeInt, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings
from configs.feature.hosted_service import HostedServiceConfig
@@ -27,24 +18,9 @@ class SecurityConfig(BaseSettings):
default="",
)
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="Duration in minutes for which a password reset token remains valid",
default=5,
)
LOGIN_DISABLED: bool = Field(
description="Whether to disable login checks",
default=False,
)
ADMIN_API_KEY_ENABLE: bool = Field(
description="Whether to enable admin api key for authentication",
default=False,
)
ADMIN_API_KEY: Optional[str] = Field(
description="admin api key for authentication",
default=None,
RESET_PASSWORD_TOKEN_EXPIRY_HOURS: PositiveInt = Field(
description="Duration in hours for which a password reset token remains valid",
default=24,
)
@@ -319,16 +295,6 @@ class LoggingConfig(BaseSettings):
default=None,
)
LOG_FILE_MAX_SIZE: PositiveInt = Field(
description="Maximum file size for file rotation retention, the unit is megabytes (MB)",
default=20,
)
LOG_FILE_BACKUP_COUNT: PositiveInt = Field(
description="Maximum file backup count file rotation retention",
default=5,
)
LOG_FORMAT: str = Field(
description="Format string for log messages",
default="%(asctime)s.%(msecs)03d %(levelname)s [%(threadName)s] [%(filename)s:%(lineno)d] - %(message)s",
@@ -517,11 +483,6 @@ class MailConfig(BaseSettings):
default=False,
)
EMAIL_SEND_IP_LIMIT_PER_MINUTE: PositiveInt = Field(
description="Maximum number of emails allowed to be sent from the same IP address in a minute",
default=50,
)
class RagEtlConfig(BaseSettings):
"""
@@ -556,26 +517,16 @@ class DataSetConfig(BaseSettings):
Configuration for dataset management
"""
PLAN_SANDBOX_CLEAN_DAY_SETTING: PositiveInt = Field(
description="Interval in days for dataset cleanup operations - plan: sandbox",
CLEAN_DAY_SETTING: PositiveInt = Field(
description="Interval in days for dataset cleanup operations",
default=30,
)
PLAN_PRO_CLEAN_DAY_SETTING: PositiveInt = Field(
description="Interval in days for dataset cleanup operations - plan: pro and team",
default=7,
)
DATASET_OPERATOR_ENABLED: bool = Field(
description="Enable or disable dataset operator functionality",
default=False,
)
TIDB_SERVERLESS_NUMBER: PositiveInt = Field(
description="number of tidb serverless cluster",
default=500,
)
class WorkspaceConfig(BaseSettings):
"""
@@ -669,33 +620,6 @@ class PositionConfig(BaseSettings):
return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""}
class LoginConfig(BaseSettings):
ENABLE_EMAIL_CODE_LOGIN: bool = Field(
description="whether to enable email code login",
default=False,
)
ENABLE_EMAIL_PASSWORD_LOGIN: bool = Field(
description="whether to enable email password login",
default=True,
)
ENABLE_SOCIAL_OAUTH_LOGIN: bool = Field(
description="whether to enable github/google oauth login",
default=False,
)
EMAIL_CODE_LOGIN_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
description="expiry time in minutes for email code login token",
default=5,
)
ALLOW_REGISTER: bool = Field(
description="whether to enable register",
default=False,
)
ALLOW_CREATE_WORKSPACE: bool = Field(
description="whether to enable create workspace",
default=False,
)
class FeatureConfig(
# place the configs in alphabet order
AppExecutionConfig,
@@ -721,7 +645,6 @@ class FeatureConfig(
UpdateConfig,
WorkflowConfig,
WorkspaceConfig,
LoginConfig,
# hosted services config
HostedServiceConfig,
CeleryBeatConfig,
+1 -11
View File
@@ -27,9 +27,7 @@ from configs.middleware.vdb.pgvectors_config import PGVectoRSConfig
from configs.middleware.vdb.qdrant_config import QdrantConfig
from configs.middleware.vdb.relyt_config import RelytConfig
from configs.middleware.vdb.tencent_vector_config import TencentVectorDBConfig
from configs.middleware.vdb.tidb_on_qdrant_config import TidbOnQdrantConfig
from configs.middleware.vdb.tidb_vector_config import TiDBVectorConfig
from configs.middleware.vdb.upstash_config import UpstashConfig
from configs.middleware.vdb.vikingdb_config import VikingDBConfig
from configs.middleware.vdb.weaviate_config import WeaviateConfig
@@ -37,8 +35,7 @@ from configs.middleware.vdb.weaviate_config import WeaviateConfig
class StorageConfig(BaseSettings):
STORAGE_TYPE: str = Field(
description="Type of storage to use."
" Options: 'local', 's3', 'aliyun-oss', 'azure-blob', 'baidu-obs', 'google-storage', 'huawei-obs', "
"'oci-storage', 'tencent-cos', 'volcengine-tos', 'supabase'. Default is 'local'.",
" Options: 'local', 's3', 'azure-blob', 'aliyun-oss', 'google-storage'. Default is 'local'.",
default="local",
)
@@ -55,11 +52,6 @@ class VectorStoreConfig(BaseSettings):
default=None,
)
VECTOR_STORE_WHITELIST_ENABLE: Optional[bool] = Field(
description="Enable whitelist for vector store.",
default=False,
)
class KeywordStoreConfig(BaseSettings):
KEYWORD_STORE: str = Field(
@@ -253,7 +245,5 @@ class MiddlewareConfig(
ElasticsearchConfig,
InternalTestConfig,
VikingDBConfig,
UpstashConfig,
TidbOnQdrantConfig,
):
pass
+1 -1
View File
@@ -14,7 +14,7 @@ class OracleConfig(BaseSettings):
default=None,
)
ORACLE_PORT: PositiveInt = Field(
ORACLE_PORT: Optional[PositiveInt] = Field(
description="Port number on which the Oracle database server is listening (default is 1521)",
default=1521,
)
@@ -14,7 +14,7 @@ class PGVectorConfig(BaseSettings):
default=None,
)
PGVECTOR_PORT: PositiveInt = Field(
PGVECTOR_PORT: Optional[PositiveInt] = Field(
description="Port number on which the PostgreSQL server is listening (default is 5433)",
default=5433,
)
@@ -14,7 +14,7 @@ class PGVectoRSConfig(BaseSettings):
default=None,
)
PGVECTO_RS_PORT: PositiveInt = Field(
PGVECTO_RS_PORT: Optional[PositiveInt] = Field(
description="Port number on which the PostgreSQL server with PGVecto.RS is listening (default is 5431)",
default=5431,
)
@@ -1,65 +0,0 @@
from typing import Optional
from pydantic import Field, NonNegativeInt, PositiveInt
from pydantic_settings import BaseSettings
class TidbOnQdrantConfig(BaseSettings):
"""
Tidb on Qdrant configs
"""
TIDB_ON_QDRANT_URL: Optional[str] = Field(
description="Tidb on Qdrant url",
default=None,
)
TIDB_ON_QDRANT_API_KEY: Optional[str] = Field(
description="Tidb on Qdrant api key",
default=None,
)
TIDB_ON_QDRANT_CLIENT_TIMEOUT: NonNegativeInt = Field(
description="Tidb on Qdrant client timeout in seconds",
default=20,
)
TIDB_ON_QDRANT_GRPC_ENABLED: bool = Field(
description="whether enable grpc support for Tidb on Qdrant connection",
default=False,
)
TIDB_ON_QDRANT_GRPC_PORT: PositiveInt = Field(
description="Tidb on Qdrant grpc port",
default=6334,
)
TIDB_PUBLIC_KEY: Optional[str] = Field(
description="Tidb account public key",
default=None,
)
TIDB_PRIVATE_KEY: Optional[str] = Field(
description="Tidb account private key",
default=None,
)
TIDB_API_URL: Optional[str] = Field(
description="Tidb API url",
default=None,
)
TIDB_IAM_API_URL: Optional[str] = Field(
description="Tidb IAM API url",
default=None,
)
TIDB_REGION: Optional[str] = Field(
description="Tidb serverless region",
default="regions/aws-us-east-1",
)
TIDB_PROJECT_ID: Optional[str] = Field(
description="Tidb project id",
default=None,
)
@@ -1,20 +0,0 @@
from typing import Optional
from pydantic import Field
from pydantic_settings import BaseSettings
class UpstashConfig(BaseSettings):
"""
Configuration settings for Upstash vector database
"""
UPSTASH_VECTOR_URL: Optional[str] = Field(
description="URL of the upstash server (e.g., 'https://vector.upstash.io')",
default=None,
)
UPSTASH_VECTOR_TOKEN: Optional[str] = Field(
description="Token for authenticating with the upstash server",
default=None,
)
+12 -24
View File
@@ -11,39 +11,27 @@ class VikingDBConfig(BaseModel):
"""
VIKINGDB_ACCESS_KEY: Optional[str] = Field(
description="The Access Key provided by Volcengine VikingDB for API authentication."
"Refer to the following documentation for details on obtaining credentials:"
"https://www.volcengine.com/docs/6291/65568",
default=None,
default=None, description="The Access Key provided by Volcengine VikingDB for API authentication."
)
VIKINGDB_SECRET_KEY: Optional[str] = Field(
description="The Secret Key provided by Volcengine VikingDB for API authentication.",
default=None,
default=None, description="The Secret Key provided by Volcengine VikingDB for API authentication."
)
VIKINGDB_REGION: str = Field(
description="The region of the Volcengine VikingDB service.(e.g., 'cn-shanghai', 'cn-beijing').",
VIKINGDB_REGION: Optional[str] = Field(
default="cn-shanghai",
description="The region of the Volcengine VikingDB service.(e.g., 'cn-shanghai', 'cn-beijing').",
)
VIKINGDB_HOST: str = Field(
VIKINGDB_HOST: Optional[str] = Field(
default="api-vikingdb.mlp.cn-shanghai.volces.com",
description="The host of the Volcengine VikingDB service.(e.g., 'api-vikingdb.volces.com', \
'api-vikingdb.mlp.cn-shanghai.volces.com')",
default="api-vikingdb.mlp.cn-shanghai.volces.com",
)
VIKINGDB_SCHEME: str = Field(
description="The scheme of the Volcengine VikingDB service.(e.g., 'http', 'https').",
VIKINGDB_SCHEME: Optional[str] = Field(
default="http",
description="The scheme of the Volcengine VikingDB service.(e.g., 'http', 'https').",
)
VIKINGDB_CONNECTION_TIMEOUT: int = Field(
description="The connection timeout of the Volcengine VikingDB service.",
default=30,
VIKINGDB_CONNECTION_TIMEOUT: Optional[int] = Field(
default=30, description="The connection timeout of the Volcengine VikingDB service."
)
VIKINGDB_SOCKET_TIMEOUT: int = Field(
description="The socket timeout of the Volcengine VikingDB service.",
default=30,
VIKINGDB_SOCKET_TIMEOUT: Optional[int] = Field(
default=30, description="The socket timeout of the Volcengine VikingDB service."
)
+1 -1
View File
@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
default="0.10.2",
default="0.10.0-beta3",
)
COMMIT_SHA: str = Field(
+3 -6
View File
@@ -12,13 +12,10 @@ VIDEO_EXTENSIONS.extend([ext.upper() for ext in VIDEO_EXTENSIONS])
AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "webm", "amr"]
AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"]
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
if dify_config.ETL_TYPE == "Unstructured":
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "pdf", "html", "htm", "xlsx", "xls"]
DOCUMENT_EXTENSIONS.extend(("docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
if dify_config.UNSTRUCTURED_API_URL:
DOCUMENT_EXTENSIONS.append("ppt")
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
else:
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"]
DOCUMENT_EXTENSIONS.extend(("docx", "csv", "eml", "msg", "pptx", "ppt", "xml", "epub"))
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
+3 -3
View File
@@ -1,10 +1,10 @@
import os
from functools import wraps
from flask import request
from flask_restful import Resource, reqparse
from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config
from constants.languages import supported_language
from controllers.console import api
from controllers.console.wraps import only_edition_cloud
@@ -15,7 +15,7 @@ from models.model import App, InstalledApp, RecommendedApp
def admin_required(view):
@wraps(view)
def decorated(*args, **kwargs):
if not dify_config.ADMIN_API_KEY:
if not os.getenv("ADMIN_API_KEY"):
raise Unauthorized("API key is invalid.")
auth_header = request.headers.get("Authorization")
@@ -31,7 +31,7 @@ def admin_required(view):
if auth_scheme != "bearer":
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
if dify_config.ADMIN_API_KEY != auth_token:
if os.getenv("ADMIN_API_KEY") != auth_token:
raise Unauthorized("API key is invalid.")
return view(*args, **kwargs)
-35
View File
@@ -52,39 +52,4 @@ class RuleGenerateApi(Resource):
return rules
class RuleCodeGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("instruction", type=str, required=True, nullable=False, location="json")
parser.add_argument("model_config", type=dict, required=True, nullable=False, location="json")
parser.add_argument("no_variable", type=bool, required=True, default=False, location="json")
parser.add_argument("code_language", type=str, required=False, default="javascript", location="json")
args = parser.parse_args()
account = current_user
CODE_GENERATION_MAX_TOKENS = int(os.getenv("CODE_GENERATION_MAX_TOKENS", "1024"))
try:
code_result = LLMGenerator.generate_code(
tenant_id=account.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
code_language=args["code_language"],
max_tokens=CODE_GENERATION_MAX_TOKENS,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e:
raise CompletionRequestError(e.description)
return code_result
api.add_resource(RuleGenerateApi, "/rule-generate")
api.add_resource(RuleCodeGenerateApi, "/rule-code-generate")
-2
View File
@@ -105,8 +105,6 @@ class ChatMessageListApi(Resource):
if rest_count > 0:
has_more = True
history_messages = list(reversed(history_messages))
return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more)
@@ -10,10 +10,10 @@ from controllers.console import api
from controllers.console.app.wraps import get_app_model
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from enums import WorkflowRunTriggeredFrom
from extensions.ext_database import db
from libs.helper import DatetimeString
from libs.login import login_required
from models.enums import WorkflowRunTriggeredFrom
from models.model import AppMode
+19 -19
View File
@@ -1,15 +1,17 @@
import base64
import datetime
import secrets
from flask import request
from flask_restful import Resource, reqparse
from constants.languages import supported_language
from controllers.console import api
from controllers.console.error import AlreadyActivateError
from extensions.ext_database import db
from libs.helper import StrLen, email, extract_remote_ip, timezone
from models.account import AccountStatus, Tenant
from services.account_service import AccountService, RegisterService
from libs.helper import StrLen, email, timezone
from libs.password import hash_password, valid_password
from models.account import AccountStatus
from services.account_service import RegisterService
class ActivateCheckApi(Resource):
@@ -25,18 +27,8 @@ class ActivateCheckApi(Resource):
token = args["token"]
invitation = RegisterService.get_invitation_if_token_valid(workspaceId, reg_email, token)
if invitation:
data = invitation.get("data", {})
tenant: Tenant = invitation.get("tenant", None)
workspace_name = tenant.name if tenant else None
workspace_id = tenant.id if tenant else None
invitee_email = data.get("email") if data else None
return {
"is_valid": invitation is not None,
"data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email},
}
else:
return {"is_valid": False}
return {"is_valid": invitation is not None, "workspace_name": invitation["tenant"].name if invitation else None}
class ActivateApi(Resource):
@@ -46,6 +38,7 @@ class ActivateApi(Resource):
parser.add_argument("email", type=email, required=False, nullable=True, location="json")
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
parser.add_argument("password", type=valid_password, required=True, nullable=False, location="json")
parser.add_argument(
"interface_language", type=supported_language, required=True, nullable=False, location="json"
)
@@ -61,6 +54,15 @@ class ActivateApi(Resource):
account = invitation["account"]
account.name = args["name"]
# generate password salt
salt = secrets.token_bytes(16)
base64_salt = base64.b64encode(salt).decode()
# encrypt password with salt
password_hashed = hash_password(args["password"], salt)
base64_password_hashed = base64.b64encode(password_hashed).decode()
account.password = base64_password_hashed
account.password_salt = base64_salt
account.interface_language = args["interface_language"]
account.timezone = args["timezone"]
account.interface_theme = "light"
@@ -68,9 +70,7 @@ class ActivateApi(Resource):
account.initialized_at = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
db.session.commit()
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
return {"result": "success", "data": token_pair.model_dump()}
return {"result": "success"}
api.add_resource(ActivateCheckApi, "/activate/check")
+1 -25
View File
@@ -27,29 +27,5 @@ class InvalidTokenError(BaseHTTPException):
class PasswordResetRateLimitExceededError(BaseHTTPException):
error_code = "password_reset_rate_limit_exceeded"
description = "Too many password reset emails have been sent. Please try again in 1 minutes."
code = 429
class EmailCodeError(BaseHTTPException):
error_code = "email_code_error"
description = "Email code is invalid or expired."
code = 400
class EmailOrPasswordMismatchError(BaseHTTPException):
error_code = "email_or_password_mismatch"
description = "The email or password is mismatched."
code = 400
class EmailPasswordLoginLimitError(BaseHTTPException):
error_code = "email_code_login_limit"
description = "Too many incorrect password attempts. Please try again later."
code = 429
class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
error_code = "email_code_login_rate_limit_exceeded"
description = "Too many login emails have been sent. Please try again in 5 minutes."
description = "Password reset rate limit exceeded. Try again later."
code = 429
+30 -64
View File
@@ -1,82 +1,65 @@
import base64
import logging
import secrets
from flask import request
from flask_restful import Resource, reqparse
from constants.languages import languages
from controllers.console import api
from controllers.console.auth.error import (
EmailCodeError,
InvalidEmailError,
InvalidTokenError,
PasswordMismatchError,
PasswordResetRateLimitExceededError,
)
from controllers.console.error import EmailSendIpLimitError, NotAllowedRegister
from controllers.console.setup import setup_required
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
from libs.helper import email, extract_remote_ip
from libs.helper import email as email_validate
from libs.password import hash_password, valid_password
from models.account import Account
from services.account_service import AccountService, TenantService
from services.errors.workspace import WorkSpaceNotAllowedCreateError
from services.feature_service import FeatureService
from models import Account
from services.account_service import AccountService
from services.errors.account import RateLimitExceededError
class ForgotPasswordSendEmailApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("language", type=str, required=False, location="json")
parser.add_argument("email", type=str, required=True, location="json")
args = parser.parse_args()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
email = args["email"]
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
if not email_validate(email):
raise InvalidEmailError()
account = Account.query.filter_by(email=email).first()
if account:
try:
AccountService.send_reset_password_email(account=account)
except RateLimitExceededError:
logging.warning(f"Rate limit exceeded for email: {account.email}")
raise PasswordResetRateLimitExceededError()
else:
language = "en-US"
# Return success to avoid revealing email registration status
logging.warning(f"Attempt to reset password for unregistered email: {email}")
account = Account.query.filter_by(email=args["email"]).first()
token = None
if account is None:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
return {"result": "fail", "data": token, "code": "account_not_found"}
else:
raise NotAllowedRegister()
else:
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
return {"result": "success", "data": token}
return {"result": "success"}
class ForgotPasswordCheckApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json")
parser.add_argument("code", type=str, required=True, location="json")
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
token = args["token"]
user_email = args["email"]
reset_data = AccountService.get_reset_password_data(token)
token_data = AccountService.get_reset_password_data(args["token"])
if token_data is None:
raise InvalidTokenError()
if user_email != token_data.get("email"):
raise InvalidEmailError()
if args["code"] != token_data.get("code"):
raise EmailCodeError()
return {"is_valid": True, "email": token_data.get("email")}
if reset_data is None:
return {"is_valid": False, "email": None}
return {"is_valid": True, "email": reset_data.get("email")}
class ForgotPasswordResetApi(Resource):
@@ -109,26 +92,9 @@ class ForgotPasswordResetApi(Resource):
base64_password_hashed = base64.b64encode(password_hashed).decode()
account = Account.query.filter_by(email=reset_data.get("email")).first()
if account:
account.password = base64_password_hashed
account.password_salt = base64_salt
db.session.commit()
tenant = TenantService.get_join_tenants(account)
if not tenant and not FeatureService.get_system_features().is_allow_create_workspace:
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
tenant_was_created.send(tenant)
else:
try:
account = AccountService.create_account_and_tenant(
email=reset_data.get("email"),
name=reset_data.get("email"),
password=password_confirm,
interface_language=languages[0],
)
except WorkSpaceNotAllowedCreateError:
pass
account.password = base64_password_hashed
account.password_salt = base64_salt
db.session.commit()
return {"result": "success"}
+49 -148
View File
@@ -5,29 +5,12 @@ from flask import request
from flask_restful import Resource, reqparse
import services
from constants.languages import languages
from controllers.console import api
from controllers.console.auth.error import (
EmailCodeError,
EmailOrPasswordMismatchError,
EmailPasswordLoginLimitError,
InvalidEmailError,
InvalidTokenError,
)
from controllers.console.error import (
AccountBannedError,
EmailSendIpLimitError,
NotAllowedCreateWorkspace,
NotAllowedRegister,
)
from controllers.console.setup import setup_required
from events.tenant_event import tenant_was_created
from libs.helper import email, extract_remote_ip
from libs.password import valid_password
from models.account import Account
from services.account_service import AccountService, RegisterService, TenantService
from services.errors.workspace import WorkSpaceNotAllowedCreateError
from services.feature_service import FeatureService
from models import Account
from services.account_service import AccountService, TenantService
class LoginApi(Resource):
@@ -40,43 +23,15 @@ class LoginApi(Resource):
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("password", type=valid_password, required=True, location="json")
parser.add_argument("remember_me", type=bool, required=False, default=False, location="json")
parser.add_argument("invite_token", type=str, required=False, default=None, location="json")
parser.add_argument("language", type=str, required=False, default="en-US", location="json")
args = parser.parse_args()
is_login_error_rate_limit = AccountService.is_login_error_rate_limit(args["email"])
if is_login_error_rate_limit:
raise EmailPasswordLoginLimitError()
invitation = args["invite_token"]
if invitation:
invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation)
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
# todo: Verify the recaptcha
try:
if invitation:
data = invitation.get("data", {})
invitee_email = data.get("email") if data else None
if invitee_email != args["email"]:
raise InvalidEmailError()
account = AccountService.authenticate(args["email"], args["password"], args["invite_token"])
else:
account = AccountService.authenticate(args["email"], args["password"])
except services.errors.account.AccountLoginError:
raise AccountBannedError()
except services.errors.account.AccountPasswordError:
AccountService.add_login_error_rate_limit(args["email"])
raise EmailOrPasswordMismatchError()
except services.errors.account.AccountNotFoundError:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
return {"result": "fail", "data": token, "code": "account_not_found"}
else:
raise NotAllowedRegister()
account = AccountService.authenticate(args["email"], args["password"])
except services.errors.account.AccountLoginError as e:
return {"code": "unauthorized", "message": str(e)}, 401
# SELF_HOSTED only have one workspace
tenants = TenantService.get_join_tenants(account)
if len(tenants) == 0:
@@ -86,7 +41,7 @@ class LoginApi(Resource):
}
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(args["email"])
return {"result": "success", "data": token_pair.model_dump()}
@@ -94,111 +49,60 @@ class LogoutApi(Resource):
@setup_required
def get(self):
account = cast(Account, flask_login.current_user)
if isinstance(account, flask_login.AnonymousUserMixin):
return {"result": "success"}
AccountService.logout(account=account)
flask_login.logout_user()
return {"result": "success"}
class ResetPasswordSendEmailApi(Resource):
class ResetPasswordApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("language", type=str, required=False, location="json")
args = parser.parse_args()
def get(self):
# parser = reqparse.RequestParser()
# parser.add_argument('email', type=email, required=True, location='json')
# args = parser.parse_args()
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
# import mailchimp_transactional as MailchimpTransactional
# from mailchimp_transactional.api_client import ApiClientError
account = AccountService.get_user_through_email(args["email"])
if account is None:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_reset_password_email(email=args["email"], language=language)
else:
raise NotAllowedRegister()
else:
token = AccountService.send_reset_password_email(account=account, language=language)
# account = {'email': args['email']}
# account = AccountService.get_by_email(args['email'])
# if account is None:
# raise ValueError('Email not found')
# new_password = AccountService.generate_password()
# AccountService.update_password(account, new_password)
return {"result": "success", "data": token}
# todo: Send email
# MAILCHIMP_API_KEY = dify_config.MAILCHIMP_TRANSACTIONAL_API_KEY
# mailchimp = MailchimpTransactional(MAILCHIMP_API_KEY)
# message = {
# 'from_email': 'noreply@example.com',
# 'to': [{'email': account['email']}],
# 'subject': 'Reset your Dify password',
# 'html': """
# <p>Dear User,</p>
# <p>The Dify team has generated a new password for you, details as follows:</p>
# <p><strong>{new_password}</strong></p>
# <p>Please change your password to log in as soon as possible.</p>
# <p>Regards,</p>
# <p>The Dify Team</p>
# """
# }
class EmailCodeLoginSendEmailApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("language", type=str, required=False, location="json")
args = parser.parse_args()
# response = mailchimp.messages.send({
# 'message': message,
# # required for transactional email
# ' settings': {
# 'sandbox_mode': dify_config.MAILCHIMP_SANDBOX_MODE,
# },
# })
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
# Check if MSG was sent
# if response.status_code != 200:
# # handle error
# pass
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
account = AccountService.get_user_through_email(args["email"])
if account is None:
if FeatureService.get_system_features().is_allow_register:
token = AccountService.send_email_code_login_email(email=args["email"], language=language)
else:
raise NotAllowedRegister()
else:
token = AccountService.send_email_code_login_email(account=account, language=language)
return {"result": "success", "data": token}
class EmailCodeLoginApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json")
parser.add_argument("code", type=str, required=True, location="json")
parser.add_argument("token", type=str, required=True, location="json")
args = parser.parse_args()
user_email = args["email"]
token_data = AccountService.get_email_code_login_data(args["token"])
if token_data is None:
raise InvalidTokenError()
if token_data["email"] != args["email"]:
raise InvalidEmailError()
if token_data["code"] != args["code"]:
raise EmailCodeError()
AccountService.revoke_email_code_login_token(args["token"])
account = AccountService.get_user_through_email(user_email)
if account:
tenant = TenantService.get_join_tenants(account)
if not tenant:
if not FeatureService.get_system_features().is_allow_create_workspace:
raise NotAllowedCreateWorkspace()
else:
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
tenant_was_created.send(tenant)
if account is None:
try:
account = AccountService.create_account_and_tenant(
email=user_email, name=user_email, interface_language=languages[0]
)
except WorkSpaceNotAllowedCreateError:
return NotAllowedCreateWorkspace()
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(args["email"])
return {"result": "success", "data": token_pair.model_dump()}
return {"result": "success"}
class RefreshTokenApi(Resource):
@@ -216,7 +120,4 @@ class RefreshTokenApi(Resource):
api.add_resource(LoginApi, "/login")
api.add_resource(LogoutApi, "/logout")
api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login")
api.add_resource(EmailCodeLoginApi, "/email-code-login/validity")
api.add_resource(ResetPasswordSendEmailApi, "/reset-password")
api.add_resource(RefreshTokenApi, "/refresh-token")
+6 -55
View File
@@ -5,20 +5,15 @@ from typing import Optional
import requests
from flask import current_app, redirect, request
from flask_restful import Resource
from werkzeug.exceptions import Unauthorized
from configs import dify_config
from constants.languages import languages
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
from libs.helper import extract_remote_ip
from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
from models import Account
from models.account import AccountStatus
from services.account_service import AccountService, RegisterService, TenantService
from services.errors.account import AccountNotFoundError
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
from services.feature_service import FeatureService
from .. import api
@@ -48,7 +43,6 @@ def get_oauth_providers():
class OAuthLogin(Resource):
def get(self, provider: str):
invite_token = request.args.get("invite_token") or None
OAUTH_PROVIDERS = get_oauth_providers()
with current_app.app_context():
oauth_provider = OAUTH_PROVIDERS.get(provider)
@@ -56,7 +50,7 @@ class OAuthLogin(Resource):
if not oauth_provider:
return {"error": "Invalid provider"}, 400
auth_url = oauth_provider.get_authorization_url(invite_token=invite_token)
auth_url = oauth_provider.get_authorization_url()
return redirect(auth_url)
@@ -69,11 +63,6 @@ class OAuthCallback(Resource):
return {"error": "Invalid provider"}, 400
code = request.args.get("code")
state = request.args.get("state")
invite_token = None
if state:
invite_token = state
try:
token = oauth_provider.get_access_token(code)
user_info = oauth_provider.get_user_info(token)
@@ -81,43 +70,17 @@ class OAuthCallback(Resource):
logging.exception(f"An error occurred during the OAuth process with {provider}: {e.response.text}")
return {"error": "OAuth process failed"}, 400
if invite_token and RegisterService.is_valid_invite_token(invite_token):
invitation = RegisterService._get_invitation_by_token(token=invite_token)
if invitation:
invitation_email = invitation.get("email", None)
if invitation_email != user_info.email:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Invalid invitation token.")
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin/invite-settings?invite_token={invite_token}")
try:
account = _generate_account(provider, user_info)
except AccountNotFoundError:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Account not found.")
except (WorkSpaceNotFoundError, WorkSpaceNotAllowedCreateError):
return redirect(
f"{dify_config.CONSOLE_WEB_URL}/signin"
"?message=Workspace not found, please contact system admin to invite you to join in a workspace."
)
account = _generate_account(provider, user_info)
# Check account status
if account.status == AccountStatus.BANNED.value:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Account is banned.")
if account.status in {AccountStatus.BANNED.value, AccountStatus.CLOSED.value}:
return {"error": "Account is banned or closed."}, 403
if account.status == AccountStatus.PENDING.value:
account.status = AccountStatus.ACTIVE.value
account.initialized_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.session.commit()
try:
TenantService.create_owner_tenant_if_not_exist(account)
except Unauthorized:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Workspace not found.")
except WorkSpaceNotAllowedCreateError:
return redirect(
f"{dify_config.CONSOLE_WEB_URL}/signin"
"?message=Workspace not found, please contact system admin to invite you to join in a workspace."
)
TenantService.create_owner_tenant_if_not_exist(account)
token_pair = AccountService.login(
account=account,
@@ -142,20 +105,8 @@ def _generate_account(provider: str, user_info: OAuthUserInfo):
# Get account by openid or email.
account = _get_account_by_openid_or_email(provider, user_info)
if account:
tenant = TenantService.get_join_tenants(account)
if not tenant:
if not FeatureService.get_system_features().is_allow_create_workspace:
raise WorkSpaceNotAllowedCreateError()
else:
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
tenant_was_created.send(tenant)
if not account:
if not FeatureService.get_system_features().is_allow_register:
raise AccountNotFoundError()
# Create account
account_name = user_info.name or "Dify"
account = RegisterService.register(
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider
@@ -102,13 +102,6 @@ class DatasetListApi(Resource):
help="type is required. Name must be between 1 to 40 characters.",
type=_validate_name,
)
parser.add_argument(
"description",
type=str,
nullable=True,
required=False,
default="",
)
parser.add_argument(
"indexing_technique",
type=str,
@@ -147,7 +140,6 @@ class DatasetListApi(Resource):
dataset = DatasetService.create_empty_dataset(
tenant_id=current_user.current_tenant_id,
name=args["name"],
description=args["description"],
indexing_technique=args["indexing_technique"],
account=current_user,
permission=DatasetPermissionEnum.ONLY_ME,
@@ -627,7 +619,6 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.PGVECTO_RS
| VectorType.BAIDU
| VectorType.VIKINGDB
| VectorType.UPSTASH
):
return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
case (
@@ -639,7 +630,6 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.ORACLE
| VectorType.ELASTICSEARCH
| VectorType.PGVECTOR
| VectorType.TIDB_ON_QDRANT
):
return {
"retrieval_method": [
@@ -667,7 +657,6 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.PGVECTO_RS
| VectorType.BAIDU
| VectorType.VIKINGDB
| VectorType.UPSTASH
):
return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
case (
+8 -11
View File
@@ -2,7 +2,7 @@ import urllib.parse
from flask import request
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from flask_restful import Resource, marshal_with
import services
from configs import dify_config
@@ -30,12 +30,13 @@ class FileApi(Resource):
@account_initialization_required
@marshal_with(upload_config_fields)
def get(self):
file_size_limit = dify_config.UPLOAD_FILE_SIZE_LIMIT
batch_count_limit = dify_config.UPLOAD_FILE_BATCH_LIMIT
image_file_size_limit = dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT
return {
"file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT,
"batch_count_limit": dify_config.UPLOAD_FILE_BATCH_LIMIT,
"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT,
"video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT,
"audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT,
"file_size_limit": file_size_limit,
"batch_count_limit": batch_count_limit,
"image_file_size_limit": image_file_size_limit,
}, 200
@setup_required
@@ -47,10 +48,6 @@ class FileApi(Resource):
# get file from request
file = request.files["file"]
parser = reqparse.RequestParser()
parser.add_argument("source", type=str, required=False, location="args")
source = parser.parse_args().get("source")
# check file
if "file" not in request.files:
raise NoFileUploadedError()
@@ -58,7 +55,7 @@ class FileApi(Resource):
if len(request.files) > 1:
raise TooManyFilesError()
try:
upload_file = FileService.upload_file(file=file, user=current_user, source=source)
upload_file = FileService.upload_file(file=file, user=current_user)
except services.errors.file.FileTooLargeError as file_too_large_error:
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
@@ -1,24 +1,88 @@
from flask_restful import Resource
import logging
from flask_login import current_user
from flask_restful import Resource, marshal, reqparse
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services
from controllers.console import api
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
from controllers.console.app.error import (
CompletionRequestError,
ProviderModelCurrentlyNotSupportError,
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
from controllers.console.datasets.error import DatasetNotInitializedError
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from core.errors.error import (
LLMBadRequestError,
ModelCurrentlyNotSupportError,
ProviderTokenNotInitError,
QuotaExceededError,
)
from core.model_runtime.errors.invoke import InvokeError
from fields.hit_testing_fields import hit_testing_record_fields
from libs.login import login_required
from services.dataset_service import DatasetService
from services.hit_testing_service import HitTestingService
class HitTestingApi(Resource, DatasetsHitTestingBase):
class HitTestingApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self, dataset_id):
dataset_id_str = str(dataset_id)
dataset = self.get_and_validate_dataset(dataset_id_str)
args = self.parse_args()
self.hit_testing_args_check(args)
dataset = DatasetService.get_dataset(dataset_id_str)
if dataset is None:
raise NotFound("Dataset not found.")
return self.perform_hit_testing(dataset, args)
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
parser = reqparse.RequestParser()
parser.add_argument("query", type=str, location="json")
parser.add_argument("retrieval_model", type=dict, required=False, location="json")
parser.add_argument("external_retrieval_model", type=dict, required=False, location="json")
args = parser.parse_args()
HitTestingService.hit_testing_args_check(args)
try:
response = HitTestingService.retrieve(
dataset=dataset,
query=args["query"],
account=current_user,
retrieval_model=args["retrieval_model"],
external_retrieval_model=args["external_retrieval_model"],
limit=10,
)
return {"query": response["query"], "records": marshal(response["records"], hit_testing_record_fields)}
except services.errors.index.IndexNotInitializedError:
raise DatasetNotInitializedError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except LLMBadRequestError:
raise ProviderNotInitializeError(
"No Embedding Model or Reranking Model available. Please configure a valid provider "
"in the Settings -> Model Provider."
)
except InvokeError as e:
raise CompletionRequestError(e.description)
except ValueError as e:
raise ValueError(str(e))
except Exception as e:
logging.exception("Hit testing failed.")
raise InternalServerError(str(e))
api.add_resource(HitTestingApi, "/datasets/<uuid:dataset_id>/hit-testing")
@@ -1,85 +0,0 @@
import logging
from flask_login import current_user
from flask_restful import marshal, reqparse
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
import services.dataset_service
from controllers.console.app.error import (
CompletionRequestError,
ProviderModelCurrentlyNotSupportError,
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
from controllers.console.datasets.error import DatasetNotInitializedError
from core.errors.error import (
LLMBadRequestError,
ModelCurrentlyNotSupportError,
ProviderTokenNotInitError,
QuotaExceededError,
)
from core.model_runtime.errors.invoke import InvokeError
from fields.hit_testing_fields import hit_testing_record_fields
from services.dataset_service import DatasetService
from services.hit_testing_service import HitTestingService
class DatasetsHitTestingBase:
@staticmethod
def get_and_validate_dataset(dataset_id: str):
dataset = DatasetService.get_dataset(dataset_id)
if dataset is None:
raise NotFound("Dataset not found.")
try:
DatasetService.check_dataset_permission(dataset, current_user)
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
return dataset
@staticmethod
def hit_testing_args_check(args):
HitTestingService.hit_testing_args_check(args)
@staticmethod
def parse_args():
parser = reqparse.RequestParser()
parser.add_argument("query", type=str, location="json")
parser.add_argument("retrieval_model", type=dict, required=False, location="json")
parser.add_argument("external_retrieval_model", type=dict, required=False, location="json")
return parser.parse_args()
@staticmethod
def perform_hit_testing(dataset, args):
try:
response = HitTestingService.retrieve(
dataset=dataset,
query=args["query"],
account=current_user,
retrieval_model=args["retrieval_model"],
external_retrieval_model=args["external_retrieval_model"],
limit=10,
)
return {"query": response["query"], "records": marshal(response["records"], hit_testing_record_fields)}
except services.errors.index.IndexNotInitializedError:
raise DatasetNotInitializedError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except LLMBadRequestError:
raise ProviderNotInitializeError(
"No Embedding Model or Reranking Model available. Please configure a valid provider "
"in the Settings -> Model Provider."
)
except InvokeError as e:
raise CompletionRequestError(e.description)
except ValueError as e:
raise ValueError(str(e))
except Exception as e:
logging.exception("Hit testing failed.")
raise InternalServerError(str(e))
-24
View File
@@ -38,27 +38,3 @@ class AlreadyActivateError(BaseHTTPException):
error_code = "already_activate"
description = "Auth Token is invalid or account already activated, please check again."
code = 403
class NotAllowedCreateWorkspace(BaseHTTPException):
error_code = "not_allowed_create_workspace"
description = "Workspace not found, please contact system admin to invite you to join in a workspace."
code = 400
class AccountBannedError(BaseHTTPException):
error_code = "account_banned"
description = "Account is banned."
code = 400
class NotAllowedRegister(BaseHTTPException):
error_code = "unauthorized"
description = "Account not found."
code = 400
class EmailSendIpLimitError(BaseHTTPException):
error_code = "email_send_ip_limit"
description = "Too many emails have been sent from this IP address recently. Please try again later."
code = 429
+2 -12
View File
@@ -21,12 +21,7 @@ class AppParameterApi(InstalledAppResource):
"options": fields.List(fields.String),
}
system_parameters_fields = {
"image_file_size_limit": fields.Integer,
"video_file_size_limit": fields.Integer,
"audio_file_size_limit": fields.Integer,
"file_size_limit": fields.Integer,
}
system_parameters_fields = {"image_file_size_limit": fields.String}
parameters_fields = {
"opening_statement": fields.String,
@@ -87,12 +82,7 @@ class AppParameterApi(InstalledAppResource):
}
},
),
"system_parameters": {
"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT,
"video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT,
"audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT,
"file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT,
},
"system_parameters": {"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT},
}
+10 -29
View File
@@ -1,5 +1,5 @@
from flask import Response, request
from flask_restful import Resource, reqparse
from flask_restful import Resource
from werkzeug.exceptions import NotFound
import services
@@ -10,10 +10,6 @@ from services.file_service import FileService
class ImagePreviewApi(Resource):
"""
Deprecated
"""
def get(self, file_id):
file_id = str(file_id)
@@ -41,39 +37,24 @@ class FilePreviewApi(Resource):
def get(self, file_id):
file_id = str(file_id)
parser = reqparse.RequestParser()
parser.add_argument("timestamp", type=str, required=True, location="args")
parser.add_argument("nonce", type=str, required=True, location="args")
parser.add_argument("sign", type=str, required=True, location="args")
parser.add_argument("as_attachment", type=bool, required=False, default=False, location="args")
timestamp = request.args.get("timestamp")
nonce = request.args.get("nonce")
sign = request.args.get("sign")
args = parser.parse_args()
if not args["timestamp"] or not args["nonce"] or not args["sign"]:
if not timestamp or not nonce or not sign:
return {"content": "Invalid request."}, 400
try:
generator, upload_file = FileService.get_file_generator_by_file_id(
generator, mimetype = FileService.get_signed_file_preview(
file_id=file_id,
timestamp=args["timestamp"],
nonce=args["nonce"],
sign=args["sign"],
timestamp=timestamp,
nonce=nonce,
sign=sign,
)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
response = Response(
generator,
mimetype=upload_file.mime_type,
direct_passthrough=True,
headers={},
)
if upload_file.size > 0:
response.headers["Content-Length"] = str(upload_file.size)
if args["as_attachment"]:
response.headers["Content-Disposition"] = f"attachment; filename={upload_file.name}"
return response
return Response(generator, mimetype=mimetype)
class WorkspaceWebappLogoApi(Resource):
+5 -15
View File
@@ -16,7 +16,6 @@ class ToolFilePreviewApi(Resource):
parser.add_argument("timestamp", type=str, required=True, location="args")
parser.add_argument("nonce", type=str, required=True, location="args")
parser.add_argument("sign", type=str, required=True, location="args")
parser.add_argument("as_attachment", type=bool, required=False, default=False, location="args")
args = parser.parse_args()
@@ -29,27 +28,18 @@ class ToolFilePreviewApi(Resource):
raise Forbidden("Invalid request.")
try:
stream, tool_file = ToolFileManager.get_file_generator_by_tool_file_id(
result = ToolFileManager.get_file_generator_by_tool_file_id(
file_id,
)
if not stream or not tool_file:
if not result:
raise NotFound("file is not found")
generator, mimetype = result
except Exception:
raise UnsupportedFileTypeError()
response = Response(
stream,
mimetype=tool_file.mimetype,
direct_passthrough=True,
headers={},
)
if tool_file.size > 0:
response.headers["Content-Length"] = str(tool_file.size)
if args["as_attachment"]:
response.headers["Content-Disposition"] = f"attachment; filename={tool_file.name}"
return response
return Response(generator, mimetype=mimetype)
api.add_resource(ToolFilePreviewApi, "/files/tools/<uuid:file_id>.<string:extension>")
@@ -21,7 +21,7 @@ class EnterpriseWorkspace(Resource):
if account is None:
return {"message": "owner account not found."}, 404
tenant = TenantService.create_tenant(args["name"], is_from_dashboard=True)
tenant = TenantService.create_tenant(args["name"])
TenantService.create_tenant_member(tenant, account, role="owner")
tenant_was_created.send(tenant)
+2 -1
View File
@@ -5,6 +5,7 @@ from libs.external_api import ExternalApi
bp = Blueprint("service_api", __name__, url_prefix="/v1")
api = ExternalApi(bp)
from . import index
from .app import app, audio, completion, conversation, file, message, workflow
from .dataset import dataset, document, hit_testing, segment
from .dataset import dataset, document, segment
+2 -12
View File
@@ -21,12 +21,7 @@ class AppParameterApi(Resource):
"options": fields.List(fields.String),
}
system_parameters_fields = {
"image_file_size_limit": fields.Integer,
"video_file_size_limit": fields.Integer,
"audio_file_size_limit": fields.Integer,
"file_size_limit": fields.Integer,
}
system_parameters_fields = {"image_file_size_limit": fields.String}
parameters_fields = {
"opening_statement": fields.String,
@@ -86,12 +81,7 @@ class AppParameterApi(Resource):
}
},
),
"system_parameters": {
"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT,
"video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT,
"audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT,
"file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT,
},
"system_parameters": {"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT},
}
@@ -4,6 +4,7 @@ from flask_restful import Resource, reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
from constants import UUID_NIL
from controllers.service_api import api
from controllers.service_api.app.error import (
AppUnavailableError,
@@ -107,6 +108,7 @@ class ChatApi(Resource):
parser.add_argument("conversation_id", type=uuid_value, location="json")
parser.add_argument("retriever_from", type=str, required=False, default="dev", location="json")
parser.add_argument("auto_generate_name", type=bool, required=False, default=True, location="json")
parser.add_argument("parent_message_id", type=uuid_value, required=False, default=UUID_NIL, location="json")
args = parser.parse_args()
+1 -1
View File
@@ -48,7 +48,7 @@ class MessageListApi(Resource):
"tool_input": fields.String,
"created_at": TimestampField,
"observation": fields.String,
"message_files": fields.List(fields.Nested(message_file_fields)),
"message_files": fields.List(fields.String),
}
message_fields = {
@@ -66,13 +66,6 @@ class DatasetListApi(DatasetApiResource):
help="type is required. Name must be between 1 to 40 characters.",
type=_validate_name,
)
parser.add_argument(
"description",
type=str,
nullable=True,
required=False,
default="",
)
parser.add_argument(
"indexing_technique",
type=str,
@@ -115,7 +108,6 @@ class DatasetListApi(DatasetApiResource):
dataset = DatasetService.create_empty_dataset(
tenant_id=tenant_id,
name=args["name"],
description=args["description"],
indexing_technique=args["indexing_technique"],
account=current_user,
permission=args["permission"],
@@ -1,17 +0,0 @@
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
from controllers.service_api import api
from controllers.service_api.wraps import DatasetApiResource
class HitTestingApi(DatasetApiResource, DatasetsHitTestingBase):
def post(self, tenant_id, dataset_id):
dataset_id_str = str(dataset_id)
dataset = self.get_and_validate_dataset(dataset_id_str)
args = self.parse_args()
self.hit_testing_args_check(args)
return self.perform_hit_testing(dataset, args)
api.add_resource(HitTestingApi, "/datasets/<uuid:dataset_id>/hit-testing")
+2 -12
View File
@@ -21,12 +21,7 @@ class AppParameterApi(WebApiResource):
"options": fields.List(fields.String),
}
system_parameters_fields = {
"image_file_size_limit": fields.Integer,
"video_file_size_limit": fields.Integer,
"audio_file_size_limit": fields.Integer,
"file_size_limit": fields.Integer,
}
system_parameters_fields = {"image_file_size_limit": fields.String}
parameters_fields = {
"opening_statement": fields.String,
@@ -85,12 +80,7 @@ class AppParameterApi(WebApiResource):
}
},
),
"system_parameters": {
"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT,
"video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT,
"audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT,
"file_size_limit": dify_config.UPLOAD_FILE_SIZE_LIMIT,
},
"system_parameters": {"image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT},
}
+3 -7
View File
@@ -1,7 +1,7 @@
import urllib.parse
from flask import request
from flask_restful import marshal_with, reqparse
from flask_restful import marshal_with
import services
from controllers.web import api
@@ -18,10 +18,6 @@ class FileApi(WebApiResource):
# get file from request
file = request.files["file"]
parser = reqparse.RequestParser()
parser.add_argument("source", type=str, required=False, location="args")
source = parser.parse_args().get("source")
# check file
if "file" not in request.files:
raise NoFileUploadedError()
@@ -29,7 +25,7 @@ class FileApi(WebApiResource):
if len(request.files) > 1:
raise TooManyFilesError()
try:
upload_file = FileService.upload_file(file=file, user=end_user, source=source)
upload_file = FileService.upload_file(file, end_user)
except services.errors.file.FileTooLargeError as file_too_large_error:
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
@@ -46,7 +42,7 @@ class RemoteFileInfoApi(WebApiResource):
response = ssrf_proxy.head(decoded_url)
return {
"file_type": response.headers.get("Content-Type", "application/octet-stream"),
"file_length": int(response.headers.get("Content-Length", -1)),
"file_length": int(response.headers.get("Content-Length", 0)),
}
except Exception as e:
return {"error": str(e)}, 400
-12
View File
@@ -165,12 +165,6 @@ class BaseAgentRunner(AppRunner):
continue
parameter_type = parameter.type.as_normal_type()
if parameter.type in {
ToolParameter.ToolParameterType.SYSTEM_FILES,
ToolParameter.ToolParameterType.FILE,
ToolParameter.ToolParameterType.FILES,
}:
continue
enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT:
enum = [option.value for option in parameter.options]
@@ -256,12 +250,6 @@ class BaseAgentRunner(AppRunner):
continue
parameter_type = parameter.type.as_normal_type()
if parameter.type in {
ToolParameter.ToolParameterType.SYSTEM_FILES,
ToolParameter.ToolParameterType.FILE,
ToolParameter.ToolParameterType.FILES,
}:
continue
enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT:
enum = [option.value for option in parameter.options]
@@ -62,8 +62,6 @@ class CotAgentOutputParser:
thought_str = "thought:"
thought_idx = 0
last_character = ""
for response in llm_response:
if response.delta.usage:
usage_dict["usage"] = response.delta.usage
@@ -76,38 +74,35 @@ class CotAgentOutputParser:
while index < len(response):
steps = 1
delta = response[index : index + steps]
yield_delta = False
last_character = response[index - 1] if index > 0 else ""
if delta == "`":
last_character = delta
code_block_cache += delta
code_block_delimiter_count += 1
else:
if not in_code_block:
if code_block_delimiter_count > 0:
last_character = delta
yield code_block_cache
code_block_cache = ""
else:
last_character = delta
code_block_cache += delta
code_block_delimiter_count = 0
if not in_code_block and not in_json:
if delta.lower() == action_str[action_idx] and action_idx == 0:
if last_character not in {"\n", " ", ""}:
yield_delta = True
else:
last_character = delta
action_cache += delta
action_idx += 1
if action_idx == len(action_str):
action_cache = ""
action_idx = 0
index += steps
yield delta
continue
action_cache += delta
action_idx += 1
if action_idx == len(action_str):
action_cache = ""
action_idx = 0
index += steps
continue
elif delta.lower() == action_str[action_idx] and action_idx > 0:
last_character = delta
action_cache += delta
action_idx += 1
if action_idx == len(action_str):
@@ -117,25 +112,24 @@ class CotAgentOutputParser:
continue
else:
if action_cache:
last_character = delta
yield action_cache
action_cache = ""
action_idx = 0
if delta.lower() == thought_str[thought_idx] and thought_idx == 0:
if last_character not in {"\n", " ", ""}:
yield_delta = True
else:
last_character = delta
thought_cache += delta
thought_idx += 1
if thought_idx == len(thought_str):
thought_cache = ""
thought_idx = 0
index += steps
yield delta
continue
thought_cache += delta
thought_idx += 1
if thought_idx == len(thought_str):
thought_cache = ""
thought_idx = 0
index += steps
continue
elif delta.lower() == thought_str[thought_idx] and thought_idx > 0:
last_character = delta
thought_cache += delta
thought_idx += 1
if thought_idx == len(thought_str):
@@ -145,20 +139,12 @@ class CotAgentOutputParser:
continue
else:
if thought_cache:
last_character = delta
yield thought_cache
thought_cache = ""
thought_idx = 0
if yield_delta:
index += steps
last_character = delta
yield delta
continue
if code_block_delimiter_count == 3:
if in_code_block:
last_character = delta
yield from extra_json_from_code_block(code_block_cache)
code_block_cache = ""
@@ -170,10 +156,8 @@ class CotAgentOutputParser:
if delta == "{":
json_quote_count += 1
in_json = True
last_character = delta
json_cache += delta
elif delta == "}":
last_character = delta
json_cache += delta
if json_quote_count > 0:
json_quote_count -= 1
@@ -184,19 +168,16 @@ class CotAgentOutputParser:
continue
else:
if in_json:
last_character = delta
json_cache += delta
if got_json:
got_json = False
last_character = delta
yield parse_action(json_cache)
json_cache = ""
json_quote_count = 0
in_json = False
if not in_code_block and not in_json:
last_character = delta
yield delta.replace("`", "")
index += steps
@@ -53,11 +53,11 @@ class BasicVariablesConfigManager:
VariableEntity(
type=variable_type,
variable=variable.get("variable"),
description=variable.get("description") or "",
description=variable.get("description", ""),
label=variable.get("label"),
required=variable.get("required", False),
max_length=variable.get("max_length"),
options=variable.get("options") or [],
options=variable.get("options", []),
)
)
+1 -11
View File
@@ -2,7 +2,7 @@ from collections.abc import Sequence
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, Field
from core.file import FileExtraConfig, FileTransferMethod, FileType
from core.model_runtime.entities.message_entities import PromptMessageRole
@@ -114,16 +114,6 @@ class VariableEntity(BaseModel):
allowed_file_extensions: Sequence[str] = Field(default_factory=list)
allowed_file_upload_methods: Sequence[FileTransferMethod] = Field(default_factory=list)
@field_validator("description", mode="before")
@classmethod
def convert_none_description(cls, v: Any) -> str:
return v or ""
@field_validator("options", mode="before")
@classmethod
def convert_none_options(cls, v: Any) -> Sequence[str]:
return v or []
class ExternalDataVariableEntity(BaseModel):
"""
@@ -17,13 +17,10 @@ class FileUploadConfigManager:
file_upload_dict = config.get("file_upload")
if file_upload_dict:
if file_upload_dict.get("enabled"):
transform_methods = file_upload_dict.get("allowed_file_upload_methods") or file_upload_dict.get(
"allowed_upload_methods", []
)
data = {
"image_config": {
"number_limits": file_upload_dict["number_limits"],
"transfer_methods": transform_methods,
"transfer_methods": file_upload_dict["allowed_file_upload_methods"],
}
}
@@ -10,7 +10,6 @@ from flask import Flask, current_app
from pydantic import ValidationError
import contexts
from constants import UUID_NIL
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner
@@ -23,10 +22,10 @@ from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity,
from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.ops_trace_manager import TraceQueueManager
from enums import CreatedByRole
from extensions.ext_database import db
from factories import file_factory
from models.account import Account
from models.enums import CreatedByRole
from models.model import App, Conversation, EndUser, Message
from models.workflow import Workflow
@@ -133,7 +132,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
else self._prepare_user_inputs(user_inputs=inputs, app_config=app_config, user_id=user.id, role=role),
query=query,
files=file_objs,
parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL,
parent_message_id=args.get("parent_message_id"),
user_id=user.id,
stream=stream,
invoke_from=invoke_from,
@@ -20,8 +20,8 @@ from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.enums import SystemVariableKey
from core.workflow.workflow_entry import WorkflowEntry
from enums import UserFrom
from extensions.ext_database import db
from models.enums import UserFrom
from models.model import App, Conversation, EndUser, Message
from models.workflow import ConversationVariable, WorkflowType
@@ -51,12 +51,12 @@ from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.ops_trace_manager import TraceQueueManager
from core.workflow.enums import SystemVariableKey
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes import NodeType
from enums import CreatedByRole
from enums.workflow_nodes import NodeType
from events.message_event import message_was_created
from extensions.ext_database import db
from models import Conversation, EndUser, Message, MessageFile
from models.account import Account
from models.enums import CreatedByRole
from models.workflow import (
Workflow,
WorkflowNodeExecution,
@@ -8,7 +8,6 @@ from typing import Any, Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
from constants import UUID_NIL
from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager
@@ -20,10 +19,10 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa
from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, InvokeFrom
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.ops_trace_manager import TraceQueueManager
from enums import CreatedByRole
from extensions.ext_database import db
from factories import file_factory
from models import Account, App, EndUser
from models.enums import CreatedByRole
logger = logging.getLogger(__name__)
@@ -141,7 +140,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
else self._prepare_user_inputs(user_inputs=inputs, app_config=app_config, user_id=user.id, role=role),
query=query,
files=file_objs,
parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL,
parent_message_id=args.get("parent_message_id"),
user_id=user.id,
stream=stream,
invoke_from=invoke_from,
+1 -1
View File
@@ -7,7 +7,7 @@ from factories import file_factory
if TYPE_CHECKING:
from core.app.app_config.entities import AppConfig, VariableEntity
from models.enums import CreatedByRole
from enums import CreatedByRole
class BaseAppGenerator:
+2 -3
View File
@@ -8,7 +8,6 @@ from typing import Any, Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
from constants import UUID_NIL
from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
@@ -20,10 +19,10 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa
from core.app.entities.app_invoke_entities import ChatAppGenerateEntity, InvokeFrom
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.ops_trace_manager import TraceQueueManager
from enums import CreatedByRole
from extensions.ext_database import db
from factories import file_factory
from models.account import Account
from models.enums import CreatedByRole
from models.model import App, EndUser
logger = logging.getLogger(__name__)
@@ -139,7 +138,7 @@ class ChatAppGenerator(MessageBasedAppGenerator):
else self._prepare_user_inputs(user_inputs=inputs, app_config=app_config, user_id=user.id, role=role),
query=query,
files=file_objs,
parent_message_id=args.get("parent_message_id") if invoke_from != InvokeFrom.SERVICE_API else UUID_NIL,
parent_message_id=args.get("parent_message_id"),
user_id=user.id,
invoke_from=invoke_from,
extras=extras,
@@ -19,10 +19,10 @@ from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueMa
from core.app.entities.app_invoke_entities import CompletionAppGenerateEntity, InvokeFrom
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.ops_trace_manager import TraceQueueManager
from enums import CreatedByRole
from extensions.ext_database import db
from factories import file_factory
from models import Account, App, EndUser, Message
from models.enums import CreatedByRole
from services.errors.app import MoreLikeThisDisabledError
from services.errors.message import MessageNotExistsError
@@ -27,7 +27,6 @@ from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBa
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from extensions.ext_database import db
from models import Account
from models.enums import CreatedByRole
from models.model import App, AppMode, AppModelConfig, Conversation, EndUser, Message, MessageFile
from services.errors.app_model_config import AppModelConfigBrokenError
from services.errors.conversation import ConversationCompletedError, ConversationNotExistsError
@@ -241,7 +240,7 @@ class MessageBasedAppGenerator(BaseAppGenerator):
belongs_to="user",
url=file.remote_url,
upload_file_id=file.related_id,
created_by_role=(CreatedByRole.ACCOUNT if account_id else CreatedByRole.END_USER),
created_by_role=("account" if account_id else "end_user"),
created_by=account_id or end_user_id or "",
)
db.session.add(message_file)
+1 -1
View File
@@ -22,10 +22,10 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat
from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.ops_trace_manager import TraceQueueManager
from enums import CreatedByRole
from extensions.ext_database import db
from factories import file_factory
from models import Account, App, EndUser, Workflow
from models.enums import CreatedByRole
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -13,8 +13,8 @@ from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.enums import SystemVariableKey
from core.workflow.workflow_entry import WorkflowEntry
from enums import UserFrom
from extensions.ext_database import db
from models.enums import UserFrom
from models.model import App, EndUser
from models.workflow import WorkflowType
+7 -5
View File
@@ -40,10 +40,11 @@ from core.workflow.graph_engine.entities.event import (
ParallelBranchRunSucceededEvent,
)
from core.workflow.graph_engine.entities.graph import Graph
from core.workflow.nodes import NodeType
from core.workflow.nodes.iteration import IterationNodeData
from core.workflow.nodes.node_mapping import node_type_classes_mapping
from core.workflow.nodes.base_node import BaseNode
from core.workflow.nodes.iteration.entities import IterationNodeData
from core.workflow.nodes.node_mapping import node_classes
from core.workflow.workflow_entry import WorkflowEntry
from enums import NodeType
from extensions.ext_database import db
from models.model import App
from models.workflow import Workflow
@@ -136,8 +137,9 @@ class WorkflowBasedAppRunner(AppRunner):
raise ValueError("iteration node id not found in workflow graph")
# Get node class
node_type = NodeType(iteration_node_config.get("data", {}).get("type"))
node_cls = node_type_classes_mapping[node_type]
node_type = NodeType.value_of(iteration_node_config.get("data", {}).get("type"))
node_cls = node_classes.get(node_type)
node_cls = cast(type[BaseNode], node_cls)
# init variable pool
variable_pool = VariablePool(
+10 -31
View File
@@ -2,9 +2,8 @@ from collections.abc import Mapping, Sequence
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
from pydantic import BaseModel, ConfigDict
from constants import UUID_NIL
from core.app.app_config.entities import AppConfig, EasyUIBasedAppConfig, WorkflowUIBasedAppConfig
from core.entities.provider_configuration import ProviderModelBundle
from core.file.models import File
@@ -117,36 +116,13 @@ class EasyUIBasedAppGenerateEntity(AppGenerateEntity):
model_config = ConfigDict(protected_namespaces=())
class ConversationAppGenerateEntity(AppGenerateEntity):
"""
Base entity for conversation-based app generation.
"""
conversation_id: Optional[str] = None
parent_message_id: Optional[str] = Field(
default=None,
description=(
"Starting from v0.9.0, parent_message_id is used to support message regeneration for internal chat API."
"For service API, we need to ensure its forward compatibility, "
"so passing in the parent_message_id as request arg is not supported for now. "
"It needs to be set to UUID_NIL so that the subsequent processing will treat it as legacy messages."
),
)
@field_validator("parent_message_id")
@classmethod
def validate_parent_message_id(cls, v, info: ValidationInfo):
if info.data.get("invoke_from") == InvokeFrom.SERVICE_API and v != UUID_NIL:
raise ValueError("parent_message_id should be UUID_NIL for service API")
return v
class ChatAppGenerateEntity(ConversationAppGenerateEntity, EasyUIBasedAppGenerateEntity):
class ChatAppGenerateEntity(EasyUIBasedAppGenerateEntity):
"""
Chat Application Generate Entity.
"""
pass
conversation_id: Optional[str] = None
parent_message_id: Optional[str] = None
class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity):
@@ -157,15 +133,16 @@ class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity):
pass
class AgentChatAppGenerateEntity(ConversationAppGenerateEntity, EasyUIBasedAppGenerateEntity):
class AgentChatAppGenerateEntity(EasyUIBasedAppGenerateEntity):
"""
Agent Chat Application Generate Entity.
"""
pass
conversation_id: Optional[str] = None
parent_message_id: Optional[str] = None
class AdvancedChatAppGenerateEntity(ConversationAppGenerateEntity):
class AdvancedChatAppGenerateEntity(AppGenerateEntity):
"""
Advanced Chat Application Generate Entity.
"""
@@ -173,6 +150,8 @@ class AdvancedChatAppGenerateEntity(ConversationAppGenerateEntity):
# app config
app_config: WorkflowUIBasedAppConfig
conversation_id: Optional[str] = None
parent_message_id: Optional[str] = None
workflow_run_id: Optional[str] = None
query: str
+2 -2
View File
@@ -5,10 +5,10 @@ from typing import Any, Optional
from pydantic import BaseModel, field_validator
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeRunMetadataKey
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes import NodeType
from core.workflow.nodes.base import BaseNodeData
from enums import NodeType
class QueueEvent(str, Enum):
@@ -53,7 +53,7 @@ class BasedGenerateTaskPipeline:
self._output_moderation_handler = self._init_output_moderation()
self._stream = stream
def _handle_error(self, event: QueueErrorEvent, message: Optional[Message] = None):
def _handle_error(self, event: QueueErrorEvent, message: Optional[Message] = None) -> Exception:
"""
Handle error event.
:param event: event
@@ -100,7 +100,7 @@ class BasedGenerateTaskPipeline:
return message
def _error_to_stream_response(self, e: Exception):
def _error_to_stream_response(self, e: Exception) -> ErrorStreamResponse:
"""
Error to stream response.
:param e: exception
@@ -4,8 +4,6 @@ from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
from typing import Any, Optional, Union, cast
from sqlalchemy.orm import Session
from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity
from core.app.entities.queue_entities import (
QueueIterationCompletedEvent,
@@ -36,12 +34,11 @@ from core.ops.entities.trace_entity import TraceTaskName
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask
from core.tools.tool_manager import ToolManager
from core.workflow.enums import SystemVariableKey
from core.workflow.nodes import NodeType
from core.workflow.nodes.tool.entities import ToolNodeData
from core.workflow.workflow_entry import WorkflowEntry
from enums import CreatedByRole, NodeType, WorkflowRunTriggeredFrom
from extensions.ext_database import db
from models.account import Account
from models.enums import CreatedByRole, WorkflowRunTriggeredFrom
from models.model import EndUser
from models.workflow import (
Workflow,
@@ -138,7 +135,7 @@ class WorkflowCycleManage:
outputs = WorkflowEntry.handle_special_values(outputs)
workflow_run.status = WorkflowRunStatus.SUCCEEDED.value
workflow_run.outputs = json.dumps(outputs or {})
workflow_run.outputs = json.dumps(outputs) if outputs else None
workflow_run.elapsed_time = time.perf_counter() - start_at
workflow_run.total_tokens = total_tokens
workflow_run.total_steps = total_steps
@@ -234,30 +231,30 @@ class WorkflowCycleManage:
self, workflow_run: WorkflowRun, event: QueueNodeStartedEvent
) -> WorkflowNodeExecution:
# init workflow node execution
workflow_node_execution = WorkflowNodeExecution()
workflow_node_execution.tenant_id = workflow_run.tenant_id
workflow_node_execution.app_id = workflow_run.app_id
workflow_node_execution.workflow_id = workflow_run.workflow_id
workflow_node_execution.triggered_from = WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value
workflow_node_execution.workflow_run_id = workflow_run.id
workflow_node_execution.predecessor_node_id = event.predecessor_node_id
workflow_node_execution.index = event.node_run_index
workflow_node_execution.node_execution_id = event.node_execution_id
workflow_node_execution.node_id = event.node_id
workflow_node_execution.node_type = event.node_type.value
workflow_node_execution.title = event.node_data.title
workflow_node_execution.status = WorkflowNodeExecutionStatus.RUNNING.value
workflow_node_execution.created_by_role = workflow_run.created_by_role
workflow_node_execution.created_by = workflow_run.created_by
workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None)
with Session(db.engine, expire_on_commit=False) as session:
workflow_node_execution = WorkflowNodeExecution()
workflow_node_execution.tenant_id = workflow_run.tenant_id
workflow_node_execution.app_id = workflow_run.app_id
workflow_node_execution.workflow_id = workflow_run.workflow_id
workflow_node_execution.triggered_from = WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN.value
workflow_node_execution.workflow_run_id = workflow_run.id
workflow_node_execution.predecessor_node_id = event.predecessor_node_id
workflow_node_execution.index = event.node_run_index
workflow_node_execution.node_execution_id = event.node_execution_id
workflow_node_execution.node_id = event.node_id
workflow_node_execution.node_type = event.node_type.value
workflow_node_execution.title = event.node_data.title
workflow_node_execution.status = WorkflowNodeExecutionStatus.RUNNING.value
workflow_node_execution.created_by_role = workflow_run.created_by_role
workflow_node_execution.created_by = workflow_run.created_by
workflow_node_execution.created_at = datetime.now(timezone.utc).replace(tzinfo=None)
session.add(workflow_node_execution)
session.commit()
session.refresh(workflow_node_execution)
db.session.add(workflow_node_execution)
db.session.commit()
db.session.refresh(workflow_node_execution)
db.session.close()
self._wip_workflow_node_executions[workflow_node_execution.node_execution_id] = workflow_node_execution
return workflow_node_execution
def _handle_workflow_node_execution_success(self, event: QueueNodeSucceededEvent) -> WorkflowNodeExecution:
@@ -6,11 +6,11 @@ import numpy as np
from sqlalchemy.exc import IntegrityError
from configs import dify_config
from core.entities.embedding_type import EmbeddingInputType
from core.embedding.embedding_constant import EmbeddingInputType
from core.model_manager import ModelInstance
from core.model_runtime.entities.model_entities import ModelPropertyKey
from core.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel
from core.rag.embedding.embedding_base import Embeddings
from core.rag.datasource.entity.embedding import Embeddings
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs import helper
+57 -85
View File
@@ -1,11 +1,10 @@
import base64
from configs import dify_config
from core.file import file_repository
from core.helper import ssrf_proxy
from core.model_runtime.entities import AudioPromptMessageContent, ImagePromptMessageContent
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
from extensions.ext_database import db
from extensions.ext_storage import storage
from models import UploadFile
from . import helpers
from .enums import FileAttribute
@@ -13,7 +12,7 @@ from .models import File, FileTransferMethod, FileType
from .tool_file_parser import ToolFileParser
def get_attr(*, file: File, attr: FileAttribute):
def get_attr(*, file: "File", attr: "FileAttribute"):
match attr:
case FileAttribute.TYPE:
return file.type.value
@@ -33,7 +32,7 @@ def get_attr(*, file: File, attr: FileAttribute):
raise ValueError(f"Invalid file attribute: {attr}")
def to_prompt_message_content(f: File, /):
def to_prompt_message_content(file: "File", /):
"""
Convert a File object to an ImagePromptMessageContent object.
@@ -53,42 +52,34 @@ def to_prompt_message_content(f: File, /):
The detail level of the image prompt is determined by the file's extra_config.
If not specified, it defaults to ImagePromptMessageContent.DETAIL.LOW.
"""
match f.type:
case FileType.IMAGE:
if dify_config.MULTIMODAL_SEND_IMAGE_FORMAT == "url":
data = _to_url(f)
else:
data = _to_base64_data_string(f)
if file.type != FileType.IMAGE:
raise ValueError("Only image file can convert to prompt message content")
if f._extra_config and f._extra_config.image_config and f._extra_config.image_config.detail:
detail = f._extra_config.image_config.detail
else:
detail = ImagePromptMessageContent.DETAIL.LOW
url_or_b64_data = _get_url_or_b64_data(file=file)
if url_or_b64_data is None:
raise ValueError("Missing file data")
return ImagePromptMessageContent(data=data, detail=detail)
case FileType.AUDIO:
encoded_string = _file_to_encoded_string(f)
if f.extension is None:
raise ValueError("Missing file extension")
return AudioPromptMessageContent(data=encoded_string, format=f.extension.lstrip("."))
case _:
raise ValueError(f"file type {f.type} is not supported")
# decide the detail of image prompt message content
if file._extra_config and file._extra_config.image_config and file._extra_config.image_config.detail:
detail = file._extra_config.image_config.detail
else:
detail = ImagePromptMessageContent.DETAIL.LOW
return ImagePromptMessageContent(data=url_or_b64_data, detail=detail)
def download(f: File, /):
if f.transfer_method == FileTransferMethod.TOOL_FILE:
tool_file = file_repository.get_tool_file(session=db.session(), file=f)
return _download_file_content(tool_file.file_key)
elif f.transfer_method == FileTransferMethod.LOCAL_FILE:
upload_file = file_repository.get_upload_file(session=db.session(), file=f)
return _download_file_content(upload_file.key)
# remote file
response = ssrf_proxy.get(f.remote_url, follow_redirects=True)
response.raise_for_status()
return response.content
def download(*, upload_file_id: str, tenant_id: str):
upload_file = (
db.session.query(UploadFile).filter(UploadFile.id == upload_file_id, UploadFile.tenant_id == tenant_id).first()
)
if not upload_file:
raise ValueError("upload file not found")
return _download(upload_file.key)
def _download_file_content(path: str, /):
def _download(path: str, /):
"""
Download and return the contents of a file as bytes.
@@ -109,56 +100,37 @@ def _download_file_content(path: str, /):
return data
def _get_encoded_string(f: File, /):
match f.transfer_method:
case FileTransferMethod.REMOTE_URL:
response = ssrf_proxy.get(f.remote_url)
response.raise_for_status()
content = response.content
encoded_string = base64.b64encode(content).decode("utf-8")
return encoded_string
case FileTransferMethod.LOCAL_FILE:
upload_file = file_repository.get_upload_file(session=db.session(), file=f)
data = _download_file_content(upload_file.key)
encoded_string = base64.b64encode(data).decode("utf-8")
return encoded_string
case FileTransferMethod.TOOL_FILE:
tool_file = file_repository.get_tool_file(session=db.session(), file=f)
data = _download_file_content(tool_file.file_key)
encoded_string = base64.b64encode(data).decode("utf-8")
return encoded_string
case _:
raise ValueError(f"Unsupported transfer method: {f.transfer_method}")
def _get_base64(*, upload_file_id: str, tenant_id: str) -> str | None:
upload_file = (
db.session.query(UploadFile).filter(UploadFile.id == upload_file_id, UploadFile.tenant_id == tenant_id).first()
)
if not upload_file:
return None
data = _download(upload_file.key)
if data is None:
return None
encoded_string = base64.b64encode(data).decode("utf-8")
return f"data:{upload_file.mime_type};base64,{encoded_string}"
def _to_base64_data_string(f: File, /):
encoded_string = _get_encoded_string(f)
return f"data:{f.mime_type};base64,{encoded_string}"
def _get_url_or_b64_data(file: "File"):
if file.type == FileType.IMAGE:
if file.transfer_method == FileTransferMethod.REMOTE_URL:
return file.remote_url
elif file.transfer_method == FileTransferMethod.LOCAL_FILE:
if file.related_id is None:
raise ValueError("Missing file related_id")
def _file_to_encoded_string(f: File, /):
match f.type:
case FileType.IMAGE:
return _to_base64_data_string(f)
case FileType.AUDIO:
return _get_encoded_string(f)
case _:
raise ValueError(f"file type {f.type} is not supported")
def _to_url(f: File, /):
if f.transfer_method == FileTransferMethod.REMOTE_URL:
if f.remote_url is None:
raise ValueError("Missing file remote_url")
return f.remote_url
elif f.transfer_method == FileTransferMethod.LOCAL_FILE:
if f.related_id is None:
raise ValueError("Missing file related_id")
return helpers.get_signed_file_url(upload_file_id=f.related_id)
elif f.transfer_method == FileTransferMethod.TOOL_FILE:
# add sign url
if f.related_id is None or f.extension is None:
raise ValueError("Missing file related_id or extension")
return ToolFileParser.get_tool_file_manager().sign_file(tool_file_id=f.related_id, extension=f.extension)
else:
raise ValueError(f"Unsupported transfer method: {f.transfer_method}")
if dify_config.MULTIMODAL_SEND_IMAGE_FORMAT == "url":
return helpers.get_signed_image_url(upload_file_id=file.related_id)
return _get_base64(upload_file_id=file.related_id, tenant_id=file.tenant_id)
elif file.transfer_method == FileTransferMethod.TOOL_FILE:
# add sign url
if file.related_id is None or file.extension is None:
raise ValueError("Missing file related_id or extension")
return ToolFileParser.get_tool_file_manager().sign_file(
tool_file_id=file.related_id, extension=file.extension
)
-32
View File
@@ -1,32 +0,0 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from models import ToolFile, UploadFile
from .models import File
def get_upload_file(*, session: Session, file: File):
if file.related_id is None:
raise ValueError("Missing file related_id")
stmt = select(UploadFile).filter(
UploadFile.id == file.related_id,
UploadFile.tenant_id == file.tenant_id,
)
record = session.scalar(stmt)
if not record:
raise ValueError(f"upload file {file.related_id} not found")
return record
def get_tool_file(*, session: Session, file: File):
if file.related_id is None:
raise ValueError("Missing file related_id")
stmt = select(ToolFile).filter(
ToolFile.id == file.related_id,
ToolFile.tenant_id == file.tenant_id,
)
record = session.scalar(stmt)
if not record:
raise ValueError(f"tool file {file.related_id} not found")
return record
+13
View File
@@ -7,6 +7,19 @@ import time
from configs import dify_config
def get_signed_image_url(upload_file_id: str) -> str:
url = f"{dify_config.FILES_URL}/files/{upload_file_id}/image-preview"
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
key = dify_config.SECRET_KEY.encode()
msg = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
sign = hmac.new(key, msg.encode(), hashlib.sha256).digest()
encoded_sign = base64.urlsafe_b64encode(sign).decode()
return f"{url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
def get_signed_file_url(upload_file_id: str) -> str:
url = f"{dify_config.FILES_URL}/files/{upload_file_id}/file-preview"
+1 -1
View File
@@ -72,7 +72,7 @@ class File(BaseModel):
elif self.transfer_method == FileTransferMethod.LOCAL_FILE:
if self.related_id is None:
raise ValueError("Missing file related_id")
return helpers.get_signed_file_url(upload_file_id=self.related_id)
return helpers.get_signed_image_url(upload_file_id=self.related_id)
elif self.transfer_method == FileTransferMethod.TOOL_FILE:
assert self.related_id is not None
assert self.extension is not None
+47 -44
View File
@@ -1,9 +1,8 @@
from typing import Optional
from flask import Flask
from flask import Config, Flask
from pydantic import BaseModel
from configs import dify_config
from core.entities.provider_entities import QuotaUnit, RestrictModel
from core.model_runtime.entities.model_entities import ModelType
from models.provider import ProviderQuotaType
@@ -45,30 +44,32 @@ class HostingConfiguration:
moderation_config: HostedModerationConfig = None
def init_app(self, app: Flask) -> None:
if dify_config.EDITION != "CLOUD":
config = app.config
if config.get("EDITION") != "CLOUD":
return
self.provider_map["azure_openai"] = self.init_azure_openai()
self.provider_map["openai"] = self.init_openai()
self.provider_map["anthropic"] = self.init_anthropic()
self.provider_map["minimax"] = self.init_minimax()
self.provider_map["spark"] = self.init_spark()
self.provider_map["zhipuai"] = self.init_zhipuai()
self.provider_map["azure_openai"] = self.init_azure_openai(config)
self.provider_map["openai"] = self.init_openai(config)
self.provider_map["anthropic"] = self.init_anthropic(config)
self.provider_map["minimax"] = self.init_minimax(config)
self.provider_map["spark"] = self.init_spark(config)
self.provider_map["zhipuai"] = self.init_zhipuai(config)
self.moderation_config = self.init_moderation_config()
self.moderation_config = self.init_moderation_config(config)
@staticmethod
def init_azure_openai() -> HostingProvider:
def init_azure_openai(app_config: Config) -> HostingProvider:
quota_unit = QuotaUnit.TIMES
if dify_config.HOSTED_AZURE_OPENAI_ENABLED:
if app_config.get("HOSTED_AZURE_OPENAI_ENABLED"):
credentials = {
"openai_api_key": dify_config.HOSTED_AZURE_OPENAI_API_KEY,
"openai_api_base": dify_config.HOSTED_AZURE_OPENAI_API_BASE,
"openai_api_key": app_config.get("HOSTED_AZURE_OPENAI_API_KEY"),
"openai_api_base": app_config.get("HOSTED_AZURE_OPENAI_API_BASE"),
"base_model_name": "gpt-35-turbo",
}
quotas = []
hosted_quota_limit = dify_config.HOSTED_AZURE_OPENAI_QUOTA_LIMIT
hosted_quota_limit = int(app_config.get("HOSTED_AZURE_OPENAI_QUOTA_LIMIT", "1000"))
trial_quota = TrialHostingQuota(
quota_limit=hosted_quota_limit,
restrict_models=[
@@ -121,31 +122,31 @@ class HostingConfiguration:
quota_unit=quota_unit,
)
def init_openai(self) -> HostingProvider:
def init_openai(self, app_config: Config) -> HostingProvider:
quota_unit = QuotaUnit.CREDITS
quotas = []
if dify_config.HOSTED_OPENAI_TRIAL_ENABLED:
hosted_quota_limit = dify_config.HOSTED_OPENAI_QUOTA_LIMIT
trial_models = self.parse_restrict_models_from_env("HOSTED_OPENAI_TRIAL_MODELS")
if app_config.get("HOSTED_OPENAI_TRIAL_ENABLED"):
hosted_quota_limit = int(app_config.get("HOSTED_OPENAI_QUOTA_LIMIT", "200"))
trial_models = self.parse_restrict_models_from_env(app_config, "HOSTED_OPENAI_TRIAL_MODELS")
trial_quota = TrialHostingQuota(quota_limit=hosted_quota_limit, restrict_models=trial_models)
quotas.append(trial_quota)
if dify_config.HOSTED_OPENAI_PAID_ENABLED:
paid_models = self.parse_restrict_models_from_env("HOSTED_OPENAI_PAID_MODELS")
if app_config.get("HOSTED_OPENAI_PAID_ENABLED"):
paid_models = self.parse_restrict_models_from_env(app_config, "HOSTED_OPENAI_PAID_MODELS")
paid_quota = PaidHostingQuota(restrict_models=paid_models)
quotas.append(paid_quota)
if len(quotas) > 0:
credentials = {
"openai_api_key": dify_config.HOSTED_OPENAI_API_KEY,
"openai_api_key": app_config.get("HOSTED_OPENAI_API_KEY"),
}
if dify_config.HOSTED_OPENAI_API_BASE:
credentials["openai_api_base"] = dify_config.HOSTED_OPENAI_API_BASE
if app_config.get("HOSTED_OPENAI_API_BASE"):
credentials["openai_api_base"] = app_config.get("HOSTED_OPENAI_API_BASE")
if dify_config.HOSTED_OPENAI_API_ORGANIZATION:
credentials["openai_organization"] = dify_config.HOSTED_OPENAI_API_ORGANIZATION
if app_config.get("HOSTED_OPENAI_API_ORGANIZATION"):
credentials["openai_organization"] = app_config.get("HOSTED_OPENAI_API_ORGANIZATION")
return HostingProvider(enabled=True, credentials=credentials, quota_unit=quota_unit, quotas=quotas)
@@ -155,26 +156,26 @@ class HostingConfiguration:
)
@staticmethod
def init_anthropic() -> HostingProvider:
def init_anthropic(app_config: Config) -> HostingProvider:
quota_unit = QuotaUnit.TOKENS
quotas = []
if dify_config.HOSTED_ANTHROPIC_TRIAL_ENABLED:
hosted_quota_limit = dify_config.HOSTED_ANTHROPIC_QUOTA_LIMIT
if app_config.get("HOSTED_ANTHROPIC_TRIAL_ENABLED"):
hosted_quota_limit = int(app_config.get("HOSTED_ANTHROPIC_QUOTA_LIMIT", "0"))
trial_quota = TrialHostingQuota(quota_limit=hosted_quota_limit)
quotas.append(trial_quota)
if dify_config.HOSTED_ANTHROPIC_PAID_ENABLED:
if app_config.get("HOSTED_ANTHROPIC_PAID_ENABLED"):
paid_quota = PaidHostingQuota()
quotas.append(paid_quota)
if len(quotas) > 0:
credentials = {
"anthropic_api_key": dify_config.HOSTED_ANTHROPIC_API_KEY,
"anthropic_api_key": app_config.get("HOSTED_ANTHROPIC_API_KEY"),
}
if dify_config.HOSTED_ANTHROPIC_API_BASE:
credentials["anthropic_api_url"] = dify_config.HOSTED_ANTHROPIC_API_BASE
if app_config.get("HOSTED_ANTHROPIC_API_BASE"):
credentials["anthropic_api_url"] = app_config.get("HOSTED_ANTHROPIC_API_BASE")
return HostingProvider(enabled=True, credentials=credentials, quota_unit=quota_unit, quotas=quotas)
@@ -184,9 +185,9 @@ class HostingConfiguration:
)
@staticmethod
def init_minimax() -> HostingProvider:
def init_minimax(app_config: Config) -> HostingProvider:
quota_unit = QuotaUnit.TOKENS
if dify_config.HOSTED_MINIMAX_ENABLED:
if app_config.get("HOSTED_MINIMAX_ENABLED"):
quotas = [FreeHostingQuota()]
return HostingProvider(
@@ -202,9 +203,9 @@ class HostingConfiguration:
)
@staticmethod
def init_spark() -> HostingProvider:
def init_spark(app_config: Config) -> HostingProvider:
quota_unit = QuotaUnit.TOKENS
if dify_config.HOSTED_SPARK_ENABLED:
if app_config.get("HOSTED_SPARK_ENABLED"):
quotas = [FreeHostingQuota()]
return HostingProvider(
@@ -220,9 +221,9 @@ class HostingConfiguration:
)
@staticmethod
def init_zhipuai() -> HostingProvider:
def init_zhipuai(app_config: Config) -> HostingProvider:
quota_unit = QuotaUnit.TOKENS
if dify_config.HOSTED_ZHIPUAI_ENABLED:
if app_config.get("HOSTED_ZHIPUAI_ENABLED"):
quotas = [FreeHostingQuota()]
return HostingProvider(
@@ -238,15 +239,17 @@ class HostingConfiguration:
)
@staticmethod
def init_moderation_config() -> HostedModerationConfig:
if dify_config.HOSTED_MODERATION_ENABLED and dify_config.HOSTED_MODERATION_PROVIDERS:
return HostedModerationConfig(enabled=True, providers=dify_config.HOSTED_MODERATION_PROVIDERS.split(","))
def init_moderation_config(app_config: Config) -> HostedModerationConfig:
if app_config.get("HOSTED_MODERATION_ENABLED") and app_config.get("HOSTED_MODERATION_PROVIDERS"):
return HostedModerationConfig(
enabled=True, providers=app_config.get("HOSTED_MODERATION_PROVIDERS").split(",")
)
return HostedModerationConfig(enabled=False)
@staticmethod
def parse_restrict_models_from_env(env_var: str) -> list[RestrictModel]:
models_str = dify_config.model_dump().get(env_var)
def parse_restrict_models_from_env(app_config: Config, env_var: str) -> list[RestrictModel]:
models_str = app_config.get(env_var)
models_list = models_str.split(",") if models_str else []
return [
RestrictModel(model=model_name.strip(), model_type=ModelType.LLM)
-50
View File
@@ -8,8 +8,6 @@ from core.llm_generator.output_parser.suggested_questions_after_answer import Su
from core.llm_generator.prompts import (
CONVERSATION_TITLE_PROMPT,
GENERATOR_QA_PROMPT,
JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE,
PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE,
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
)
from core.model_manager import ModelManager
@@ -241,54 +239,6 @@ class LLMGenerator:
return rule_config
@classmethod
def generate_code(
cls,
tenant_id: str,
instruction: str,
model_config: dict,
code_language: str = "javascript",
max_tokens: int = 1000,
) -> dict:
if code_language == "python":
prompt_template = PromptTemplateParser(PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE)
else:
prompt_template = PromptTemplateParser(JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE)
prompt = prompt_template.format(
inputs={
"INSTRUCTION": instruction,
"CODE_LANGUAGE": code_language,
},
remove_template_variables=False,
)
model_manager = ModelManager()
model_instance = model_manager.get_model_instance(
tenant_id=tenant_id,
model_type=ModelType.LLM,
provider=model_config.get("provider") if model_config else None,
model=model_config.get("name") if model_config else None,
)
prompt_messages = [UserPromptMessage(content=prompt)]
model_parameters = {"max_tokens": max_tokens, "temperature": 0.01}
try:
response = model_instance.invoke_llm(
prompt_messages=prompt_messages, model_parameters=model_parameters, stream=False
)
generated_code = response.message.content
return {"code": generated_code, "language": code_language, "error": ""}
except InvokeError as e:
error = str(e)
return {"code": "", "language": code_language, "error": f"Failed to generate code. Error: {error}"}
except Exception as e:
logging.exception(e)
return {"code": "", "language": code_language, "error": f"An unexpected error occurred: {str(e)}"}
@classmethod
def generate_qa_document(cls, tenant_id: str, query, document_language: str):
prompt = GENERATOR_QA_PROMPT.format(language=document_language)
-67
View File
@@ -61,73 +61,6 @@ User Input: yo, 你今天咋样?
User Input:
""" # noqa: E501
PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE = (
"You are an expert programmer. Generate code based on the following instructions:\n\n"
"Instructions: {{INSTRUCTION}}\n\n"
"Write the code in {{CODE_LANGUAGE}}.\n\n"
"Please ensure that you meet the following requirements:\n"
"1. Define a function named 'main'.\n"
"2. The 'main' function must return a dictionary (dict).\n"
"3. You may modify the arguments of the 'main' function, but include appropriate type hints.\n"
"4. The returned dictionary should contain at least one key-value pair.\n\n"
"5. You may ONLY use the following libraries in your code: \n"
"- json\n"
"- datetime\n"
"- math\n"
"- random\n"
"- re\n"
"- string\n"
"- sys\n"
"- time\n"
"- traceback\n"
"- uuid\n"
"- os\n"
"- base64\n"
"- hashlib\n"
"- hmac\n"
"- binascii\n"
"- collections\n"
"- functools\n"
"- operator\n"
"- itertools\n\n"
"Example:\n"
"def main(arg1: str, arg2: int) -> dict:\n"
" return {\n"
' "result": arg1 * arg2,\n'
" }\n\n"
"IMPORTANT:\n"
"- Provide ONLY the code without any additional explanations, comments, or markdown formatting.\n"
"- DO NOT use markdown code blocks (``` or ``` python). Return the raw code directly.\n"
"- The code should start immediately after this instruction, without any preceding newlines or spaces.\n"
"- The code should be complete, functional, and follow best practices for {{CODE_LANGUAGE}}.\n\n"
"- Always use the format return {'result': ...} for the output.\n\n"
"Generated Code:\n"
)
JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE = (
"You are an expert programmer. Generate code based on the following instructions:\n\n"
"Instructions: {{INSTRUCTION}}\n\n"
"Write the code in {{CODE_LANGUAGE}}.\n\n"
"Please ensure that you meet the following requirements:\n"
"1. Define a function named 'main'.\n"
"2. The 'main' function must return an object.\n"
"3. You may modify the arguments of the 'main' function, but include appropriate JSDoc annotations.\n"
"4. The returned object should contain at least one key-value pair.\n\n"
"5. The returned object should always be in the format: {result: ...}\n\n"
"Example:\n"
"function main(arg1, arg2) {\n"
" return {\n"
" result: arg1 * arg2\n"
" };\n"
"}\n\n"
"IMPORTANT:\n"
"- Provide ONLY the code without any additional explanations, comments, or markdown formatting.\n"
"- DO NOT use markdown code blocks (``` or ``` javascript). Return the raw code directly.\n"
"- The code should start immediately after this instruction, without any preceding newlines or spaces.\n"
"- The code should be complete, functional, and follow best practices for {{CODE_LANGUAGE}}.\n\n"
"Generated Code:\n"
)
SUGGESTED_QUESTIONS_AFTER_ANSWER_INSTRUCTION_PROMPT = (
"Please help me predict the three most likely questions that human would ask, "
"and keeping each question under 20 characters.\n"
+5 -5
View File
@@ -1,8 +1,7 @@
from typing import Optional
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.file import file_manager
from core.file.models import FileType
from core.file import FileType, file_manager
from core.model_manager import ModelInstance
from core.model_runtime.entities import (
AssistantPromptMessage,
@@ -99,9 +98,10 @@ class TokenBufferMemory:
prompt_message_contents: list[PromptMessageContent] = []
prompt_message_contents.append(TextPromptMessageContent(data=message.query))
for file_obj in file_objs:
if file_obj.type in {FileType.IMAGE, FileType.AUDIO}:
prompt_message = file_manager.to_prompt_message_content(file_obj)
prompt_message_contents.append(prompt_message)
if file_obj.type != FileType.IMAGE:
continue
prompt_message = file_manager.to_prompt_message_content(file_obj)
prompt_message_contents.append(prompt_message)
prompt_messages.append(UserPromptMessage(content=prompt_message_contents))
else:
+1 -1
View File
@@ -3,7 +3,7 @@ import os
from collections.abc import Callable, Generator, Iterable, Sequence
from typing import IO, Any, Optional, Union, cast
from core.entities.embedding_type import EmbeddingInputType
from core.embedding.embedding_constant import EmbeddingInputType
from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle
from core.entities.provider_entities import ModelLoadBalancingConfiguration
from core.errors.error import ProviderTokenNotInitError
@@ -218,7 +218,7 @@ For instance, Xinference supports `max_tokens`, `temperature`, and `top_p` param
However, some vendors may support different parameters for different models. For example, the `OpenLLM` vendor supports `top_k`, but not all models provided by this vendor support `top_k`. Let's say model A supports `top_k` but model B does not. In such cases, we need to dynamically generate the model parameter schema, as illustrated below:
```python
def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
"""
used to define customizable model schema
"""
@@ -205,7 +205,7 @@ provider_credential_schema:
但是有的供应商根据不同的模型支持不同的参数,如供应商`OpenLLM`支持`top_k`,但是并不是这个供应商提供的所有模型都支持`top_k`,我们这里举例A模型支持`top_k`B模型不支持`top_k`,那么我们需要在这里动态生成模型参数的Schema,如下所示:
```python
def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
"""
used to define customizable model schema
"""
@@ -1,7 +1,6 @@
from .llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
from .message_entities import (
AssistantPromptMessage,
AudioPromptMessageContent,
ImagePromptMessageContent,
PromptMessage,
PromptMessageContent,
@@ -34,5 +33,4 @@ __all__ = [
"LLMResult",
"LLMResultChunk",
"LLMResultChunkDelta",
"AudioPromptMessageContent",
]
@@ -105,7 +105,6 @@ class LLMResult(BaseModel):
Model class for llm result.
"""
id: Optional[str] = None
model: str
prompt_messages: list[PromptMessage]
message: AssistantPromptMessage
@@ -2,7 +2,7 @@ from abc import ABC
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field, field_validator
from pydantic import BaseModel, field_validator
class PromptMessageRole(Enum):
@@ -55,7 +55,6 @@ class PromptMessageContentType(Enum):
TEXT = "text"
IMAGE = "image"
AUDIO = "audio"
class PromptMessageContent(BaseModel):
@@ -75,12 +74,6 @@ class TextPromptMessageContent(PromptMessageContent):
type: PromptMessageContentType = PromptMessageContentType.TEXT
class AudioPromptMessageContent(PromptMessageContent):
type: PromptMessageContentType = PromptMessageContentType.AUDIO
data: str = Field(..., description="Base64 encoded audio data")
format: str = Field(..., description="Audio format")
class ImagePromptMessageContent(PromptMessageContent):
"""
Model class for image prompt message content.
@@ -107,16 +107,7 @@ class LargeLanguageModel(AIModel):
callbacks=callbacks,
)
else:
result = self._invoke(
model=model,
credentials=credentials,
prompt_messages=prompt_messages,
model_parameters=model_parameters,
tools=tools,
stop=stop,
stream=stream,
user=user,
)
result = self._invoke(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user)
except Exception as e:
self._trigger_invoke_error_callbacks(
model=model,
@@ -4,7 +4,7 @@ from typing import Optional
from pydantic import ConfigDict
from core.entities.embedding_type import EmbeddingInputType
from core.embedding.embedding_constant import EmbeddingInputType
from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
from core.model_runtime.entities.text_embedding_entities import TextEmbeddingResult
from core.model_runtime.model_providers.__base.ai_model import AIModel
@@ -1,4 +1,3 @@
- claude-3-5-sonnet-20241022
- claude-3-5-sonnet-20240620
- claude-3-haiku-20240307
- claude-3-opus-20240229
@@ -1,39 +0,0 @@
model: claude-3-5-sonnet-20241022
label:
en_US: claude-3-5-sonnet-20241022
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_tokens
use_template: max_tokens
required: true
default: 8192
min: 1
max: 8192
- name: response_format
use_template: response_format
pricing:
input: '3.00'
output: '15.00'
unit: '0.000001'
currency: USD
@@ -294,7 +294,7 @@ class AzureAIStudioLargeLanguageModel(LargeLanguageModel):
],
}
def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
"""
Used to define customizable model schema
"""
@@ -148,7 +148,7 @@ class AzureRerankModel(RerankModel):
InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError, json.JSONDecodeError],
}
def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
"""
used to define customizable model schema
"""
@@ -1098,14 +1098,6 @@ LLM_BASE_MODELS = [
ModelPropertyKey.CONTEXT_SIZE: 128000,
},
parameter_rules=[
ParameterRule(
name="temperature",
**PARAMETER_RULE_TEMPLATE[DefaultParameterName.TEMPERATURE],
),
ParameterRule(
name="top_p",
**PARAMETER_RULE_TEMPLATE[DefaultParameterName.TOP_P],
),
ParameterRule(
name="response_format",
label=I18nObject(zh_Hans="回复格式", en_US="response_format"),
@@ -1143,14 +1135,6 @@ LLM_BASE_MODELS = [
ModelPropertyKey.CONTEXT_SIZE: 128000,
},
parameter_rules=[
ParameterRule(
name="temperature",
**PARAMETER_RULE_TEMPLATE[DefaultParameterName.TEMPERATURE],
),
ParameterRule(
name="top_p",
**PARAMETER_RULE_TEMPLATE[DefaultParameterName.TOP_P],
),
ParameterRule(
name="response_format",
label=I18nObject(zh_Hans="回复格式", en_US="response_format"),
@@ -53,9 +53,6 @@ model_credential_schema:
type: select
required: true
options:
- label:
en_US: 2024-10-01-preview
value: 2024-10-01-preview
- label:
en_US: 2024-09-01-preview
value: 2024-09-01-preview
@@ -45,7 +45,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
stream: bool = True,
user: Optional[str] = None,
) -> Union[LLMResult, Generator]:
base_model_name = self._get_base_model_name(credentials)
base_model_name = credentials.get("base_model_name")
if not base_model_name:
raise ValueError("Base Model Name is required")
ai_model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model)
if ai_model_entity and ai_model_entity.entity.model_properties.get(ModelPropertyKey.MODE) == LLMMode.CHAT.value:
@@ -79,7 +81,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
prompt_messages: list[PromptMessage],
tools: Optional[list[PromptMessageTool]] = None,
) -> int:
base_model_name = self._get_base_model_name(credentials)
base_model_name = credentials.get("base_model_name")
if not base_model_name:
raise ValueError("Base Model Name is required")
model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model)
if not model_entity:
raise ValueError(f"Base Model Name {base_model_name} is invalid")
@@ -104,7 +108,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
if "base_model_name" not in credentials:
raise CredentialsValidateFailedError("Base Model Name is required")
base_model_name = self._get_base_model_name(credentials)
base_model_name = credentials.get("base_model_name")
if not base_model_name:
raise CredentialsValidateFailedError("Base Model Name is required")
ai_model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model)
if not ai_model_entity:
@@ -113,15 +119,7 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
try:
client = AzureOpenAI(**self._to_credential_kwargs(credentials))
if model.startswith("o1"):
client.chat.completions.create(
messages=[{"role": "user", "content": "ping"}],
model=model,
temperature=1,
max_completion_tokens=20,
stream=False,
)
elif ai_model_entity.entity.model_properties.get(ModelPropertyKey.MODE) == LLMMode.CHAT.value:
if ai_model_entity.entity.model_properties.get(ModelPropertyKey.MODE) == LLMMode.CHAT.value:
# chat model
client.chat.completions.create(
messages=[{"role": "user", "content": "ping"}],
@@ -143,7 +141,9 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
raise CredentialsValidateFailedError(str(ex))
def get_customizable_model_schema(self, model: str, credentials: dict) -> Optional[AIModelEntity]:
base_model_name = self._get_base_model_name(credentials)
base_model_name = credentials.get("base_model_name")
if not base_model_name:
raise ValueError("Base Model Name is required")
ai_model_entity = self._get_ai_model_entity(base_model_name=base_model_name, model=model)
return ai_model_entity.entity if ai_model_entity else None
@@ -300,6 +300,11 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
if tools:
extra_model_kwargs["tools"] = [helper.dump_model(PromptMessageFunction(function=tool)) for tool in tools]
# extra_model_kwargs['functions'] = [{
# "name": tool.name,
# "description": tool.description,
# "parameters": tool.parameters
# } for tool in tools]
if stop:
extra_model_kwargs["stop"] = stop
@@ -756,9 +761,3 @@ class AzureOpenAILargeLanguageModel(_CommonAzureOpenAI, LargeLanguageModel):
ai_model_entity_copy.entity.label.en_US = model
ai_model_entity_copy.entity.label.zh_Hans = model
return ai_model_entity_copy
def _get_base_model_name(self, credentials: dict) -> str:
base_model_name = credentials.get("base_model_name")
if not base_model_name:
raise ValueError("Base Model Name is required")
return base_model_name
@@ -7,7 +7,7 @@ import numpy as np
import tiktoken
from openai import AzureOpenAI
from core.entities.embedding_type import EmbeddingInputType
from core.embedding.embedding_constant import EmbeddingInputType
from core.model_runtime.entities.model_entities import AIModelEntity, PriceType
from core.model_runtime.entities.text_embedding_entities import EmbeddingUsage, TextEmbeddingResult
from core.model_runtime.errors.validate import CredentialsValidateFailedError
@@ -4,7 +4,7 @@ from typing import Optional
from requests import post
from core.entities.embedding_type import EmbeddingInputType
from core.embedding.embedding_constant import EmbeddingInputType
from core.model_runtime.entities.model_entities import PriceType
from core.model_runtime.entities.text_embedding_entities import EmbeddingUsage, TextEmbeddingResult
from core.model_runtime.errors.invoke import (
@@ -1,60 +0,0 @@
model: anthropic.claude-3-5-sonnet-20241022-v2:0
label:
en_US: Claude 3.5 Sonnet V2
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 200000
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
parameter_rules:
- name: max_tokens
use_template: max_tokens
required: true
type: int
default: 4096
min: 1
max: 4096
help:
zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
- name: temperature
use_template: temperature
required: false
type: float
default: 1
min: 0.0
max: 1.0
help:
zh_Hans: 生成内容的随机性。
en_US: The amount of randomness injected into the response.
- name: top_p
required: false
type: float
default: 0.999
min: 0.000
max: 1.000
help:
zh_Hans: 在核采样中,Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p,但不能同时更改两者。
en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
- name: top_k
required: false
type: int
default: 0
min: 0
# tip docs from aws has error, max value is 500
max: 500
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
- name: response_format
use_template: response_format
pricing:
input: '0.003'
output: '0.015'
unit: '0.001'
currency: USD
@@ -52,8 +52,6 @@ parameter_rules:
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
- name: response_format
use_template: response_format
pricing:
input: '0.00025'
output: '0.00125'
@@ -51,8 +51,6 @@ parameter_rules:
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
- name: response_format
use_template: response_format
pricing:
input: '0.003'
output: '0.015'

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