Compare commits

..

156 Commits
v2.0.2 ... main

Author SHA1 Message Date
wxg0103 847755b1c2 feat: add response body schemas for various API endpoints 2025-08-19 15:15:46 +08:00
wxg0103 b57455d0ee refactor: expand permissions for application access token
--bug=1060032 --user=王孝刚 【桂物智慧】 api 调用客户端修改对话摘要接口,报错403,没有访问权限 https://www.tapd.cn/57709429/s/1757188
2025-08-19 11:06:36 +08:00
maninhill 2a257edff9
chore: Update README.md (#3587)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-07-14 17:49:32 +08:00
maninhill d47699331c
chore: Update README_CN.md (#3585) 2025-07-14 17:28:34 +08:00
maninhill 90c64d77dd
chore: Update README.md (#3584) 2025-07-14 17:27:02 +08:00
liqiang-fit2cloud e1ada3ffe2
Update build-and-push.yml
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-07-10 17:37:21 +08:00
wangdan-fit2cloud b62c79fda6 fix: text bug
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-07-10 11:37:44 +08:00
wangdan-fit2cloud 3d9e7dd4b1 feat: optimization 2025-07-10 11:37:44 +08:00
CaptainB 8ff15865a7 fix: remove redundant import of pickle in function_code.py
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-07-09 15:02:06 +08:00
wanghe-fit2cloud 48899d55d1 docs: Add some badges
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-07-07 13:15:57 +08:00
CaptainB 1cc4107bfe chore: remove ignore rule for pymupdf dependency in dependabot configuration
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-07-04 11:38:01 +08:00
CaptainB b13cd03706 fix: prevent usage of 'stdio' in MCP server configuration
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-06-24 11:46:53 +08:00
liuchang_cloud 69f024492b
fix(documet):upgrade document updatetime when upgrade document_char_length (#3211)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
fixed #2942
2025-06-20 10:17:25 +08:00
Mr.zhao a9c46cd7e0
fix: Reorganize the model cache (#3315)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-06-19 18:01:45 +08:00
CaptainB a9e9f5b085 chore: add ignore rule for pymupdf dependency in dependabot configuration
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-06-16 13:07:33 +08:00
wxg0103 e12b1fe14e refactor: gemini
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-06-05 09:11:06 +08:00
shaohuzhang1 7ce66a7bf3
perf: Function library execution (#3175)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-05-30 18:56:01 +08:00
zhangshaohu decd3395db build: Sensitive directory permissions 2025-05-22 21:59:06 +08:00
shaohuzhang1 9d7a383348
fix: The dropdown data of subsequent nodes in the form cannot be displayed back (#3131)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-05-22 14:36:10 +08:00
shaohuzhang1 8f7d91798b
fix: The dropdown data of subsequent nodes in the form cannot be displayed back (#3129) 2025-05-22 10:54:25 +08:00
wxg0103 81a3af2c8b refactor: openai token 2025-05-22 09:11:12 +08:00
wxg0103 2ec0d22b14 refactor: openai token
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-21 18:50:49 +08:00
wxg0103 27a77dc657 refactor: openai token 2025-05-21 18:33:56 +08:00
shaohuzhang1 187e9c1e4e
build: Sensitive directory permissions (#3127) 2025-05-21 17:39:09 +08:00
shaohuzhang1 e5bab10824
build: Sensitive directory permissions (#3126) 2025-05-21 16:43:46 +08:00
shaohuzhang1 347f4a0b03
fix: The default suffix for workflow file upload nodes is set to uppercase DOC and PPT (#3125)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-21 13:25:46 +08:00
shaohuzhang1 289ebf42a6
fix: Verification code is not case sensitive (#3121) 2025-05-21 11:56:58 +08:00
shaohuzhang1 71fdce08d7
build: Sensitive directory permissions (#3120) 2025-05-21 11:45:38 +08:00
shaohuzhang1 adc5af9cef
fix: Upload a file in the application dialogue, with a file name containing HTML character entities. After uploading, the file_id of the file is empty #3070 (#3119) 2025-05-21 11:40:48 +08:00
wangdan-fit2cloud ce2ab322f6 fix: modification of copywriting 2025-05-21 11:15:20 +08:00
shaohuzhang1 a7e31b94c7
fix: AI conversation jumps to 404 (#3118) 2025-05-21 10:58:00 +08:00
wangdan-fit2cloud 8498687794 fix: captcha style 2025-05-21 10:48:27 +08:00
wxg0103 190ca3e198 refactor: login captcha 2025-05-21 10:34:01 +08:00
shaohuzhang1 c1ddec1a61
feat: Login and add graphic captcha (#3117)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-20 20:16:36 +08:00
wangdan-fit2cloud 1ba8077e95 fix: optimize markdown style 2025-05-20 19:34:48 +08:00
wxg0103 9a42bd2302 refactor: add client_id 2025-05-20 18:49:22 +08:00
shaohuzhang1 35b662a52d
perf: Optimize document extraction for complex table files (#3116)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-20 13:44:20 +08:00
wangdan-fit2cloud a4faf52261
fix: Parameter setting page optimization (#3115) 2025-05-20 11:46:16 +08:00
wangdan-fit2cloud a30316d87a
fix: breadcrumb issue (#3111)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-19 18:18:36 +08:00
wangdan-fit2cloud a4d10cbe3b
fix: Application of multi-file style issues and modification of copywriting (#3110) 2025-05-19 16:39:45 +08:00
wangdan-fit2cloud ceccf9f1fa
fix: Line break truncated characters in English(#3103) 2025-05-19 16:38:26 +08:00
shaohuzhang1 3964db20dc
fix: Advanced orchestration defines global variables and assigns them through assignment nodes. If there is a form collection node before the reference node, the assignment node becomes invalid and the initial variable value is still referenced #3082 (#3108) 2025-05-19 14:36:40 +08:00
shaohuzhang1 57ada0708f
fix: When the function parameter is of non string type, if the parameter is empty, the function will run with an error #3053 (#3107) 2025-05-19 14:13:49 +08:00
shaohuzhang1 a1a92a833a
fix: Excel export prohibits inputting external links or formulas (#3106) 2025-05-19 14:04:16 +08:00
shaohuzhang1 5e0d8048f9
fix: Excel export prohibits inputting external links or formulas (#3105) 2025-05-19 13:51:13 +08:00
shaohuzhang1 8903b35aec
fix: dialogue log export error (#3100)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-05-16 13:22:10 +08:00
shaohuzhang1 fa4f7e99fd
perf: Refine the Model Manager code (#3099) 2025-05-16 12:37:52 +08:00
shaohuzhang1 b0630b3ddd
perf: Refine the Model Manager code (#3098) 2025-05-16 12:09:39 +08:00
shaohuzhang1 b2bf69740c
perf: Refine the Model Manager code (#3094)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-15 22:47:46 +08:00
shaohuzhang1 8cf66b9eca
perf: Refine the Model Manager code (#3093) 2025-05-15 22:38:20 +08:00
shaohuzhang1 1db8577ca6
perf: Refine the Model Manager code (#3091) 2025-05-15 21:53:10 +08:00
shaohuzhang1 949e4dea9e
perf: Refine the Model Manager code (#3089) 2025-05-15 21:22:41 +08:00
CaptainB c12988bc8a fix: update MCP server configuration text to include STREAMABLE_HTTP support
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
--bug=1056115 --user=刘瑞斌 【应用编排】编辑mcp节点的config后,提问会报错 https://www.tapd.cn/57709429/s/1699333
--bug=1056092 --user=刘瑞斌 【应用编排】MCP的支持文案要加上支持 Streamable HTTP https://www.tapd.cn/57709429/s/1699357
2025-05-15 16:48:25 +08:00
shaohuzhang1 2728453f6c
fix: The workflow includes a form collection node. After the normal Q&A on the demonstration page is completed, refreshing the page will display the output of the nodes before the form collection node (#3081) 2025-05-14 10:06:07 +08:00
shaohuzhang1 ccf43bbcd9
feat: add pool options recycle (#3080) 2025-05-13 16:56:06 +08:00
CaptainB 8d8de53e38 chore: update langchain-mcp-adapters and mcp package versions
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-05-09 16:28:50 +08:00
CaptainB 7faf556771 refactor: remove unused initialization parameters from function library update
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
--bug=1055178 --user=刘瑞斌 【函数】启用的函数,禁用后再启用,函数的部分启用参数会被置为X https://www.tapd.cn/57709429/s/1694852
2025-05-08 13:05:34 +08:00
wxg0103 76ec8ad6f6 refactor: regolo icon
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-05-07 10:51:43 +08:00
Daniele Scasciafratte 0cf05a76a0
feat(provider): added regolo.ai (#3041) 2025-05-07 10:12:47 +08:00
CaptainB 357edbfbe0 ci: remove redundant pip package ecosystem configuration from dependabot
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-04-30 11:21:23 +08:00
CaptainB 0609a9afc8 ci: add pip package ecosystem support to dependabot configuration 2025-04-30 11:19:16 +08:00
shaohuzhang1 96e59a018f
fix: The simple application demonstration cannot be opened (#3028)
Some checks failed
Typos Check / Spell Check with Typos (push) Waiting to run
sync2gitee / repo-sync (push) Has been cancelled
2025-04-29 22:12:41 +08:00
shaohuzhang1 79b2de8893
feat: Ali Bailian supplier model list adds qwen3 model (#3026) 2025-04-29 19:08:17 +08:00
shaohuzhang1 0c7cca035e
fix: Cannot copy applications with opening statement characters exceeding 4096 (#3022)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-29 15:26:13 +08:00
shaohuzhang1 00a3e5ddc3
fix: The similarity of knowledge base mixed retrieval should be 0-2 (#3021) 2025-04-29 15:18:19 +08:00
shaohuzhang1 a6533c0db7
feat: Support Alibaba Bailian supplier qwen3 model (#3020) 2025-04-29 14:57:41 +08:00
shaohuzhang1 704077d066
fix: Workflow application node parameter saving cannot be reflected back (#3019) 2025-04-29 14:35:36 +08:00
shaohuzhang1 59ee0c1270
fix: During the conversation, there is sensitive data in the application profile obtained by the application (#3018) 2025-04-29 13:40:15 +08:00
wangdan-fit2cloud b37cc3ba1c
perf: The style of MCP nodes when there is no data (#3017) 2025-04-29 11:34:27 +08:00
yuxiaobin dfe6d0a91b
add support for docx, xlsx (#3014) 2025-04-29 11:25:12 +08:00
shaohuzhang1 5813eedd4f
perf: Slow dialogue log query (#3016) 2025-04-29 11:24:22 +08:00
liqiang-fit2cloud 363150380d build: update build-and-push.yml.
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-28 14:55:26 +08:00
liqiang-fit2cloud 47f9c04664 build(deps): update pyproject.toml. 2025-04-25 15:55:01 +08:00
dependabot[bot] 17cd88edda build(deps): update langchain-anthropic requirement
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
Updates the requirements on [langchain-anthropic](https://github.com/langchain-ai/langchain) to permit the latest version.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-anthropic==0.3.10...langchain-anthropic==0.3.12)

---
updated-dependencies:
- dependency-name: langchain-anthropic
  dependency-version: 0.3.12
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-25 12:04:22 +08:00
dependabot[bot] d33b620dc8 build(deps): update langchain-ollama requirement from 0.3.1 to 0.3.2
Updates the requirements on [langchain-ollama](https://github.com/langchain-ai/langchain) to permit the latest version.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-ollama==0.3.1...langchain-ollama==0.3.2)

---
updated-dependencies:
- dependency-name: langchain-ollama
  dependency-version: 0.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-25 12:02:46 +08:00
CaptainB b95cb20704 ci: add pip package ecosystem support to dependabot configuration 2025-04-25 11:44:15 +08:00
shaohuzhang1 4ff1944b60
fix: User input parameter occupancy issue (#2976)
Some checks failed
Typos Check / Spell Check with Typos (push) Waiting to run
sync2gitee / repo-sync (push) Has been cancelled
2025-04-24 17:39:44 +08:00
wangdan-fit2cloud 1c6b0f8a86 fix: api input bug 2025-04-24 17:32:22 +08:00
shaohuzhang1 d85801fe58
build: ui build error (#2975) 2025-04-24 16:39:01 +08:00
shaohuzhang1 e79e7d505d
feat: Add local model worker parameters (#2974) 2025-04-24 16:22:01 +08:00
shaohuzhang1 33b1cd65b0
fix: Ollama maximum output token field (#2973) 2025-04-24 16:16:36 +08:00
CaptainB 8d503c8bf8 fix: update post_cell function to handle different newline characters in cell values
--bug=1054683 --user=刘瑞斌 【github#2831】知识库上传excel、应用编排文档内容提取节点中上传excel,单元格中有换行,导入后没有在一个单元格里显示 https://www.tapd.cn/57709429/s/1690232
2025-04-24 16:05:09 +08:00
wangdan-fit2cloud df7f922013 fix: fix api input bug 2025-04-24 15:27:41 +08:00
liqiang-fit2cloud a0541203e4 deps: lock versions.
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-24 13:38:59 +08:00
liqiang-fit2cloud fb64731cd8 deps: lock versions. 2025-04-24 13:31:27 +08:00
shaohuzhang1 d960a18711
fix: Thinking about process labels during simple application debugging does not take effect (#2971) 2025-04-24 13:11:28 +08:00
liqiang-fit2cloud ee35cc21e9 build: update build-and-push.yml. 2025-04-24 12:54:36 +08:00
liqiang-fit2cloud e4a60daa17 deps: lock versions. 2025-04-24 12:45:49 +08:00
liqiang-fit2cloud 7bcb770ee5 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-24 12:33:38 +08:00
wangdan-fit2cloud b5b09dc8b4
fix: Fixed interface parameter transmission not displaying during debugging (#2969) 2025-04-24 11:24:44 +08:00
shaohuzhang1 b1f6092620
fix: The tag based thinking process does not output (#2968) 2025-04-24 10:57:10 +08:00
wxg0103 c8441cfd73 refactor: loading
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-23 16:04:57 +08:00
wangdan-fit2cloud 7e4b147576 fix: upload style 2025-04-23 15:35:54 +08:00
CaptainB 9cd082089a feat: update file extension display to uppercase in upload settings 2025-04-23 15:34:07 +08:00
wangdan-fit2cloud d4541e23f9 fix: Fix the issue of uploading and deleting other files 2025-04-23 15:02:05 +08:00
wxg0103 5e7e91cced refactor: loading 2025-04-23 13:53:04 +08:00
wangdan-fit2cloud d9787bb548 fix: style bug 2025-04-23 10:53:38 +08:00
CaptainB 131b5b3bbe feat: add existing file extensions warning and improve file type display in upload settings
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-23 10:48:29 +08:00
wxg0103 e58f95832b fix: aws computer tokens error
--bug=1055094 --user=王孝刚 【应用】aws模型在应用编排中使用时,回答内容会带一串报错信息,并且消耗tokens显示为0 https://www.tapd.cn/57709429/s/1689448
2025-04-23 10:41:18 +08:00
wxg0103 f24337d5f3 fix: The forgot password page displays an error
--bug=1055073 --user=王孝刚 【长城开发科技】自定平台登录设置在点击忘记密码时还是默认提示 https://www.tapd.cn/57709429/s/1689108
2025-04-23 10:05:46 +08:00
shaohuzhang1 d6f1d25b59
fix: Change the max_token of the Qianfan large model to max_output_token (#2955)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-22 18:10:49 +08:00
shaohuzhang1 0ec198fa43
fix: Lost historical conversation records (#2954)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-22 17:58:28 +08:00
wangdan-fit2cloud 77e96624ee
fix: upload file style (#2953) 2025-04-22 17:31:14 +08:00
CaptainB 5e02809db2 feat: add support for handling and displaying additional file types in the chat interface 2025-04-22 16:42:08 +08:00
shaohuzhang1 6fe001fcf8
fix: collection form function cannot be used normally and will be stuck in the answer #2857 (#2952) 2025-04-22 16:24:39 +08:00
CaptainB f646102262 fix: update file upload settings to allow dynamic video extensions and improve input handling for other file types 2025-04-22 16:20:01 +08:00
shaohuzhang1 b97f4e16ba
dependencies: Upgrade Django to 4.2.20 (#2950) 2025-04-22 14:58:31 +08:00
shaohuzhang1 0c14306889
fix: Docx segmented font title recognition (#2949) 2025-04-22 14:51:45 +08:00
wxg0103 f1d043f67b fix: The forgot password page displays an error
--bug=1055073 --user=王孝刚 【长城开发科技】自定平台登录设置在点击忘记密码时还是默认提示 https://www.tapd.cn/57709429/s/1689108
2025-04-22 14:19:43 +08:00
shaohuzhang1 c39a6e81d7
fix: When there is an empty string in the context, the conversation reports an error (#2947) 2025-04-22 13:59:53 +08:00
shaohuzhang1 9c56c7e198
fix: When automatic sending is not selected, the text conversion should be displayed in the position of the question input box (#2946) 2025-04-22 11:54:56 +08:00
shaohuzhang1 6484fef8ea
fix: Edit advanced orchestration applications, add application nodes, and display that some applications are unavailable after being added (#2945) 2025-04-22 11:25:47 +08:00
wangdan-fit2cloud 8a194481ac
feat: Adjust file upload and add other file function styles (#2944) 2025-04-22 11:12:42 +08:00
shaohuzhang1 072b817792
fix: OpenAI interface call for session, passing asker does not take effect (#2943) 2025-04-22 11:06:50 +08:00
CaptainB d32f7d36a6 feat: add support for uploading other file types and extend file upload settings
--story=1018411 --user=刘瑞斌 【工作流应用】文件上传可以支持其它文件类型 https://www.tapd.cn/57709429/s/1688679
2025-04-22 09:56:54 +08:00
wangdan-fit2cloud 54c9d4e725
feat: Support uploading files by copying, pasting, dragging and dropping (#2939) 2025-04-21 20:30:40 +08:00
shaohuzhang1 d2637c3de2
fix: Model parameters are not effective (#2937) 2025-04-21 18:06:09 +08:00
wxg0103 2550324003 refactor: oidc add field_mapping 2025-04-21 17:45:49 +08:00
wxg0103 bf52dd8174 fix: i18n error
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
--bug=1054853 --user=王孝刚 【API文档】-获取知识库文档分页列表接口的名称错误 https://www.tapd.cn/57709429/s/1688248
2025-04-21 10:20:44 +08:00
maninhill 2ecec57d2f
chore: Update README.md (#2927)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-04-18 17:34:11 +08:00
CaptainB b5fda0e020 chore: add dependabot configuration for weekly pip updates for v2
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-18 13:58:11 +08:00
liqiang-fit2cloud 45a60cd9a7 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-16 15:55:50 +08:00
maninhill 2b82675853
chore: Update README.md (#2897)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-04-16 12:15:24 +08:00
maninhill 1d3bf1ca73
chore: Update README_CN.md (#2893) 2025-04-16 09:50:28 +08:00
maninhill 04cb9c96fe
chore: Update README_CN.md (#2892) 2025-04-16 09:48:05 +08:00
liqiang-fit2cloud 45d8ac2eee Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-15 17:52:19 +08:00
maninhill eb60331c88
chore: Update README_CN.md (#2886)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-15 15:49:20 +08:00
liqiang-fit2cloud 8fc9b0a22d Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-15 15:24:31 +08:00
liqiang-fit2cloud ec5fd9e343 docs: change defaultSlogan.
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-15 14:37:10 +08:00
maninhill 1e49939c38
chore: Update README.md (#2884) 2025-04-15 14:27:10 +08:00
liqiang-fit2cloud 5a7b23aa00 docs: change defaultSlogan. 2025-04-15 14:23:32 +08:00
liqiang-fit2cloud 791505b7b8 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-15 13:50:17 +08:00
maninhill da10649adb Update README_CN.md 2025-04-15 12:57:07 +08:00
maninhill a025e3960d
chore: Update README_CN.md (#2880) 2025-04-15 12:51:12 +08:00
maninhill 98ed348de9
chore: Update README_CN.md (#2879) 2025-04-15 12:22:50 +08:00
maninhill b9dcc36b31
chore: Update README_CN.md (#2878) 2025-04-15 11:54:19 +08:00
liqiang-fit2cloud 7a3d3844ae Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-15 11:05:03 +08:00
maninhill 6ea2cf149a
chore: Update README_CN.md (#2874)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-14 19:18:44 +08:00
maninhill c30677d8b0
chore: Update README.md (#2873) 2025-04-14 19:02:23 +08:00
wxg0103 a1a2fb5628 fix: application title error
--bug=1054724 --user=王孝刚 【github#2855】【应用】修改应用名称后,演示界面及访问链接的web标签页名称还是旧名称 https://www.tapd.cn/57709429/s/1685555
2025-04-14 18:42:58 +08:00
CaptainB f9cb0e24d6 refactor: enhance input handling for custom sources in form fields
--bug=1054775 --user=刘瑞斌 【应用编排】MCP调用节点,自定义工具参数建议支持选择数据类型 https://www.tapd.cn/57709429/s/1685483
2025-04-14 17:43:42 +08:00
liqiang-fit2cloud 2dc42183cb Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-14 16:35:49 +08:00
wangdan-fit2cloud c781c11d26
fix: Application chat page style issue (#2866)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-14 15:11:55 +08:00
liqiang-fit2cloud e178cfe5c0 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-14 14:22:06 +08:00
CaptainB 3b24373cd0 fix: handle line breaks in cell content for markdown table formatting
--bug=1054683 --user=刘瑞斌 【github#2831】知识库上传excel、应用编排文档内容提取节点中上传excel,单元格中有换行,导入后没有在一个单元格里显示 https://www.tapd.cn/57709429/s/1685274
2025-04-14 14:21:51 +08:00
liqiang-fit2cloud 125ed8aa7a Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-14 11:01:00 +08:00
ivy 0b60a03e5d perf: refine copywriting
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-04-11 16:56:41 +08:00
liqiang-fit2cloud bb3f17ebfe Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-11 11:08:32 +08:00
liqiang-fit2cloud 1695710cbe Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-10 16:42:08 +08:00
liqiang-fit2cloud ebe8506c67 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-10 14:37:17 +08:00
liqiang-fit2cloud 3c3bd9884f Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-10 09:48:40 +08:00
liqiang-fit2cloud f188383fea Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 17:33:23 +08:00
liqiang-fit2cloud bbab359813 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 16:33:28 +08:00
liqiang-fit2cloud 8381ca5287 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 15:08:32 +08:00
liqiang-fit2cloud f5282bf1e7 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 10:03:53 +08:00
liqiang-fit2cloud c0ffc0aaf5 security: fix reverse shell vulnerability in function library. 2025-04-09 09:57:11 +08:00
1640 changed files with 67058 additions and 99767 deletions

View File

@ -1,2 +1,2 @@
.git* .git*
.idea* .idea*

View File

@ -6,4 +6,12 @@ updates:
interval: "weekly" interval: "weekly"
timezone: "Asia/Shanghai" timezone: "Asia/Shanghai"
day: "friday" day: "friday"
target-branch: "v3" target-branch: "v2"
groups:
python-dependencies:
patterns:
- "*"
# ignore:
# - dependency-name: "pymupdf"
# versions: ["*"]

View File

@ -14,19 +14,33 @@ on:
- linux/amd64,linux/arm64 - linux/amd64,linux/arm64
jobs: jobs:
build-and-push-python-pg-to-ghcr: build-and-push-python-pg-to-ghcr:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Check Disk Space
run: df -h
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Check Disk Space
run: df -h
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.ref_name }} ref: main
- name: Prepare - name: Prepare
id: prepare id: prepare
run: | run: |
DOCKER_IMAGE=ghcr.io/1panel-dev/maxkb-base DOCKER_IMAGE=ghcr.io/1panel-dev/maxkb-python-pg
DOCKER_PLATFORMS=${{ github.event.inputs.architecture }} DOCKER_PLATFORMS=${{ github.event.inputs.architecture }}
TAG_NAME=python3.11-pg17.5 TAG_NAME=python3.11-pg15.8
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}" DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
echo ::set-output name=docker_image::${DOCKER_IMAGE} echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=version::${TAG_NAME} echo ::set-output name=version::${TAG_NAME}
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --no-cache \ echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --no-cache \
@ -37,7 +51,8 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with: with:
cache-image: false # Until https://github.com/tonistiigi/binfmt/issues/215
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
@ -48,4 +63,4 @@ jobs:
password: ${{ secrets.GH_TOKEN }} password: ${{ secrets.GH_TOKEN }}
- name: Docker Buildx (build-and-push) - name: Docker Buildx (build-and-push)
run: | run: |
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile-base docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile-python-pg

View File

@ -5,7 +5,7 @@ on:
inputs: inputs:
dockerImageTag: dockerImageTag:
description: 'Docker Image Tag' description: 'Docker Image Tag'
default: 'v2.0.2' default: 'v1.0.1'
required: true required: true
architecture: architecture:
description: 'Architecture' description: 'Architecture'
@ -19,12 +19,26 @@ on:
jobs: jobs:
build-and-push-vector-model-to-ghcr: build-and-push-vector-model-to-ghcr:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Check Disk Space
run: df -h
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Check Disk Space
run: df -h
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.ref_name }} ref: main
- name: Prepare - name: Prepare
id: prepare id: prepare
run: | run: |
@ -42,7 +56,8 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with: with:
cache-image: false # Until https://github.com/tonistiigi/binfmt/issues/215
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry

View File

@ -1,13 +1,13 @@
name: build-and-push name: build-and-push
run-name: 构建镜像并推送仓库 ${{ github.event.inputs.dockerImageTag }} (${{ github.event.inputs.registry }}) (${{ github.event.inputs.architecture }}) run-name: 构建镜像并推送仓库 ${{ github.event.inputs.dockerImageTag }} (${{ github.event.inputs.registry }})
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
dockerImageTag: dockerImageTag:
description: 'Image Tag' description: 'Image Tag'
default: 'v2.0.1-dev' default: 'v1.10.7-dev'
required: true required: true
dockerImageTagWithLatest: dockerImageTagWithLatest:
description: '是否发布latest tag正式发版时选择测试版本切勿选择' description: '是否发布latest tag正式发版时选择测试版本切勿选择'
@ -41,7 +41,6 @@ jobs:
- name: Check Disk Space - name: Check Disk Space
run: df -h run: df -h
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
if: ${{ contains(github.event.inputs.architecture, ',') }}
uses: jlumbroso/free-disk-space@main uses: jlumbroso/free-disk-space@main
with: with:
tool-cache: true tool-cache: true
@ -49,8 +48,8 @@ jobs:
dotnet: true dotnet: true
haskell: true haskell: true
large-packages: true large-packages: true
docker-images: false docker-images: true
swap-storage: false swap-storage: true
- name: Check Disk Space - name: Check Disk Space
run: df -h run: df -h
- name: Checkout - name: Checkout
@ -65,17 +64,15 @@ jobs:
TAG_NAME=${{ github.event.inputs.dockerImageTag }} TAG_NAME=${{ github.event.inputs.dockerImageTag }}
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }} TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*} --tag ${DOCKER_IMAGE}:latest" DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
else else
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}" DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
fi fi
echo "buildx_args=--platform ${DOCKER_PLATFORMS} --memory-swap -1 \ echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --memory-swap -1 \
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \ --build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \
${DOCKER_IMAGE_TAGS} ." >> $GITHUB_OUTPUT ${DOCKER_IMAGE_TAGS} .
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with:
cache-image: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
@ -102,7 +99,6 @@ jobs:
- name: Check Disk Space - name: Check Disk Space
run: df -h run: df -h
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
if: ${{ contains(github.event.inputs.architecture, ',') }}
uses: jlumbroso/free-disk-space@main uses: jlumbroso/free-disk-space@main
with: with:
tool-cache: true tool-cache: true
@ -110,8 +106,8 @@ jobs:
dotnet: true dotnet: true
haskell: true haskell: true
large-packages: true large-packages: true
docker-images: false docker-images: true
swap-storage: false swap-storage: true
- name: Check Disk Space - name: Check Disk Space
run: df -h run: df -h
- name: Checkout - name: Checkout
@ -126,17 +122,15 @@ jobs:
TAG_NAME=${{ github.event.inputs.dockerImageTag }} TAG_NAME=${{ github.event.inputs.dockerImageTag }}
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }} TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*} --tag ${DOCKER_IMAGE}:latest" DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
else else
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}" DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
fi fi
echo "buildx_args=--platform ${DOCKER_PLATFORMS} --memory-swap -1 \ echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} --memory-swap -1 \
--build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \ --build-arg DOCKER_IMAGE_TAG=${{ github.event.inputs.dockerImageTag }} --build-arg BUILD_AT=$(TZ=Asia/Shanghai date +'%Y-%m-%dT%H:%M') --build-arg GITHUB_COMMIT=`git rev-parse --short HEAD` --no-cache \
${DOCKER_IMAGE_TAGS} ." >> $GITHUB_OUTPUT ${DOCKER_IMAGE_TAGS} .
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with:
cache-image: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry

View File

@ -1,7 +1,8 @@
name: Typos Check name: Typos Check
on: on:
workflow_dispatch:
push: push:
branches:
- main
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
@ -11,19 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v4 uses: actions/checkout@v2
with:
ref: ${{ github.ref_name }}
- name: Create config file
run: |
cat <<EOF > typo-check-config.toml
[files]
extend-exclude = [
"**/*_svg",
"**/migrations/**"
]
EOF
- name: Check spelling - name: Check spelling
uses: crate-ci/typos@master uses: crate-ci/typos@master
with:
config: ./typo-check-config.toml

9
.gitignore vendored
View File

@ -137,9 +137,9 @@ celerybeat.pid
# Environments # Environments
.env .env
.venv .venv
# env/ env/
venv/ venv/
# ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
@ -183,6 +183,5 @@ apps/xpack
data data
.dev .dev
poetry.lock poetry.lock
apps/models_provider/impl/*/icon/ apps/setting/models_provider/impl/*/icon/
tmp/ tmp/
config.yml

4
.typos.toml Normal file
View File

@ -0,0 +1,4 @@
[files]
extend-exclude = [
'apps/setting/models_provider/impl/*/icon/*'
]

View File

@ -27,4 +27,4 @@ When reporting issues, always include:
* Snapshots or log files if needed * Snapshots or log files if needed
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
replace those parts with "REDACTED" or other strings like "****". replace those parts with "REDACTED" or other strings like "****".

View File

@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>. <https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -14,7 +14,7 @@
MaxKB = Max Knowledge Brain, it is an open-source platform for building enterprise-grade agents. MaxKB integrates Retrieval-Augmented Generation (RAG) pipelines, supports robust workflows, and provides advanced MCP tool-use capabilities. MaxKB is widely applied in scenarios such as intelligent customer service, corporate internal knowledge bases, academic research, and education. MaxKB = Max Knowledge Brain, it is an open-source platform for building enterprise-grade agents. MaxKB integrates Retrieval-Augmented Generation (RAG) pipelines, supports robust workflows, and provides advanced MCP tool-use capabilities. MaxKB is widely applied in scenarios such as intelligent customer service, corporate internal knowledge bases, academic research, and education.
- **RAG Pipeline**: Supports direct uploading of documents / automatic crawling of online documents, with features for automatic text splitting, vectorization. This effectively reduces hallucinations in large models, providing a superior smart Q&A interaction experience. - **RAG Pipeline**: Supports direct uploading of documents / automatic crawling of online documents, with features for automatic text splitting, vectorization. This effectively reduces hallucinations in large models, providing a superior smart Q&A interaction experience.
- **Agentic Workflow**: Equipped with a powerful workflow engine, function library and MCP tool-use, enabling the orchestration of AI processes to meet the needs of complex business scenarios. - **Agentic Workflow**: Equipped with a powerful workflow engine, function library and MCP tool-use, enabling the orchestration of AI processes to meet the needs of complex business scenarios.
- **Seamless Integration**: Facilitates zero-coding rapid integration into third-party business systems, quickly equipping existing systems with intelligent Q&A capabilities to enhance user satisfaction. - **Seamless Integration**: Facilitates zero-coding rapid integration into third-party business systems, quickly equipping existing systems with intelligent Q&A capabilities to enhance user satisfaction.
- **Model-Agnostic**: Supports various large models, including private models (such as DeepSeek, Llama, Qwen, etc.) and public models (like OpenAI, Claude, Gemini, etc.). - **Model-Agnostic**: Supports various large models, including private models (such as DeepSeek, Llama, Qwen, etc.) and public models (like OpenAI, Claude, Gemini, etc.).
- **Multi Modal**: Native support for input and output text, image, audio and video. - **Multi Modal**: Native support for input and output text, image, audio and video.
@ -24,7 +24,7 @@ MaxKB = Max Knowledge Brain, it is an open-source platform for building enterpri
Execute the script below to start a MaxKB container using Docker: Execute the script below to start a MaxKB container using Docker:
```bash ```bash
docker run -d --name=maxkb --restart=always -p 8080:8080 -v ~/.maxkb:/opt/maxkb 1panel/maxkb docker run -d --name=maxkb --restart=always -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data -v ~/.python-packages:/opt/maxkb/app/sandbox/python-packages 1panel/maxkb
``` ```
Access MaxKB web interface at `http://your_server_ip:8080` with default admin credentials: Access MaxKB web interface at `http://your_server_ip:8080` with default admin credentials:
@ -32,18 +32,18 @@ Access MaxKB web interface at `http://your_server_ip:8080` with default admin cr
- username: admin - username: admin
- password: MaxKB@123.. - password: MaxKB@123..
中国用户如遇到 Docker 镜像 Pull 失败问题,请参照该 [离线安装文档](https://maxkb.cn/docs/v2/installation/offline_installtion/) 进行安装。 中国用户如遇到 Docker 镜像 Pull 失败问题,请参照该 [离线安装文档](https://maxkb.cn/docs/installation/offline_installtion/) 进行安装。
## Screenshots ## Screenshots
<table style="border-collapse: collapse; border: 1px solid black;"> <table style="border-collapse: collapse; border: 1px solid black;">
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/eb285512-a66a-4752-8941-c65ed1592238" alt="MaxKB Demo1" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/overview.png" alt="MaxKB Demo1" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/f732f1f5-472c-4fd2-93c1-a277eda83d04" alt="MaxKB Demo2" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/screenshot-models.png" alt="MaxKB Demo2" /></td>
</tr> </tr>
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/c927474a-9a23-4830-822f-5db26025c9b2" alt="MaxKB Demo3" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/screenshot-knowledge.png" alt="MaxKB Demo3" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/e6268996-a46d-4e58-9f30-31139df78ad2" alt="MaxKB Demo4" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://maxkb.hk/images/screenshot-function.png" alt="MaxKB Demo4" /></td>
</tr> </tr>
</table> </table>
@ -54,6 +54,67 @@ Access MaxKB web interface at `http://your_server_ip:8080` with default admin cr
- LLM Framework[LangChain](https://www.langchain.com/) - LLM Framework[LangChain](https://www.langchain.com/)
- Database[PostgreSQL + pgvector](https://www.postgresql.org/) - Database[PostgreSQL + pgvector](https://www.postgresql.org/)
## Feature Comparison
<table style="width: 100%;">
<tr>
<th align="center">Feature</th>
<th align="center">LangChain</th>
<th align="center">Dify.AI</th>
<th align="center">Flowise</th>
<th align="center">MaxKB <br>Built upon LangChain</th>
</tr>
<tr>
<td align="center">Supported LLMs</td>
<td align="center">Rich Variety</td>
<td align="center">Rich Variety</td>
<td align="center">Rich Variety</td>
<td align="center">Rich Variety</td>
</tr>
<tr>
<td align="center">RAG Engine</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">Agent</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">Workflow</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">Observability</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">SSO/Access control</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">✅ (Pro)</td>
</tr>
<tr>
<td align="center">On-premise Deployment</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
</table>
## Star History ## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/MaxKB&type=Date)](https://star-history.com/#1Panel-dev/MaxKB&Date) [![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/MaxKB&type=Date)](https://star-history.com/#1Panel-dev/MaxKB&Date)

View File

@ -14,12 +14,12 @@
</p> </p>
<hr/> <hr/>
MaxKB = Max Knowledge Brain是一个强大易用的企业级智能体平台,致力于解决企业 AI 落地面临的技术门槛高、部署成本高、迭代周期长等问题助力企业在人工智能时代赢得先机。秉承“开箱即用伴随成长”的设计理念MaxKB 支持企业快速接入主流大模型高效构建专属知识库并提供从基础问答RAG、复杂流程自动化工作流到智能体Agent的渐进式升级路径全面赋能智能客服、智能办公助手等多种应用场景。 MaxKB = Max Knowledge Brain是一款强大易用的企业级智能体平台,支持 RAG 检索增强生成、工作流编排、MCP 工具调用能力。MaxKB 支持对接各种主流大语言模型,广泛应用于智能客服、企业内部知识库问答、员工助手、学术研究与教育等场景。
- **RAG 检索增强生成**:高效搭建本地 AI 知识库,支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化,有效减少大模型幻觉,提升问答效果; - **RAG 检索增强生成**:高效搭建本地 AI 知识库,支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化,有效减少大模型幻觉,提升问答效果;
- **灵活编排**:内置强大的工作流引擎、函数库和 MCP 工具调用能力,支持编排 AI 工作过程,满足复杂业务场景下的需求; - **灵活编排**:内置强大的工作流引擎、函数库和 MCP 工具调用能力,支持编排 AI 工作过程,满足复杂业务场景下的需求;
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度; - **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度;
- **模型中立**支持对接各种大模型包括本地私有大模型DeepSeek R1 / Qwen 3 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等和国外公共大模型OpenAI / Claude / Gemini 等)。 - **模型中立**支持对接各种大模型包括本地私有大模型DeepSeek R1 / Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等和国外公共大模型OpenAI / Claude / Gemini 等)。
MaxKB 三分钟视频介绍https://www.bilibili.com/video/BV18JypYeEkj/ MaxKB 三分钟视频介绍https://www.bilibili.com/video/BV18JypYeEkj/
@ -27,10 +27,10 @@ MaxKB 三分钟视频介绍https://www.bilibili.com/video/BV18JypYeEkj/
``` ```
# Linux 机器 # Linux 机器
docker run -d --name=maxkb --restart=always -p 8080:8080 -v ~/.maxkb:/opt/maxkb registry.fit2cloud.com/maxkb/maxkb docker run -d --name=maxkb --restart=always -p 8080:8080 -v ~/.maxkb:/var/lib/postgresql/data -v ~/.python-packages:/opt/maxkb/app/sandbox/python-packages registry.fit2cloud.com/maxkb/maxkb
# Windows 机器 # Windows 机器
docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/opt/maxkb registry.fit2cloud.com/maxkb/maxkb docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/var/lib/postgresql/data -v C:/python-packages:/opt/maxkb/app/sandbox/python-packages registry.fit2cloud.com/maxkb/maxkb
# 用户名: admin # 用户名: admin
# 密码: MaxKB@123.. # 密码: MaxKB@123..
@ -38,8 +38,8 @@ docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/opt/maxkb
- 你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB - 你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB
- 如果是内网环境,推荐使用 [离线安装包](https://community.fit2cloud.com/#/products/maxkb/downloads) 进行安装部署; - 如果是内网环境,推荐使用 [离线安装包](https://community.fit2cloud.com/#/products/maxkb/downloads) 进行安装部署;
- MaxKB 不同产品产品版本的对比请参见:[MaxKB 产品版本对比](https://maxkb.cn/price) - MaxKB 产品版本分为社区版和专业版,详情请参见:[MaxKB 产品版本对比](https://maxkb.cn/pricing.html)
- 如果您需要向团队介绍 MaxKB可以使用这个 [官方 PPT 材料](https://fit2cloud.com/maxkb/download/introduce-maxkb_202507.pdf)。 - 如果您需要向团队介绍 MaxKB可以使用这个 [官方 PPT 材料](https://maxkb.cn/download/introduce-maxkb_202503.pdf)。
如你有更多问题,可以查看使用手册,或者通过论坛与我们交流。 如你有更多问题,可以查看使用手册,或者通过论坛与我们交流。
@ -54,12 +54,12 @@ docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/opt/maxkb
<table style="border-collapse: collapse; border: 1px solid black;"> <table style="border-collapse: collapse; border: 1px solid black;">
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/eb285512-a66a-4752-8941-c65ed1592238" alt="MaxKB Demo1" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/52996290/d87395fa-a8d7-401c-82bf-c6e475d10ae9" alt="MaxKB Demo1" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/f732f1f5-472c-4fd2-93c1-a277eda83d04" alt="MaxKB Demo2" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/1Panel-dev/MaxKB/assets/52996290/47c35ee4-3a3b-4bd4-9f4f-ee20788b2b9a" alt="MaxKB Demo2" /></td>
</tr> </tr>
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/c927474a-9a23-4830-822f-5db26025c9b2" alt="MaxKB Demo3" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/9a1043cb-fa62-4f71-b9a3-0b46fa59a70e" alt="MaxKB Demo3" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/e6268996-a46d-4e58-9f30-31139df78ad2" alt="MaxKB Demo4" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/3407ce9a-779c-4eb4-858e-9441a2ddc664" alt="MaxKB Demo4" /></td>
</tr> </tr>
</table> </table>

View File

@ -36,4 +36,4 @@
- [MaxKB 应用案例重磅陕西广电网络“秦岭云”平台实现DeepSeek本地化部署](https://mp.weixin.qq.com/s/ZKmEU_wWShK1YDomKJHQeA) - [MaxKB 应用案例重磅陕西广电网络“秦岭云”平台实现DeepSeek本地化部署](https://mp.weixin.qq.com/s/ZKmEU_wWShK1YDomKJHQeA)
- [MaxKB 应用案例粤海集团完成DeepSeek私有化部署助力集团智能化管理](https://mp.weixin.qq.com/s/2JbVp0-kr9Hfp-0whH4cvg) - [MaxKB 应用案例粤海集团完成DeepSeek私有化部署助力集团智能化管理](https://mp.weixin.qq.com/s/2JbVp0-kr9Hfp-0whH4cvg)
- [MaxKB 应用案例建筑材料工业信息中心完成DeepSeek本地化部署推动行业数智化转型新发展](https://mp.weixin.qq.com/s/HThGSnND3qDF8ySEqiM4jw) - [MaxKB 应用案例建筑材料工业信息中心完成DeepSeek本地化部署推动行业数智化转型新发展](https://mp.weixin.qq.com/s/HThGSnND3qDF8ySEqiM4jw)
- [MaxKB 应用案例一起DeepSeek福建设计以AI大模型开启新篇章](https://mp.weixin.qq.com/s/m67e-H7iQBg3d24NM82UjA) - [MaxKB 应用案例一起DeepSeek福建设计以AI大模型开启新篇章](https://mp.weixin.qq.com/s/m67e-H7iQBg3d24NM82UjA)

View File

@ -1,35 +0,0 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application_access_token.py
@date2025/6/9 17:46
@desc:
"""
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from application.serializers.application_access_token import AccessTokenEditSerializer
from common.mixins.api_mixin import APIMixin
class ApplicationAccessTokenAPI(APIMixin):
@staticmethod
def get_parameters():
return [OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
), OpenApiParameter(
name="application_id",
description="应用id",
type=OpenApiTypes.STR,
location='path',
required=True,
)]
@staticmethod
def get_request():
return AccessTokenEditSerializer

View File

@ -1,218 +0,0 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application.py
@date2025/5/26 16:59
@desc:
"""
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from rest_framework import serializers
from application.serializers.application import ApplicationCreateSerializer, ApplicationListResponse, \
ApplicationImportRequest, ApplicationEditSerializer, TextToSpeechRequest, SpeechToTextRequest, PlayDemoTextRequest
from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer, ResultPageSerializer, DefaultResultSerializer
class ApplicationCreateRequest(ApplicationCreateSerializer.SimplateRequest):
work_flow = serializers.DictField(required=True, label=_("Workflow Objects"))
class ApplicationCreateResponse(ResultSerializer):
def get_data(self):
return ApplicationCreateSerializer.ApplicationResponse()
class ApplicationListResult(ResultSerializer):
def get_data(self):
return ApplicationListResponse(many=True)
class ApplicationPageResult(ResultPageSerializer):
def get_data(self):
return ApplicationListResponse(many=True)
class ApplicationQueryAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="current_page",
description=_("Current page"),
type=OpenApiTypes.INT,
location='path',
required=True,
),
OpenApiParameter(
name="page_size",
description=_("Page size"),
type=OpenApiTypes.INT,
location='path',
required=True,
),
OpenApiParameter(
name="folder_id",
description=_("folder id"),
type=OpenApiTypes.STR,
location='query',
required=False,
),
OpenApiParameter(
name="name",
description=_("Application Name"),
type=OpenApiTypes.STR,
location='query',
required=False,
),
OpenApiParameter(
name="desc",
description=_("Application Description"),
type=OpenApiTypes.STR,
location='query',
required=False,
),
OpenApiParameter(
name="user_id",
description=_("User ID"),
type=OpenApiTypes.STR,
location='query',
required=False,
),
OpenApiParameter(
name="publish_status",
description=_("Publish status") + '(published|unpublished)',
type=OpenApiTypes.STR,
location='query',
required=False,
)
]
@staticmethod
def get_response():
return ApplicationListResult
@staticmethod
def get_page_response():
return ApplicationPageResult
class ApplicationCreateAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
@staticmethod
def get_request():
return ApplicationCreateRequest
@staticmethod
def get_response():
return ApplicationCreateResponse
class ApplicationImportAPI(APIMixin):
@staticmethod
def get_parameters():
ApplicationCreateAPI.get_parameters()
@staticmethod
def get_request():
return ApplicationImportRequest
class ApplicationOperateAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="应用id",
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
class ApplicationExportAPI(APIMixin):
@staticmethod
def get_parameters():
return ApplicationOperateAPI.get_parameters()
@staticmethod
def get_response():
return DefaultResultSerializer
class ApplicationEditAPI(APIMixin):
@staticmethod
def get_request():
return ApplicationEditSerializer
class TextToSpeechAPI(APIMixin):
@staticmethod
def get_parameters():
return ApplicationOperateAPI.get_parameters()
@staticmethod
def get_request():
return TextToSpeechRequest
@staticmethod
def get_response():
return DefaultResultSerializer
class SpeechToTextAPI(APIMixin):
@staticmethod
def get_parameters():
return ApplicationOperateAPI.get_parameters()
@staticmethod
def get_request():
return SpeechToTextRequest
@staticmethod
def get_response():
return DefaultResultSerializer
class PlayDemoTextAPI(APIMixin):
@staticmethod
def get_parameters():
return ApplicationOperateAPI.get_parameters()
@staticmethod
def get_request():
return PlayDemoTextRequest
@staticmethod
def get_response():
return DefaultResultSerializer

View File

@ -1,61 +0,0 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from application.serializers.application_api_key import EditApplicationKeySerializer, ApplicationKeySerializerModel
from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer
class ApplicationKeyListResult(ResultSerializer):
def get_data(self):
return ApplicationKeySerializerModel(many=True)
class ApplicationKeyResult(ResultSerializer):
def get_data(self):
return ApplicationKeySerializerModel()
class ApplicationKeyAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
@staticmethod
def get_response():
return ApplicationKeyResult
class List(APIMixin):
@staticmethod
def get_response():
return ApplicationKeyListResult
class Operate(APIMixin):
@staticmethod
def get_parameters():
return [*ApplicationKeyAPI.get_parameters(), OpenApiParameter(
name="api_key_id",
description="ApiKeyId",
type=OpenApiTypes.STR,
location='path',
required=True,
)]
@staticmethod
def get_request():
return EditApplicationKeySerializer

View File

@ -1,135 +0,0 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application_chat.py
@date2025/6/10 13:54
@desc:
"""
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from application.serializers.application_chat import ApplicationChatQuerySerializers, \
ApplicationChatResponseSerializers, ApplicationChatRecordExportRequest
from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer, ResultPageSerializer
class ApplicationChatListResponseSerializers(ResultSerializer):
def get_data(self):
return ApplicationChatResponseSerializers(many=True)
class ApplicationChatPageResponseSerializers(ResultPageSerializer):
def get_data(self):
return ApplicationChatResponseSerializers(many=True)
class ApplicationChatQueryAPI(APIMixin):
@staticmethod
def get_request():
return ApplicationChatQuerySerializers
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
), OpenApiParameter(
name="start_time",
description="start Time",
type=OpenApiTypes.STR,
required=True,
),
OpenApiParameter(
name="end_time",
description="end Time",
type=OpenApiTypes.STR,
required=True,
),
OpenApiParameter(
name="abstract",
description="summary",
type=OpenApiTypes.STR,
required=False,
),
OpenApiParameter(
name="min_star",
description=_("Minimum number of likes"),
type=OpenApiTypes.INT,
required=False,
),
OpenApiParameter(
name="min_trample",
description=_("Minimum number of clicks"),
type=OpenApiTypes.INT,
required=False,
),
OpenApiParameter(
name="comparer",
description=_("Comparator"),
type=OpenApiTypes.STR,
required=False,
),
]
@staticmethod
def get_response():
return ApplicationChatListResponseSerializers
class ApplicationChatQueryPageAPI(APIMixin):
@staticmethod
def get_request():
return ApplicationChatQueryAPI.get_request()
@staticmethod
def get_parameters():
return [
*ApplicationChatQueryAPI.get_parameters(),
OpenApiParameter(
name="current_page",
description=_("Current page"),
type=OpenApiTypes.INT,
location='path',
required=True,
),
OpenApiParameter(
name="page_size",
description=_("Page size"),
type=OpenApiTypes.INT,
location='path',
required=True,
),
]
@staticmethod
def get_response():
return ApplicationChatPageResponseSerializers
class ApplicationChatExportAPI(APIMixin):
@staticmethod
def get_request():
return ApplicationChatRecordExportRequest
@staticmethod
def get_parameters():
return ApplicationChatQueryAPI.get_parameters()
@staticmethod
def get_response():
return None

View File

@ -1,180 +0,0 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application_chat_record.py
@date2025/6/10 15:19
@desc:
"""
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from application.serializers.application_chat_record import ApplicationChatRecordAddKnowledgeSerializer, \
ApplicationChatRecordImproveInstanceSerializer
from common.mixins.api_mixin import APIMixin
class ApplicationChatRecordQueryAPI(APIMixin):
@staticmethod
def get_response():
pass
@staticmethod
def get_request():
pass
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="Application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="chat_id",
description=_("Chat ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="order_asc",
description=_("Is it in order"),
type=OpenApiTypes.BOOL,
required=True,
)
]
class ApplicationChatRecordPageQueryAPI(APIMixin):
@staticmethod
def get_response():
pass
@staticmethod
def get_request():
pass
@staticmethod
def get_parameters():
return [*ApplicationChatRecordQueryAPI.get_parameters(),
OpenApiParameter(
name="current_page",
description=_("Current page"),
type=OpenApiTypes.INT,
location='path',
required=True,
),
OpenApiParameter(
name="page_size",
description=_("Page size"),
type=OpenApiTypes.INT,
location='path',
required=True,
)]
class ApplicationChatRecordImproveParagraphAPI(APIMixin):
@staticmethod
def get_response():
pass
@staticmethod
def get_request():
return ApplicationChatRecordImproveInstanceSerializer
@staticmethod
def get_parameters():
return [OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="Application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="chat_id",
description=_("Chat ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="chat_record_id",
description=_("Chat Record ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="knowledge_id",
description=_("Knowledge ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="document_id",
description=_("Document ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
class Operate(APIMixin):
@staticmethod
def get_parameters():
return [*ApplicationChatRecordImproveParagraphAPI.get_parameters(), OpenApiParameter(
name="paragraph_id",
description=_("Paragraph ID"),
type=OpenApiTypes.STR,
location='path',
required=True,
)]
class ApplicationChatRecordAddKnowledgeAPI(APIMixin):
@staticmethod
def get_request():
return ApplicationChatRecordAddKnowledgeSerializer
@staticmethod
def get_response():
return None
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="Application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
)]

View File

@ -1,55 +0,0 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application_stats.py
@date2025/6/9 20:45
@desc:
"""
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from application.serializers.application_stats import ApplicationStatsSerializer
from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer
class ApplicationStatsResult(ResultSerializer):
def get_data(self):
return ApplicationStatsSerializer(many=True)
class ApplicationStatsAPI(APIMixin):
@staticmethod
def get_parameters():
return [OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="start_time",
description="start Time",
type=OpenApiTypes.STR,
required=True,
),
OpenApiParameter(
name="end_time",
description="end Time",
type=OpenApiTypes.STR,
required=True,
),
]
@staticmethod
def get_response():
return ApplicationStatsResult

View File

@ -1,96 +0,0 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file application_version.py
@date2025/6/4 17:33
@desc:
"""
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from application.serializers.application_version import ApplicationVersionModelSerializer
from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer, PageDataResponse, ResultPageSerializer
class ApplicationListVersionResult(ResultSerializer):
def get_data(self):
return ApplicationVersionModelSerializer(many=True)
class ApplicationPageVersionResult(ResultPageSerializer):
def get_data(self):
return ApplicationVersionModelSerializer(many=True)
class ApplicationWorkflowVersionResult(ResultSerializer):
def get_data(self):
return ApplicationVersionModelSerializer()
class ApplicationVersionAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="application_id",
description="application ID",
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
class ApplicationVersionOperateAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="application_version_id",
description="工作流版本id",
type=OpenApiTypes.STR,
location='path',
required=True,
)
, *ApplicationVersionAPI.get_parameters()
]
@staticmethod
def get_response():
return ApplicationWorkflowVersionResult
class ApplicationVersionListAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="name",
description="Version Name",
type=OpenApiTypes.STR,
required=False,
)
, *ApplicationVersionAPI.get_parameters()]
@staticmethod
def get_response():
return ApplicationListVersionResult
class ApplicationVersionPageAPI(APIMixin):
@staticmethod
def get_parameters():
return ApplicationVersionListAPI.get_parameters()
@staticmethod
def get_response():
return ApplicationPageVersionResult

View File

@ -12,45 +12,42 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from knowledge.models import Paragraph from dataset.models import Paragraph
class ParagraphPipelineModel: class ParagraphPipelineModel:
def __init__(self, _id: str, document_id: str, knowledge_id: str, content: str, title: str, status: str, def __init__(self, _id: str, document_id: str, dataset_id: str, content: str, title: str, status: str,
is_active: bool, comprehensive_score: float, similarity: float, knowledge_name: str, is_active: bool, comprehensive_score: float, similarity: float, dataset_name: str, document_name: str,
document_name: str, hit_handling_method: str, directly_return_similarity: float, meta: dict = None):
hit_handling_method: str, directly_return_similarity: float, knowledge_type, meta: dict = None):
self.id = _id self.id = _id
self.document_id = document_id self.document_id = document_id
self.knowledge_id = knowledge_id self.dataset_id = dataset_id
self.content = content self.content = content
self.title = title self.title = title
self.status = status, self.status = status,
self.is_active = is_active self.is_active = is_active
self.comprehensive_score = comprehensive_score self.comprehensive_score = comprehensive_score
self.similarity = similarity self.similarity = similarity
self.knowledge_name = knowledge_name self.dataset_name = dataset_name
self.document_name = document_name self.document_name = document_name
self.hit_handling_method = hit_handling_method self.hit_handling_method = hit_handling_method
self.directly_return_similarity = directly_return_similarity self.directly_return_similarity = directly_return_similarity
self.meta = meta self.meta = meta
self.knowledge_type = knowledge_type
def to_dict(self): def to_dict(self):
return { return {
'id': self.id, 'id': self.id,
'document_id': self.document_id, 'document_id': self.document_id,
'knowledge_id': self.knowledge_id, 'dataset_id': self.dataset_id,
'content': self.content, 'content': self.content,
'title': self.title, 'title': self.title,
'status': self.status, 'status': self.status,
'is_active': self.is_active, 'is_active': self.is_active,
'comprehensive_score': self.comprehensive_score, 'comprehensive_score': self.comprehensive_score,
'similarity': self.similarity, 'similarity': self.similarity,
'knowledge_name': self.knowledge_name, 'dataset_name': self.dataset_name,
'document_name': self.document_name, 'document_name': self.document_name,
'knowledge_type': self.knowledge_type,
'meta': self.meta, 'meta': self.meta,
} }
@ -60,8 +57,7 @@ class ParagraphPipelineModel:
self.paragraph = {} self.paragraph = {}
self.comprehensive_score = None self.comprehensive_score = None
self.document_name = None self.document_name = None
self.knowledge_name = None self.dataset_name = None
self.knowledge_type = None
self.hit_handling_method = None self.hit_handling_method = None
self.directly_return_similarity = 0.9 self.directly_return_similarity = 0.9
self.meta = {} self.meta = {}
@ -70,7 +66,7 @@ class ParagraphPipelineModel:
if isinstance(paragraph, Paragraph): if isinstance(paragraph, Paragraph):
self.paragraph = {'id': paragraph.id, self.paragraph = {'id': paragraph.id,
'document_id': paragraph.document_id, 'document_id': paragraph.document_id,
'knowledge_id': paragraph.knowledge_id, 'dataset_id': paragraph.dataset_id,
'content': paragraph.content, 'content': paragraph.content,
'title': paragraph.title, 'title': paragraph.title,
'status': paragraph.status, 'status': paragraph.status,
@ -80,12 +76,8 @@ class ParagraphPipelineModel:
self.paragraph = paragraph self.paragraph = paragraph
return self return self
def add_knowledge_name(self, knowledge_name): def add_dataset_name(self, dataset_name):
self.knowledge_name = knowledge_name self.dataset_name = dataset_name
return self
def add_knowledge_type(self, knowledge_type):
self.knowledge_type = knowledge_type
return self return self
def add_document_name(self, document_name): def add_document_name(self, document_name):
@ -114,13 +106,12 @@ class ParagraphPipelineModel:
def build(self): def build(self):
return ParagraphPipelineModel(str(self.paragraph.get('id')), str(self.paragraph.get('document_id')), return ParagraphPipelineModel(str(self.paragraph.get('id')), str(self.paragraph.get('document_id')),
str(self.paragraph.get('knowledge_id')), str(self.paragraph.get('dataset_id')),
self.paragraph.get('content'), self.paragraph.get('title'), self.paragraph.get('content'), self.paragraph.get('title'),
self.paragraph.get('status'), self.paragraph.get('status'),
self.paragraph.get('is_active'), self.paragraph.get('is_active'),
self.comprehensive_score, self.similarity, self.knowledge_name, self.comprehensive_score, self.similarity, self.dataset_name,
self.document_name, self.hit_handling_method, self.directly_return_similarity, self.document_name, self.hit_handling_method, self.directly_return_similarity,
self.knowledge_type,
self.meta) self.meta)

View File

@ -17,14 +17,12 @@ from common.handle.impl.response.system_to_response import SystemToResponse
class PipelineManage: class PipelineManage:
def __init__(self, step_list: List[Type[IBaseChatPipelineStep]], def __init__(self, step_list: List[Type[IBaseChatPipelineStep]],
base_to_response: BaseToResponse = SystemToResponse(), base_to_response: BaseToResponse = SystemToResponse()):
debug=False):
# 步骤执行器 # 步骤执行器
self.step_list = [step() for step in step_list] self.step_list = [step() for step in step_list]
# 上下文 # 上下文
self.context = {'message_tokens': 0, 'answer_tokens': 0} self.context = {'message_tokens': 0, 'answer_tokens': 0}
self.base_to_response = base_to_response self.base_to_response = base_to_response
self.debug = debug
def run(self, context: Dict = None): def run(self, context: Dict = None):
self.context['start_time'] = time.time() self.context['start_time'] = time.time()
@ -46,7 +44,6 @@ class PipelineManage:
def __init__(self): def __init__(self):
self.step_list: List[Type[IBaseChatPipelineStep]] = [] self.step_list: List[Type[IBaseChatPipelineStep]] = []
self.base_to_response = SystemToResponse() self.base_to_response = SystemToResponse()
self.debug = False
def append_step(self, step: Type[IBaseChatPipelineStep]): def append_step(self, step: Type[IBaseChatPipelineStep]):
self.step_list.append(step) self.step_list.append(step)
@ -56,9 +53,5 @@ class PipelineManage:
self.base_to_response = base_to_response self.base_to_response = base_to_response
return self return self
def add_debug(self, debug):
self.debug = debug
return self
def build(self): def build(self):
return PipelineManage(step_list=self.step_list, base_to_response=self.base_to_response, debug=self.debug) return PipelineManage(step_list=self.step_list, base_to_response=self.base_to_response)

View File

@ -16,8 +16,9 @@ from rest_framework import serializers
from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
from application.chat_pipeline.pipeline_manage import PipelineManage from application.chat_pipeline.pipeline_manage import PipelineManage
from application.serializers.application import NoReferencesSetting from application.serializers.application_serializers import NoReferencesSetting
from common.field.common import InstanceField from common.field.common import InstanceField
from common.util.field_message import ErrMessage
class ModelField(serializers.Field): class ModelField(serializers.Field):
@ -44,7 +45,7 @@ class PostResponseHandler:
@abstractmethod @abstractmethod
def handler(self, chat_id, chat_record_id, paragraph_list: List[ParagraphPipelineModel], problem_text: str, def handler(self, chat_id, chat_record_id, paragraph_list: List[ParagraphPipelineModel], problem_text: str,
answer_text, answer_text,
manage, step, padding_problem_text: str = None, **kwargs): manage, step, padding_problem_text: str = None, client_id=None, **kwargs):
pass pass
@ -52,36 +53,35 @@ class IChatStep(IBaseChatPipelineStep):
class InstanceSerializer(serializers.Serializer): class InstanceSerializer(serializers.Serializer):
# 对话列表 # 对话列表
message_list = serializers.ListField(required=True, child=MessageField(required=True), message_list = serializers.ListField(required=True, child=MessageField(required=True),
label=_("Conversation list")) error_messages=ErrMessage.list(_("Conversation list")))
model_id = serializers.UUIDField(required=False, allow_null=True, label=_("Model id")) model_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid(_("Model id")))
# 段落列表 # 段落列表
paragraph_list = serializers.ListField(label=_("Paragraph List")) paragraph_list = serializers.ListField(error_messages=ErrMessage.list(_("Paragraph List")))
# 对话id # 对话id
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID")) chat_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("Conversation ID")))
# 用户问题 # 用户问题
problem_text = serializers.CharField(required=True, label=_("User Questions")) problem_text = serializers.CharField(required=True, error_messages=ErrMessage.uuid(_("User Questions")))
# 后置处理器 # 后置处理器
post_response_handler = InstanceField(model_type=PostResponseHandler, post_response_handler = InstanceField(model_type=PostResponseHandler,
label=_("Post-processor")) error_messages=ErrMessage.base(_("Post-processor")))
# 补全问题 # 补全问题
padding_problem_text = serializers.CharField(required=False, padding_problem_text = serializers.CharField(required=False,
label=_("Completion Question")) error_messages=ErrMessage.base(_("Completion Question")))
# 是否使用流的形式输出 # 是否使用流的形式输出
stream = serializers.BooleanField(required=False, label=_("Streaming Output")) stream = serializers.BooleanField(required=False, error_messages=ErrMessage.base(_("Streaming Output")))
chat_user_id = serializers.CharField(required=True, label=_("Chat user id")) client_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Client id")))
client_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Client Type")))
chat_user_type = serializers.CharField(required=True, label=_("Chat user Type"))
# 未查询到引用分段 # 未查询到引用分段
no_references_setting = NoReferencesSetting(required=True, no_references_setting = NoReferencesSetting(required=True,
label=_("No reference segment settings")) error_messages=ErrMessage.base(_("No reference segment settings")))
workspace_id = serializers.CharField(required=True, label=_("Workspace ID")) user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
model_setting = serializers.DictField(required=True, allow_null=True, model_setting = serializers.DictField(required=True, allow_null=True,
label=_("Model settings")) error_messages=ErrMessage.dict(_("Model settings")))
model_params_setting = serializers.DictField(required=False, allow_null=True, model_params_setting = serializers.DictField(required=False, allow_null=True,
label=_("Model parameter settings")) error_messages=ErrMessage.dict(_("Model parameter settings")))
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)
@ -102,9 +102,9 @@ class IChatStep(IBaseChatPipelineStep):
chat_id, problem_text, chat_id, problem_text,
post_response_handler: PostResponseHandler, post_response_handler: PostResponseHandler,
model_id: str = None, model_id: str = None,
workspace_id: str = None, user_id: str = None,
paragraph_list=None, paragraph_list=None,
manage: PipelineManage = None, manage: PipelineManage = None,
padding_problem_text: str = None, stream: bool = True, chat_user_id=None, chat_user_type=None, padding_problem_text: str = None, stream: bool = True, client_id=None, client_type=None,
no_references_setting=None, model_params_setting=None, model_setting=None, **kwargs): no_references_setting=None, model_params_setting=None, model_setting=None, **kwargs):
pass pass

View File

@ -9,7 +9,7 @@
import logging import logging
import time import time
import traceback import traceback
import uuid_utils.compat as uuid import uuid
from typing import List from typing import List
from django.db.models import QuerySet from django.db.models import QuerySet
@ -18,24 +18,22 @@ from django.utils.translation import gettext as _
from langchain.chat_models.base import BaseChatModel from langchain.chat_models.base import BaseChatModel
from langchain.schema import BaseMessage from langchain.schema import BaseMessage
from langchain.schema.messages import HumanMessage, AIMessage from langchain.schema.messages import HumanMessage, AIMessage
from langchain_core.messages import AIMessageChunk, SystemMessage from langchain_core.messages import AIMessageChunk
from rest_framework import status from rest_framework import status
from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
from application.chat_pipeline.pipeline_manage import PipelineManage from application.chat_pipeline.pipeline_manage import PipelineManage
from application.chat_pipeline.step.chat_step.i_chat_step import IChatStep, PostResponseHandler from application.chat_pipeline.step.chat_step.i_chat_step import IChatStep, PostResponseHandler
from application.flow.tools import Reasoning from application.flow.tools import Reasoning
from application.models import ApplicationChatUserStats, ChatUserType from application.models.api_key_model import ApplicationPublicAccessClient
from common.utils.logger import maxkb_logger from common.constants.authentication_type import AuthenticationType
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
def add_access_num(chat_user_id=None, chat_user_type=None, application_id=None): def add_access_num(client_id=None, client_type=None, application_id=None):
if [ChatUserType.ANONYMOUS_USER.value, ChatUserType.CHAT_USER.value].__contains__( if client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value and application_id is not None:
chat_user_type) and application_id is not None: application_public_access_client = (QuerySet(ApplicationPublicAccessClient).filter(client_id=client_id,
application_public_access_client = (QuerySet(ApplicationChatUserStats).filter(chat_user_id=chat_user_id, application_id=application_id)
chat_user_type=chat_user_type,
application_id=application_id)
.first()) .first())
if application_public_access_client is not None: if application_public_access_client is not None:
application_public_access_client.access_num = application_public_access_client.access_num + 1 application_public_access_client.access_num = application_public_access_client.access_num + 1
@ -65,7 +63,7 @@ def event_content(response,
message_list: List[BaseMessage], message_list: List[BaseMessage],
problem_text: str, problem_text: str,
padding_problem_text: str = None, padding_problem_text: str = None,
chat_user_id=None, chat_user_type=None, client_id=None, client_type=None,
is_ai_chat: bool = None, is_ai_chat: bool = None,
model_setting=None): model_setting=None):
if model_setting is None: if model_setting is None:
@ -126,24 +124,26 @@ def event_content(response,
request_token = 0 request_token = 0
response_token = 0 response_token = 0
write_context(step, manage, request_token, response_token, all_text) write_context(step, manage, request_token, response_token, all_text)
asker = manage.context.get('form_data', {}).get('asker', None)
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text, post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
all_text, manage, step, padding_problem_text, all_text, manage, step, padding_problem_text, client_id,
reasoning_content=reasoning_content if reasoning_content_enable else '') reasoning_content=reasoning_content if reasoning_content_enable else ''
, asker=asker)
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node', yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
[], '', True, [], '', True,
request_token, response_token, request_token, response_token,
{'node_is_end': True, 'view_type': 'many_view', {'node_is_end': True, 'view_type': 'many_view',
'node_type': 'ai-chat-node'}) 'node_type': 'ai-chat-node'})
if not manage.debug: add_access_num(client_id, client_type, manage.context.get('application_id'))
add_access_num(chat_user_id, chat_user_type, manage.context.get('application_id'))
except Exception as e: except Exception as e:
maxkb_logger.error(f'{str(e)}:{traceback.format_exc()}') logging.getLogger("max_kb_error").error(f'{str(e)}:{traceback.format_exc()}')
all_text = 'Exception:' + str(e) all_text = 'Exception:' + str(e)
write_context(step, manage, 0, 0, all_text) write_context(step, manage, 0, 0, all_text)
asker = manage.context.get('form_data', {}).get('asker', None)
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text, post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
all_text, manage, step, padding_problem_text, reasoning_content='') all_text, manage, step, padding_problem_text, client_id, reasoning_content='',
if not manage.debug: asker=asker)
add_access_num(chat_user_id, chat_user_type, manage.context.get('application_id')) add_access_num(client_id, client_type, manage.context.get('application_id'))
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node', yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
[], all_text, [], all_text,
False, False,
@ -160,28 +160,27 @@ class BaseChatStep(IChatStep):
problem_text, problem_text,
post_response_handler: PostResponseHandler, post_response_handler: PostResponseHandler,
model_id: str = None, model_id: str = None,
workspace_id: str = None, user_id: str = None,
paragraph_list=None, paragraph_list=None,
manage: PipelineManage = None, manage: PipelineManage = None,
padding_problem_text: str = None, padding_problem_text: str = None,
stream: bool = True, stream: bool = True,
chat_user_id=None, chat_user_type=None, client_id=None, client_type=None,
no_references_setting=None, no_references_setting=None,
model_params_setting=None, model_params_setting=None,
model_setting=None, model_setting=None,
**kwargs): **kwargs):
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, chat_model = get_model_instance_by_model_user_id(model_id, user_id,
**model_params_setting) if model_id is not None else None **model_params_setting) if model_id is not None else None
if stream: if stream:
return self.execute_stream(message_list, chat_id, problem_text, post_response_handler, chat_model, return self.execute_stream(message_list, chat_id, problem_text, post_response_handler, chat_model,
paragraph_list, paragraph_list,
manage, padding_problem_text, chat_user_id, chat_user_type, manage, padding_problem_text, client_id, client_type, no_references_setting,
no_references_setting,
model_setting) model_setting)
else: else:
return self.execute_block(message_list, chat_id, problem_text, post_response_handler, chat_model, return self.execute_block(message_list, chat_id, problem_text, post_response_handler, chat_model,
paragraph_list, paragraph_list,
manage, padding_problem_text, chat_user_id, chat_user_type, no_references_setting, manage, padding_problem_text, client_id, client_type, no_references_setting,
model_setting) model_setting)
def get_details(self, manage, **kwargs): def get_details(self, manage, **kwargs):
@ -198,8 +197,7 @@ class BaseChatStep(IChatStep):
@staticmethod @staticmethod
def reset_message_list(message_list: List[BaseMessage], answer_text): def reset_message_list(message_list: List[BaseMessage], answer_text):
result = [{'role': 'user' if isinstance(message, HumanMessage) else ( result = [{'role': 'user' if isinstance(message, HumanMessage) else 'ai', 'content': message.content} for
'system' if isinstance(message, SystemMessage) else 'ai'), 'content': message.content} for
message message
in in
message_list] message_list]
@ -237,17 +235,16 @@ class BaseChatStep(IChatStep):
paragraph_list=None, paragraph_list=None,
manage: PipelineManage = None, manage: PipelineManage = None,
padding_problem_text: str = None, padding_problem_text: str = None,
chat_user_id=None, chat_user_type=None, client_id=None, client_type=None,
no_references_setting=None, no_references_setting=None,
model_setting=None): model_setting=None):
chat_result, is_ai_chat = self.get_stream_result(message_list, chat_model, paragraph_list, chat_result, is_ai_chat = self.get_stream_result(message_list, chat_model, paragraph_list,
no_references_setting, problem_text) no_references_setting, problem_text)
chat_record_id = uuid.uuid7() chat_record_id = uuid.uuid1()
r = StreamingHttpResponse( r = StreamingHttpResponse(
streaming_content=event_content(chat_result, chat_id, chat_record_id, paragraph_list, streaming_content=event_content(chat_result, chat_id, chat_record_id, paragraph_list,
post_response_handler, manage, self, chat_model, message_list, problem_text, post_response_handler, manage, self, chat_model, message_list, problem_text,
padding_problem_text, chat_user_id, chat_user_type, is_ai_chat, padding_problem_text, client_id, client_type, is_ai_chat, model_setting),
model_setting),
content_type='text/event-stream;charset=utf-8') content_type='text/event-stream;charset=utf-8')
r['Cache-Control'] = 'no-cache' r['Cache-Control'] = 'no-cache'
@ -283,14 +280,14 @@ class BaseChatStep(IChatStep):
paragraph_list=None, paragraph_list=None,
manage: PipelineManage = None, manage: PipelineManage = None,
padding_problem_text: str = None, padding_problem_text: str = None,
chat_user_id=None, chat_user_type=None, no_references_setting=None, client_id=None, client_type=None, no_references_setting=None,
model_setting=None): model_setting=None):
reasoning_content_enable = model_setting.get('reasoning_content_enable', False) reasoning_content_enable = model_setting.get('reasoning_content_enable', False)
reasoning_content_start = model_setting.get('reasoning_content_start', '<think>') reasoning_content_start = model_setting.get('reasoning_content_start', '<think>')
reasoning_content_end = model_setting.get('reasoning_content_end', '</think>') reasoning_content_end = model_setting.get('reasoning_content_end', '</think>')
reasoning = Reasoning(reasoning_content_start, reasoning = Reasoning(reasoning_content_start,
reasoning_content_end) reasoning_content_end)
chat_record_id = uuid.uuid7() chat_record_id = uuid.uuid1()
# 调用模型 # 调用模型
try: try:
chat_result, is_ai_chat = self.get_block_result(message_list, chat_model, paragraph_list, chat_result, is_ai_chat = self.get_block_result(message_list, chat_model, paragraph_list,
@ -310,11 +307,12 @@ class BaseChatStep(IChatStep):
else: else:
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get( reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get(
'reasoning_content') 'reasoning_content')
asker = manage.context.get('form_data', {}).get('asker', None)
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text, post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
content, manage, self, padding_problem_text, content, manage, self, padding_problem_text, client_id,
reasoning_content=reasoning_content) reasoning_content=reasoning_content if reasoning_content_enable else '',
if not manage.debug: asker=asker)
add_access_num(chat_user_id, chat_user_type, manage.context.get('application_id')) add_access_num(client_id, client_type, manage.context.get('application_id'))
return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id), return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id),
content, True, content, True,
request_token, response_token, request_token, response_token,
@ -327,9 +325,10 @@ class BaseChatStep(IChatStep):
except Exception as e: except Exception as e:
all_text = 'Exception:' + str(e) all_text = 'Exception:' + str(e)
write_context(self, manage, 0, 0, all_text) write_context(self, manage, 0, 0, all_text)
asker = manage.context.get('form_data', {}).get('asker', None)
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text, post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
all_text, manage, self, padding_problem_text, reasoning_content='') all_text, manage, self, padding_problem_text, client_id, reasoning_content='',
if not manage.debug: asker=asker)
add_access_num(chat_user_id, chat_user_type, manage.context.get('application_id')) add_access_num(client_id, client_type, manage.context.get('application_id'))
return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id), all_text, True, 0, return manage.get_base_to_response().to_block_response(str(chat_id), str(chat_record_id), all_text, True, 0,
0, _status=status.HTTP_500_INTERNAL_SERVER_ERROR) 0, _status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -16,35 +16,34 @@ from rest_framework import serializers
from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
from application.chat_pipeline.pipeline_manage import PipelineManage from application.chat_pipeline.pipeline_manage import PipelineManage
from application.models import ChatRecord from application.models import ChatRecord
from application.serializers.application import NoReferencesSetting from application.serializers.application_serializers import NoReferencesSetting
from common.field.common import InstanceField from common.field.common import InstanceField
from common.util.field_message import ErrMessage
class IGenerateHumanMessageStep(IBaseChatPipelineStep): class IGenerateHumanMessageStep(IBaseChatPipelineStep):
class InstanceSerializer(serializers.Serializer): class InstanceSerializer(serializers.Serializer):
# 问题 # 问题
problem_text = serializers.CharField(required=True, label=_("question")) problem_text = serializers.CharField(required=True, error_messages=ErrMessage.char(_("question")))
# 段落列表 # 段落列表
paragraph_list = serializers.ListField(child=InstanceField(model_type=ParagraphPipelineModel, required=True), paragraph_list = serializers.ListField(child=InstanceField(model_type=ParagraphPipelineModel, required=True),
label=_("Paragraph List")) error_messages=ErrMessage.list(_("Paragraph List")))
# 历史对答 # 历史对答
history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True), history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
label=_("History Questions")) error_messages=ErrMessage.list(_("History Questions")))
# 多轮对话数量 # 多轮对话数量
dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations")) dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
# 最大携带知识库段落长度 # 最大携带知识库段落长度
max_paragraph_char_number = serializers.IntegerField(required=True, max_paragraph_char_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(
label=_("Maximum length of the knowledge base paragraph")) _("Maximum length of the knowledge base paragraph")))
# 模板 # 模板
prompt = serializers.CharField(required=True, label=_("Prompt word")) prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
system = serializers.CharField(required=False, allow_null=True, allow_blank=True, system = serializers.CharField(required=False, allow_null=True, allow_blank=True,
label=_("System prompt words (role)")) error_messages=ErrMessage.char(_("System prompt words (role)")))
# 补齐问题 # 补齐问题
padding_problem_text = serializers.CharField(required=False, padding_problem_text = serializers.CharField(required=False, error_messages=ErrMessage.char(_("Completion problem")))
label=_("Completion problem"))
# 未查询到引用分段 # 未查询到引用分段
no_references_setting = NoReferencesSetting(required=True, no_references_setting = NoReferencesSetting(required=True, error_messages=ErrMessage.base(_("No reference segment settings")))
label=_("No reference segment settings"))
def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]: def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
return self.InstanceSerializer return self.InstanceSerializer

View File

@ -15,7 +15,7 @@ from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineMode
from application.chat_pipeline.step.generate_human_message_step.i_generate_human_message_step import \ from application.chat_pipeline.step.generate_human_message_step.i_generate_human_message_step import \
IGenerateHumanMessageStep IGenerateHumanMessageStep
from application.models import ChatRecord from application.models import ChatRecord
from common.utils.common import flat_map from common.util.split_model import flat_map
class BaseGenerateHumanMessageStep(IGenerateHumanMessageStep): class BaseGenerateHumanMessageStep(IGenerateHumanMessageStep):

View File

@ -16,20 +16,22 @@ from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep
from application.chat_pipeline.pipeline_manage import PipelineManage from application.chat_pipeline.pipeline_manage import PipelineManage
from application.models import ChatRecord from application.models import ChatRecord
from common.field.common import InstanceField from common.field.common import InstanceField
from common.util.field_message import ErrMessage
class IResetProblemStep(IBaseChatPipelineStep): class IResetProblemStep(IBaseChatPipelineStep):
class InstanceSerializer(serializers.Serializer): class InstanceSerializer(serializers.Serializer):
# 问题文本 # 问题文本
problem_text = serializers.CharField(required=True, label=_("question")) problem_text = serializers.CharField(required=True, error_messages=ErrMessage.float(_("question")))
# 历史对答 # 历史对答
history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True), history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
label=_("History Questions")) error_messages=ErrMessage.list(_("History Questions")))
# 大语言模型 # 大语言模型
model_id = serializers.UUIDField(required=False, allow_null=True, label=_("Model id")) model_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid(_("Model id")))
workspace_id = serializers.CharField(required=True, label=_("User ID")) user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
problem_optimization_prompt = serializers.CharField(required=False, max_length=102400, problem_optimization_prompt = serializers.CharField(required=False, max_length=102400,
label=_("Question completion prompt")) error_messages=ErrMessage.char(
_("Question completion prompt")))
def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]: def get_step_serializer(self, manage: PipelineManage) -> Type[serializers.Serializer]:
return self.InstanceSerializer return self.InstanceSerializer
@ -50,6 +52,6 @@ class IResetProblemStep(IBaseChatPipelineStep):
@abstractmethod @abstractmethod
def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None, def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None,
problem_optimization_prompt=None, problem_optimization_prompt=None,
workspace_id=None, user_id=None,
**kwargs): **kwargs):
pass pass

View File

@ -13,8 +13,8 @@ from langchain.schema import HumanMessage
from application.chat_pipeline.step.reset_problem_step.i_reset_problem_step import IResetProblemStep from application.chat_pipeline.step.reset_problem_step.i_reset_problem_step import IResetProblemStep
from application.models import ChatRecord from application.models import ChatRecord
from common.utils.split_model import flat_map from common.util.split_model import flat_map
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
prompt = _( prompt = _(
"() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the <data></data> tag") "() contains the user's question. Answer the guessed user's question based on the context ({question}) Requirement: Output a complete question and put it in the <data></data> tag")
@ -23,9 +23,9 @@ prompt = _(
class BaseResetProblemStep(IResetProblemStep): class BaseResetProblemStep(IResetProblemStep):
def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None, def execute(self, problem_text: str, history_chat_record: List[ChatRecord] = None, model_id: str = None,
problem_optimization_prompt=None, problem_optimization_prompt=None,
workspace_id=None, user_id=None,
**kwargs) -> str: **kwargs) -> str:
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id) if model_id is not None else None chat_model = get_model_instance_by_model_user_id(model_id, user_id) if model_id is not None else None
if chat_model is None: if chat_model is None:
return problem_text return problem_text
start_index = len(history_chat_record) - 3 start_index = len(history_chat_record) - 3

View File

@ -16,62 +16,62 @@ from rest_framework import serializers
from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel from application.chat_pipeline.I_base_chat_pipeline import IBaseChatPipelineStep, ParagraphPipelineModel
from application.chat_pipeline.pipeline_manage import PipelineManage from application.chat_pipeline.pipeline_manage import PipelineManage
from common.util.field_message import ErrMessage
class ISearchDatasetStep(IBaseChatPipelineStep): class ISearchDatasetStep(IBaseChatPipelineStep):
class InstanceSerializer(serializers.Serializer): class InstanceSerializer(serializers.Serializer):
# 原始问题文本 # 原始问题文本
problem_text = serializers.CharField(required=True, label=_("question")) problem_text = serializers.CharField(required=True, error_messages=ErrMessage.char(_("question")))
# 系统补全问题文本 # 系统补全问题文本
padding_problem_text = serializers.CharField(required=False, padding_problem_text = serializers.CharField(required=False,
label=_("System completes question text")) error_messages=ErrMessage.char(_("System completes question text")))
# 需要查询的数据集id列表 # 需要查询的数据集id列表
knowledge_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True), dataset_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
label=_("Dataset id list")) error_messages=ErrMessage.list(_("Dataset id list")))
# 需要排除的文档id # 需要排除的文档id
exclude_document_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True), exclude_document_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
label=_("List of document ids to exclude")) error_messages=ErrMessage.list(_("List of document ids to exclude")))
# 需要排除向量id # 需要排除向量id
exclude_paragraph_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True), exclude_paragraph_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
label=_("List of exclusion vector ids")) error_messages=ErrMessage.list(_("List of exclusion vector ids")))
# 需要查询的条数 # 需要查询的条数
top_n = serializers.IntegerField(required=True, top_n = serializers.IntegerField(required=True,
label=_("Reference segment number")) error_messages=ErrMessage.integer(_("Reference segment number")))
# 相似度 0-1之间 # 相似度 0-1之间
similarity = serializers.FloatField(required=True, max_value=1, min_value=0, similarity = serializers.FloatField(required=True, max_value=1, min_value=0,
label=_("Similarity")) error_messages=ErrMessage.float(_("Similarity")))
search_mode = serializers.CharField(required=True, validators=[ search_mode = serializers.CharField(required=True, validators=[
validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"), validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"),
message=_("The type only supports embedding|keywords|blend"), code=500) message=_("The type only supports embedding|keywords|blend"), code=500)
], label=_("Retrieval Mode")) ], error_messages=ErrMessage.char(_("Retrieval Mode")))
workspace_id = serializers.CharField(required=True, label=_("Workspace ID")) user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
def get_step_serializer(self, manage: PipelineManage) -> Type[InstanceSerializer]: def get_step_serializer(self, manage: PipelineManage) -> Type[InstanceSerializer]:
return self.InstanceSerializer return self.InstanceSerializer
def _run(self, manage: PipelineManage): def _run(self, manage: PipelineManage):
paragraph_list = self.execute(**self.context['step_args'], manage=manage) paragraph_list = self.execute(**self.context['step_args'])
manage.context['paragraph_list'] = paragraph_list manage.context['paragraph_list'] = paragraph_list
self.context['paragraph_list'] = paragraph_list self.context['paragraph_list'] = paragraph_list
@abstractmethod @abstractmethod
def execute(self, problem_text: str, knowledge_id_list: list[str], exclude_document_id_list: list[str], def execute(self, problem_text: str, dataset_id_list: list[str], exclude_document_id_list: list[str],
exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None, exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None,
search_mode: str = None, search_mode: str = None,
workspace_id=None, user_id=None,
manage: PipelineManage = None,
**kwargs) -> List[ParagraphPipelineModel]: **kwargs) -> List[ParagraphPipelineModel]:
""" """
关于 用户和补全问题 说明: 补全问题如果有就使用补全问题去查询 反之就用用户原始问题查询 关于 用户和补全问题 说明: 补全问题如果有就使用补全问题去查询 反之就用用户原始问题查询
:param similarity: 相关性 :param similarity: 相关性
:param top_n: 查询多少条 :param top_n: 查询多少条
:param problem_text: 用户问题 :param problem_text: 用户问题
:param knowledge_id_list: 需要查询的数据集id列表 :param dataset_id_list: 需要查询的数据集id列表
:param exclude_document_id_list: 需要排除的文档id :param exclude_document_id_list: 需要排除的文档id
:param exclude_paragraph_id_list: 需要排除段落id :param exclude_paragraph_id_list: 需要排除段落id
:param padding_problem_text 补全问题 :param padding_problem_text 补全问题
:param search_mode 检索模式 :param search_mode 检索模式
:param workspace_id 工作空间id :param user_id 用户id
:return: 段落列表 :return: 段落列表
""" """
pass pass

View File

@ -16,52 +16,51 @@ from rest_framework.utils.formatting import lazy_format
from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel from application.chat_pipeline.I_base_chat_pipeline import ParagraphPipelineModel
from application.chat_pipeline.step.search_dataset_step.i_search_dataset_step import ISearchDatasetStep from application.chat_pipeline.step.search_dataset_step.i_search_dataset_step import ISearchDatasetStep
from common.config.embedding_config import VectorStore, ModelManage from common.config.embedding_config import VectorStore, ModelManage
from common.constants.permission_constants import RoleConstants
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.db.search import native_search from common.db.search import native_search
from common.utils.common import get_file_content from common.util.file_util import get_file_content
from knowledge.models import Paragraph, Knowledge from dataset.models import Paragraph, DataSet
from knowledge.models import SearchMode from embedding.models import SearchMode
from maxkb.conf import PROJECT_DIR from setting.models import Model
from models_provider.models import Model from setting.models_provider import get_model
from models_provider.tools import get_model, get_model_by_id from smartdoc.conf import PROJECT_DIR
def get_embedding_id(knowledge_id_list): def get_model_by_id(_id, user_id):
knowledge_list = QuerySet(Knowledge).filter(id__in=knowledge_id_list) model = QuerySet(Model).filter(id=_id).first()
if len(set([knowledge.embedding_model_id for knowledge in knowledge_list])) > 1: if model is None:
raise Exception( raise Exception(_("Model does not exist"))
_("The vector model of the associated knowledge base is inconsistent and the segmentation cannot be recalled.")) if model.permission_type == 'PRIVATE' and str(model.user_id) != str(user_id):
if len(knowledge_list) == 0: message = lazy_format(_('No permission to use this model {model_name}'), model_name=model.name)
raise Exception(message)
return model
def get_embedding_id(dataset_id_list):
dataset_list = QuerySet(DataSet).filter(id__in=dataset_id_list)
if len(set([dataset.embedding_mode_id for dataset in dataset_list])) > 1:
raise Exception(_("The vector model of the associated knowledge base is inconsistent and the segmentation cannot be recalled."))
if len(dataset_list) == 0:
raise Exception(_("The knowledge base setting is wrong, please reset the knowledge base")) raise Exception(_("The knowledge base setting is wrong, please reset the knowledge base"))
return knowledge_list[0].embedding_model_id return dataset_list[0].embedding_mode_id
class BaseSearchDatasetStep(ISearchDatasetStep): class BaseSearchDatasetStep(ISearchDatasetStep):
def execute(self, problem_text: str, knowledge_id_list: list[str], exclude_document_id_list: list[str], def execute(self, problem_text: str, dataset_id_list: list[str], exclude_document_id_list: list[str],
exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None, exclude_paragraph_id_list: list[str], top_n: int, similarity: float, padding_problem_text: str = None,
search_mode: str = None, search_mode: str = None,
workspace_id=None, user_id=None,
manage=None,
**kwargs) -> List[ParagraphPipelineModel]: **kwargs) -> List[ParagraphPipelineModel]:
get_knowledge_list_of_authorized = DatabaseModelManage.get_model('get_knowledge_list_of_authorized') if len(dataset_id_list) == 0:
chat_user_type = manage.context.get('chat_user_type')
if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
knowledge_id_list = get_knowledge_list_of_authorized(manage.context.get('chat_user_id'),
knowledge_id_list)
if len(knowledge_id_list) == 0:
return [] return []
exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text exec_problem_text = padding_problem_text if padding_problem_text is not None else problem_text
model_id = get_embedding_id(knowledge_id_list) model_id = get_embedding_id(dataset_id_list)
model = get_model_by_id(model_id, workspace_id) model = get_model_by_id(model_id, user_id)
if model.model_type != "EMBEDDING":
raise Exception(_("Model does not exist"))
self.context['model_name'] = model.name self.context['model_name'] = model.name
embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model)) embedding_model = ModelManage.get_model(model_id, lambda _id: get_model(model))
embedding_value = embedding_model.embed_query(exec_problem_text) embedding_value = embedding_model.embed_query(exec_problem_text)
vector = VectorStore.get_embedding_vector() vector = VectorStore.get_embedding_vector()
embedding_list = vector.query(exec_problem_text, embedding_value, knowledge_id_list, exclude_document_id_list, embedding_list = vector.query(exec_problem_text, embedding_value, dataset_id_list, exclude_document_id_list,
exclude_paragraph_id_list, True, top_n, similarity, SearchMode(search_mode)) exclude_paragraph_id_list, True, top_n, similarity, SearchMode(search_mode))
if embedding_list is None: if embedding_list is None:
return [] return []
@ -79,8 +78,7 @@ class BaseSearchDatasetStep(ISearchDatasetStep):
.add_paragraph(paragraph) .add_paragraph(paragraph)
.add_similarity(find_embedding.get('similarity')) .add_similarity(find_embedding.get('similarity'))
.add_comprehensive_score(find_embedding.get('comprehensive_score')) .add_comprehensive_score(find_embedding.get('comprehensive_score'))
.add_knowledge_name(paragraph.get('knowledge_name')) .add_dataset_name(paragraph.get('dataset_name'))
.add_knowledge_type(paragraph.get('knowledge_type'))
.add_document_name(paragraph.get('document_name')) .add_document_name(paragraph.get('document_name'))
.add_hit_handling_method(paragraph.get('hit_handling_method')) .add_hit_handling_method(paragraph.get('hit_handling_method'))
.add_directly_return_similarity(paragraph.get('directly_return_similarity')) .add_directly_return_similarity(paragraph.get('directly_return_similarity'))
@ -104,7 +102,7 @@ class BaseSearchDatasetStep(ISearchDatasetStep):
paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list), paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list),
get_file_content( get_file_content(
os.path.join(PROJECT_DIR, "apps", "application", 'sql', os.path.join(PROJECT_DIR, "apps", "application", 'sql',
'list_knowledge_paragraph_by_paragraph_id.sql')), 'list_dataset_paragraph_by_paragraph_id.sql')),
with_table_name=True) with_table_name=True)
# 如果向量库中存在脏数据 直接删除 # 如果向量库中存在脏数据 直接删除
if len(paragraph_list) != len(paragraph_id_list): if len(paragraph_list) != len(paragraph_id_list):

View File

@ -7,21 +7,6 @@
@desc: @desc:
""" """
from typing import List, Dict
from django.db.models import QuerySet
from django.utils.translation import gettext as _
from rest_framework.exceptions import ErrorDetail, ValidationError
from common.exception.app_exception import AppApiException
from common.utils.common import group_by
from models_provider.models import Model
from models_provider.tools import get_model_credential
from tools.models.tool import Tool
end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node', 'application-node',
'image-understand-node', 'speech-to-text-node', 'text-to-speech-node', 'image-generate-node']
class Answer: class Answer:
def __init__(self, content, view_type, runtime_node_id, chat_record_id, child_node, real_node_id, def __init__(self, content, view_type, runtime_node_id, chat_record_id, child_node, real_node_id,
@ -57,207 +42,3 @@ class NodeChunk:
def is_end(self): def is_end(self):
return self.status == 200 return self.status == 200
class Edge:
def __init__(self, _id: str, _type: str, sourceNodeId: str, targetNodeId: str, **keywords):
self.id = _id
self.type = _type
self.sourceNodeId = sourceNodeId
self.targetNodeId = targetNodeId
for keyword in keywords:
self.__setattr__(keyword, keywords.get(keyword))
class Node:
def __init__(self, _id: str, _type: str, x: int, y: int, properties: dict, **kwargs):
self.id = _id
self.type = _type
self.x = x
self.y = y
self.properties = properties
for keyword in kwargs:
self.__setattr__(keyword, kwargs.get(keyword))
class EdgeNode:
edge: Edge
node: Node
def __init__(self, edge, node):
self.edge = edge
self.node = node
class Workflow:
"""
节点列表
"""
nodes: List[Node]
"""
线列表
"""
edges: List[Edge]
"""
节点id:node
"""
node_map: Dict[str, Node]
"""
节点id:当前节点id上面的所有节点
"""
up_node_map: Dict[str, List[EdgeNode]]
"""
节点id:当前节点id下面的所有节点
"""
next_node_map: Dict[str, List[EdgeNode]]
def __init__(self, nodes: List[Node], edges: List[Edge]):
self.nodes = nodes
self.edges = edges
self.node_map = {node.id: node for node in nodes}
self.up_node_map = {key: [EdgeNode(edge, self.node_map.get(edge.sourceNodeId)) for
edge in edges] for
key, edges in
group_by(edges, key=lambda edge: edge.targetNodeId).items()}
self.next_node_map = {key: [EdgeNode(edge, self.node_map.get(edge.targetNodeId)) for edge in edges] for
key, edges in
group_by(edges, key=lambda edge: edge.sourceNodeId).items()}
def get_node(self, node_id):
"""
根据node_id 获取节点信息
@param node_id: node_id
@return: 节点信息
"""
return self.node_map.get(node_id)
def get_up_edge_nodes(self, node_id) -> List[EdgeNode]:
"""
根据节点id 获取当前连接前置节点和连线
@param node_id: 节点id
@return: 节点连线列表
"""
return self.up_node_map.get(node_id)
def get_next_edge_nodes(self, node_id) -> List[EdgeNode]:
"""
根据节点id 获取当前连接目标节点和连线
@param node_id: 节点id
@return: 节点连线列表
"""
return self.next_node_map.get(node_id)
def get_up_nodes(self, node_id) -> List[Node]:
"""
根据节点id 获取当前连接前置节点
@param node_id: 节点id
@return: 节点列表
"""
return [en.node for en in self.up_node_map.get(node_id)]
def get_next_nodes(self, node_id) -> List[Node]:
"""
根据节点id 获取当前连接目标节点
@param node_id: 节点id
@return: 节点列表
"""
return [en.node for en in self.next_node_map.get(node_id, [])]
@staticmethod
def new_instance(flow_obj: Dict):
nodes = flow_obj.get('nodes')
edges = flow_obj.get('edges')
nodes = [Node(node.get('id'), node.get('type'), **node)
for node in nodes]
edges = [Edge(edge.get('id'), edge.get('type'), **edge) for edge in edges]
return Workflow(nodes, edges)
def get_start_node(self):
return self.get_node('start-node')
def get_search_node(self):
return [node for node in self.nodes if node.type == 'search-dataset-node']
def is_valid(self):
"""
校验工作流数据
"""
self.is_valid_model_params()
self.is_valid_start_node()
self.is_valid_base_node()
self.is_valid_work_flow()
@staticmethod
def is_valid_node_params(node: Node):
from application.flow.step_node import get_node
get_node(node.type)(node, None, None)
def is_valid_node(self, node: Node):
self.is_valid_node_params(node)
if node.type == 'condition-node':
branch_list = node.properties.get('node_data').get('branch')
for branch in branch_list:
source_anchor_id = f"{node.id}_{branch.get('id')}_right"
edge_list = [edge for edge in self.edges if edge.sourceAnchorId == source_anchor_id]
if len(edge_list) == 0:
raise AppApiException(500,
_('The branch {branch} of the {node} node needs to be connected').format(
node=node.properties.get("stepName"), branch=branch.get("type")))
else:
edge_list = [edge for edge in self.edges if edge.sourceNodeId == node.id]
if len(edge_list) == 0 and not end_nodes.__contains__(node.type):
raise AppApiException(500, _("{node} Nodes cannot be considered as end nodes").format(
node=node.properties.get("stepName")))
def is_valid_work_flow(self, up_node=None):
if up_node is None:
up_node = self.get_start_node()
self.is_valid_node(up_node)
next_nodes = self.get_next_nodes(up_node)
for next_node in next_nodes:
self.is_valid_work_flow(next_node)
def is_valid_start_node(self):
start_node_list = [node for node in self.nodes if node.id == 'start-node']
if len(start_node_list) == 0:
raise AppApiException(500, _('The starting node is required'))
if len(start_node_list) > 1:
raise AppApiException(500, _('There can only be one starting node'))
def is_valid_model_params(self):
node_list = [node for node in self.nodes if (node.type == 'ai-chat-node' or node.type == 'question-node')]
for node in node_list:
model = QuerySet(Model).filter(id=node.properties.get('node_data', {}).get('model_id')).first()
if model is None:
raise ValidationError(ErrorDetail(
_('The node {node} model does not exist').format(node=node.properties.get("stepName"))))
credential = get_model_credential(model.provider, model.model_type, model.model_name)
model_params_setting = node.properties.get('node_data', {}).get('model_params_setting')
model_params_setting_form = credential.get_model_params_setting_form(
model.model_name)
if model_params_setting is None:
model_params_setting = model_params_setting_form.get_default_form_data()
node.properties.get('node_data', {})['model_params_setting'] = model_params_setting
if node.properties.get('status', 200) != 200:
raise ValidationError(
ErrorDetail(_("Node {node} is unavailable").format(node.properties.get("stepName"))))
node_list = [node for node in self.nodes if (node.type == 'function-lib-node')]
for node in node_list:
function_lib_id = node.properties.get('node_data', {}).get('function_lib_id')
if function_lib_id is None:
raise ValidationError(ErrorDetail(
_('The library ID of node {node} cannot be empty').format(node=node.properties.get("stepName"))))
f_lib = QuerySet(Tool).filter(id=function_lib_id).first()
if f_lib is None:
raise ValidationError(ErrorDetail(_("The function library for node {node} is not available").format(
node=node.properties.get("stepName"))))
def is_valid_base_node(self):
base_node_list = [node for node in self.nodes if node.id == 'base-node']
if len(base_node_list) == 0:
raise AppApiException(500, _('Basic information node is required'))
if len(base_node_list) > 1:
raise AppApiException(500, _('There can only be one basic information node'))

View File

@ -18,11 +18,13 @@ from rest_framework import serializers
from rest_framework.exceptions import ValidationError, ErrorDetail from rest_framework.exceptions import ValidationError, ErrorDetail
from application.flow.common import Answer, NodeChunk from application.flow.common import Answer, NodeChunk
from application.models import ApplicationChatUserStats from application.models import ChatRecord
from application.models import ChatRecord, ChatUserType from application.models.api_key_model import ApplicationPublicAccessClient
from common.constants.authentication_type import AuthenticationType
from common.field.common import InstanceField from common.field.common import InstanceField
from common.util.field_message import ErrMessage
chat_cache = cache chat_cache = cache.caches['chat_cache']
def write_context(step_variable: Dict, global_variable: Dict, node, workflow): def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
@ -44,14 +46,16 @@ def is_interrupt(node, step_variable: Dict, global_variable: Dict):
class WorkFlowPostHandler: class WorkFlowPostHandler:
def __init__(self, chat_info): def __init__(self, chat_info, client_id, client_type):
self.chat_info = chat_info self.chat_info = chat_info
self.client_id = client_id
self.client_type = client_type
def handler(self, workflow): def handler(self, chat_id,
workflow_body = workflow.get_body() chat_record_id,
question = workflow_body.get('question') answer,
chat_record_id = workflow_body.get('chat_record_id') workflow):
chat_id = workflow_body.get('chat_id') question = workflow.params['question']
details = workflow.get_runtime_details() details = workflow.get_runtime_details()
message_tokens = sum([row.get('message_tokens') for row in details.values() if message_tokens = sum([row.get('message_tokens') for row in details.values() if
'message_tokens' in row and row.get('message_tokens') is not None]) 'message_tokens' in row and row.get('message_tokens') is not None])
@ -80,16 +84,15 @@ class WorkFlowPostHandler:
answer_text_list=answer_text_list, answer_text_list=answer_text_list,
run_time=time.time() - workflow.context['start_time'], run_time=time.time() - workflow.context['start_time'],
index=0) index=0)
asker = workflow.context.get('asker', None)
self.chat_info.append_chat_record(chat_record) self.chat_info.append_chat_record(chat_record, self.client_id, asker)
self.chat_info.set_cache() # 重新设置缓存
chat_cache.set(chat_id,
if not self.chat_info.debug and [ChatUserType.ANONYMOUS_USER.value, ChatUserType.CHAT_USER.value].__contains__( self.chat_info, timeout=60 * 30)
workflow_body.get('chat_user_type')): if self.client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value:
application_public_access_client = (QuerySet(ApplicationChatUserStats) application_public_access_client = (QuerySet(ApplicationPublicAccessClient)
.filter(chat_user_id=workflow_body.get('chat_user_id'), .filter(client_id=self.client_id,
chat_user_type=workflow_body.get('chat_user_type'), application_id=self.chat_info.application.id).first())
application_id=self.chat_info.application_id).first())
if application_public_access_client is not None: if application_public_access_client is not None:
application_public_access_client.access_num = application_public_access_client.access_num + 1 application_public_access_client.access_num = application_public_access_client.access_num + 1
application_public_access_client.intraday_access_num = application_public_access_client.intraday_access_num + 1 application_public_access_client.intraday_access_num = application_public_access_client.intraday_access_num + 1
@ -120,36 +123,31 @@ class NodeResult:
class ReferenceAddressSerializer(serializers.Serializer): class ReferenceAddressSerializer(serializers.Serializer):
node_id = serializers.CharField(required=True, label="节点id") node_id = serializers.CharField(required=True, error_messages=ErrMessage.char("节点id"))
fields = serializers.ListField( fields = serializers.ListField(
child=serializers.CharField(required=True, label="节点字段"), required=True, child=serializers.CharField(required=True, error_messages=ErrMessage.char("节点字段")), required=True,
label="节点字段数组") error_messages=ErrMessage.list("节点字段数组"))
class FlowParamsSerializer(serializers.Serializer): class FlowParamsSerializer(serializers.Serializer):
# 历史对答 # 历史对答
history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True), history_chat_record = serializers.ListField(child=InstanceField(model_type=ChatRecord, required=True),
label="历史对答") error_messages=ErrMessage.list("历史对答"))
question = serializers.CharField(required=True, label="用户问题") question = serializers.CharField(required=True, error_messages=ErrMessage.list("用户问题"))
chat_id = serializers.CharField(required=True, label="对话id") chat_id = serializers.CharField(required=True, error_messages=ErrMessage.list("对话id"))
chat_record_id = serializers.CharField(required=True, label="对话记录id") chat_record_id = serializers.CharField(required=True, error_messages=ErrMessage.char("对话记录id"))
stream = serializers.BooleanField(required=True, label="流式输出") stream = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("流式输出"))
chat_user_id = serializers.CharField(required=False, label="对话用户id") client_id = serializers.CharField(required=False, error_messages=ErrMessage.char("客户端id"))
chat_user_type = serializers.CharField(required=False, label="对话用户类型") client_type = serializers.CharField(required=False, error_messages=ErrMessage.char("客户端类型"))
workspace_id = serializers.CharField(required=True, label="工作空间id") user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("用户id"))
re_chat = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean("换个答案"))
application_id = serializers.CharField(required=True, label="应用id")
re_chat = serializers.BooleanField(required=True, label="换个答案")
debug = serializers.BooleanField(required=True, label="是否debug")
class INode: class INode:

View File

@ -10,24 +10,26 @@ from .ai_chat_step_node import *
from .application_node import BaseApplicationNode from .application_node import BaseApplicationNode
from .condition_node import * from .condition_node import *
from .direct_reply_node import * from .direct_reply_node import *
from .document_extract_node import *
from .form_node import * from .form_node import *
from .image_generate_step_node import * from .function_lib_node import *
from .image_understand_step_node import * from .function_node import *
from .mcp_node import BaseMcpNode
from .question_node import * from .question_node import *
from .reranker_node import * from .reranker_node import *
from .search_knowledge_node import *
from .document_extract_node import *
from .image_understand_step_node import *
from .image_generate_step_node import *
from .search_dataset_node import *
from .speech_to_text_step_node import BaseSpeechToTextNode from .speech_to_text_step_node import BaseSpeechToTextNode
from .start_node import * from .start_node import *
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
from .tool_lib_node import *
from .tool_node import *
from .variable_assign_node import BaseVariableAssignNode from .variable_assign_node import BaseVariableAssignNode
from .mcp_node import BaseMcpNode
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuestionNode, node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode, BaseConditionNode, BaseReplyNode,
BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode, BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
BaseDocumentExtractNode, BaseDocumentExtractNode,
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode, BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode] BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]

View File

@ -12,28 +12,30 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
class ChatNodeSerializer(serializers.Serializer): class ChatNodeSerializer(serializers.Serializer):
model_id = serializers.CharField(required=True, label=_("Model id")) model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
system = serializers.CharField(required=False, allow_blank=True, allow_null=True, system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
label=_("Role Setting")) error_messages=ErrMessage.char(_("Role Setting")))
prompt = serializers.CharField(required=True, label=_("Prompt word")) prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
# 多轮对话数量 # 多轮对话数量
dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations")) dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(
_("Number of multi-round conversations")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False,
label=_('Whether to return content')) error_messages=ErrMessage.boolean(_('Whether to return content')))
model_params_setting = serializers.DictField(required=False, model_params_setting = serializers.DictField(required=False,
label=_("Model parameter settings")) error_messages=ErrMessage.dict(_("Model parameter settings")))
model_setting = serializers.DictField(required=False, model_setting = serializers.DictField(required=False,
label='Model settings') error_messages=ErrMessage.dict('Model settings'))
dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True, dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True,
label=_("Context Type")) error_messages=ErrMessage.char(_("Context Type")))
mcp_enable = serializers.BooleanField(required=False, mcp_enable = serializers.BooleanField(required=False,
label=_("Whether to enable MCP")) error_messages=ErrMessage.boolean(_("Whether to enable MCP")))
mcp_servers = serializers.JSONField(required=False, label=_("MCP Server")) mcp_servers = serializers.JSONField(required=False, error_messages=ErrMessage.list(_("MCP Server")))
class IChatNode(INode): class IChatNode(INode):

View File

@ -8,10 +8,10 @@
""" """
import asyncio import asyncio
import json import json
import logging
import re import re
import time import time
from functools import reduce from functools import reduce
from types import AsyncGeneratorType
from typing import List, Dict from typing import List, Dict
from django.db.models import QuerySet from django.db.models import QuerySet
@ -23,9 +23,9 @@ from langgraph.prebuilt import create_react_agent
from application.flow.i_step_node import NodeResult, INode from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
from application.flow.tools import Reasoning from application.flow.tools import Reasoning
from models_provider.models import Model from setting.models import Model
from models_provider.tools import get_model_credential, get_model_instance_by_model_workspace_id from setting.models_provider import get_model_credential
from common.utils.logger import maxkb_logger from setting.models_provider.tools import get_model_instance_by_model_user_id
tool_message_template = """ tool_message_template = """
<details> <details>
@ -33,26 +33,14 @@ tool_message_template = """
<strong>Called MCP Tool: <em>%s</em></strong> <strong>Called MCP Tool: <em>%s</em></strong>
</summary> </summary>
%s
</details>
"""
tool_message_json_template = """
```json ```json
%s %s
``` ```
</details>
""" """
def generate_tool_message_template(name, context):
if '```' in context:
return tool_message_template % (name, context)
else:
return tool_message_template % (name, tool_message_json_template % (context))
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str, def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
reasoning_content: str): reasoning_content: str):
chat_model = node_variable.get('chat_model') chat_model = node_variable.get('chat_model')
@ -116,17 +104,16 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
async def _yield_mcp_response(chat_model, message_list, mcp_servers): async def _yield_mcp_response(chat_model, message_list, mcp_servers):
client = MultiServerMCPClient(json.loads(mcp_servers)) async with MultiServerMCPClient(json.loads(mcp_servers)) as client:
tools = await client.get_tools() agent = create_react_agent(chat_model, client.get_tools())
agent = create_react_agent(chat_model, tools) response = agent.astream({"messages": message_list}, stream_mode='messages')
response = agent.astream({"messages": message_list}, stream_mode='messages') async for chunk in response:
async for chunk in response: if isinstance(chunk[0], ToolMessage):
if isinstance(chunk[0], ToolMessage): content = tool_message_template % (chunk[0].name, chunk[0].content)
content = generate_tool_message_template(chunk[0].name, chunk[0].content) chunk[0].content = content
chunk[0].content = content yield chunk[0]
yield chunk[0] if isinstance(chunk[0], AIMessageChunk):
if isinstance(chunk[0], AIMessageChunk): yield chunk[0]
yield chunk[0]
def mcp_response_generator(chat_model, message_list, mcp_servers): def mcp_response_generator(chat_model, message_list, mcp_servers):
@ -140,7 +127,7 @@ def mcp_response_generator(chat_model, message_list, mcp_servers):
except StopAsyncIteration: except StopAsyncIteration:
break break
except Exception as e: except Exception as e:
maxkb_logger.error(f'Exception: {e}') print(f'exception: {e}')
finally: finally:
loop.close() loop.close()
@ -165,9 +152,8 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
reasoning_result = reasoning.get_reasoning_content(response) reasoning_result = reasoning.get_reasoning_content(response)
reasoning_result_end = reasoning.get_end_reasoning_content() reasoning_result_end = reasoning.get_end_reasoning_content()
content = reasoning_result.get('content') + reasoning_result_end.get('content') content = reasoning_result.get('content') + reasoning_result_end.get('content')
meta = {**response.response_metadata, **response.additional_kwargs} if 'reasoning_content' in response.response_metadata:
if 'reasoning_content' in meta: reasoning_content = response.response_metadata.get('reasoning_content', '')
reasoning_content = meta.get('reasoning_content', '')
else: else:
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get('reasoning_content') reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get('reasoning_content')
_write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content) _write_context(node_variable, workflow_variable, node, workflow, content, reasoning_content)
@ -221,9 +207,8 @@ class BaseChatNode(IChatNode):
model_setting = {'reasoning_content_enable': False, 'reasoning_content_end': '</think>', model_setting = {'reasoning_content_enable': False, 'reasoning_content_end': '</think>',
'reasoning_content_start': '<think>'} 'reasoning_content_start': '<think>'}
self.context['model_setting'] = model_setting self.context['model_setting'] = model_setting
workspace_id = self.workflow_manage.get_body().get('workspace_id') chat_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **model_params_setting)
**model_params_setting)
history_message = self.get_history_message(history_chat_record, dialogue_number, dialogue_type, history_message = self.get_history_message(history_chat_record, dialogue_number, dialogue_type,
self.runtime_node_id) self.runtime_node_id)
self.context['history_message'] = history_message self.context['history_message'] = history_message

View File

@ -4,23 +4,24 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class ApplicationNodeSerializer(serializers.Serializer): class ApplicationNodeSerializer(serializers.Serializer):
application_id = serializers.CharField(required=True, label=_("Application ID")) application_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Application ID")))
question_reference_address = serializers.ListField(required=True, question_reference_address = serializers.ListField(required=True,
label=_("User Questions")) error_messages=ErrMessage.list(_("User Questions")))
api_input_field_list = serializers.ListField(required=False, label=_("API Input Fields")) api_input_field_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("API Input Fields")))
user_input_field_list = serializers.ListField(required=False, user_input_field_list = serializers.ListField(required=False,
label=_("User Input Fields")) error_messages=ErrMessage.uuid(_("User Input Fields")))
image_list = serializers.ListField(required=False, label=_("picture")) image_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("picture")))
document_list = serializers.ListField(required=False, label=_("document")) document_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("document")))
audio_list = serializers.ListField(required=False, label=_("Audio")) audio_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Audio")))
child_node = serializers.DictField(required=False, allow_null=True, child_node = serializers.DictField(required=False, allow_null=True,
label=_("Child Nodes")) error_messages=ErrMessage.dict(_("Child Nodes")))
node_data = serializers.DictField(required=False, allow_null=True, label=_("Form Data")) node_data = serializers.DictField(required=False, allow_null=True, error_messages=ErrMessage.dict(_("Form Data")))
class IApplicationNode(INode): class IApplicationNode(INode):
@ -74,7 +75,7 @@ class IApplicationNode(INode):
if 'file_id' not in audio: if 'file_id' not in audio:
raise ValueError( raise ValueError(
_("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails.")) _("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails."))
return self.execute(**{**self.flow_params_serializer.data, **self.node_params_serializer.data}, return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data,
app_document_list=app_document_list, app_image_list=app_image_list, app_document_list=app_document_list, app_image_list=app_image_list,
app_audio_list=app_audio_list, app_audio_list=app_audio_list,
message=str(question), **kwargs) message=str(question), **kwargs)

View File

@ -4,7 +4,7 @@ import re
import time import time
import uuid import uuid
from typing import Dict, List from typing import Dict, List
from django.utils.translation import gettext as _
from application.flow.common import Answer from application.flow.common import Answer
from application.flow.i_step_node import NodeResult, INode from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.application_node.i_application_node import IApplicationNode from application.flow.step_node.application_node.i_application_node import IApplicationNode
@ -171,21 +171,16 @@ class BaseApplicationNode(IApplicationNode):
if self.node_params.get('is_result', False): if self.node_params.get('is_result', False):
self.answer_text = details.get('answer') self.answer_text = details.get('answer')
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
chat_user_id,
chat_user_type,
app_document_list=None, app_image_list=None, app_audio_list=None, child_node=None, node_data=None, app_document_list=None, app_image_list=None, app_audio_list=None, child_node=None, node_data=None,
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
from chat.serializers.chat import ChatSerializers from application.serializers.chat_message_serializers import ChatMessageSerializer
if application_id == self.workflow_manage.get_body().get('application_id'):
raise Exception(_("The sub application cannot use the current node"))
# 生成嵌入应用的chat_id # 生成嵌入应用的chat_id
current_chat_id = string_to_uuid(chat_id + application_id) current_chat_id = string_to_uuid(chat_id + application_id)
Chat.objects.get_or_create(id=current_chat_id, defaults={ Chat.objects.get_or_create(id=current_chat_id, defaults={
'application_id': application_id, 'application_id': application_id,
'abstract': message[0:1024], 'abstract': message[0:1024],
'chat_user_id': chat_user_id, 'client_id': client_id,
'chat_user_type': chat_user_type
}) })
if app_document_list is None: if app_document_list is None:
app_document_list = [] app_document_list = []
@ -202,26 +197,22 @@ class BaseApplicationNode(IApplicationNode):
child_node_value = child_node.get('child_node') child_node_value = child_node.get('child_node')
application_node_dict = self.context.get('application_node_dict') application_node_dict = self.context.get('application_node_dict')
reset_application_node_dict(application_node_dict, runtime_node_id, node_data) reset_application_node_dict(application_node_dict, runtime_node_id, node_data)
response = ChatSerializers(data={
"chat_id": current_chat_id,
"chat_user_id": chat_user_id,
'chat_user_type': chat_user_type,
'application_id': application_id,
'debug': False
}).chat(instance=
{'message': message,
're_chat': re_chat,
'stream': stream,
'document_list': app_document_list,
'image_list': app_image_list,
'audio_list': app_audio_list,
'runtime_node_id': runtime_node_id,
'chat_record_id': record_id,
'child_node': child_node_value,
'node_data': node_data,
'form_data': kwargs}
)
response = ChatMessageSerializer(
data={'chat_id': current_chat_id, 'message': message,
're_chat': re_chat,
'stream': stream,
'application_id': application_id,
'client_id': client_id,
'client_type': client_type,
'document_list': app_document_list,
'image_list': app_image_list,
'audio_list': app_audio_list,
'runtime_node_id': runtime_node_id,
'chat_record_id': record_id,
'child_node': child_node_value,
'node_data': node_data,
'form_data': kwargs}).chat()
if response.status_code == 200: if response.status_code == 200:
if stream: if stream:
content_generator = response.streaming_content content_generator = response.streaming_content

View File

@ -12,18 +12,19 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode from application.flow.i_step_node import INode
from common.util.field_message import ErrMessage
class ConditionSerializer(serializers.Serializer): class ConditionSerializer(serializers.Serializer):
compare = serializers.CharField(required=True, label=_("Comparator")) compare = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Comparator")))
value = serializers.CharField(required=True, label=_("value")) value = serializers.CharField(required=True, error_messages=ErrMessage.char(_("value")))
field = serializers.ListField(required=True, label=_("Fields")) field = serializers.ListField(required=True, error_messages=ErrMessage.char(_("Fields")))
class ConditionBranchSerializer(serializers.Serializer): class ConditionBranchSerializer(serializers.Serializer):
id = serializers.CharField(required=True, label=_("Branch id")) id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Branch id")))
type = serializers.CharField(required=True, label=_("Branch Type")) type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Branch Type")))
condition = serializers.CharField(required=True, label=_("Condition or|and")) condition = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Condition or|and")))
conditions = ConditionSerializer(many=True) conditions = ConditionSerializer(many=True)

View File

@ -12,17 +12,16 @@ from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class ReplyNodeParamsSerializer(serializers.Serializer): class ReplyNodeParamsSerializer(serializers.Serializer):
reply_type = serializers.CharField(required=True, label=_("Response Type")) reply_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Response Type")))
fields = serializers.ListField(required=False, label=_("Reference Field")) fields = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Reference Field")))
content = serializers.CharField(required=False, allow_blank=True, allow_null=True, content = serializers.CharField(required=False, allow_blank=True, allow_null=True,
label=_("Direct answer content")) error_messages=ErrMessage.char(_("Direct answer content")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)

View File

@ -6,10 +6,11 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
class DocumentExtractNodeSerializer(serializers.Serializer): class DocumentExtractNodeSerializer(serializers.Serializer):
document_list = serializers.ListField(required=False, label=_("document")) document_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("document")))
class IDocumentExtractNode(INode): class IDocumentExtractNode(INode):

View File

@ -7,9 +7,9 @@ from django.db.models import QuerySet
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.document_extract_node.i_document_extract_node import IDocumentExtractNode from application.flow.step_node.document_extract_node.i_document_extract_node import IDocumentExtractNode
from knowledge.models import File, FileSourceType from dataset.models import File
from knowledge.serializers.document import split_handles, parse_table_handle_list, FileBufferHandle from dataset.serializers.document_serializers import split_handles, parse_table_handle_list, FileBufferHandle
from oss.serializers.file import FileSerializer from dataset.serializers.file_serializers import FileSerializer
def bytes_to_uploaded_file(file_bytes, file_name="file.txt"): def bytes_to_uploaded_file(file_bytes, file_name="file.txt"):
@ -37,11 +37,11 @@ def bytes_to_uploaded_file(file_bytes, file_name="file.txt"):
splitter = '\n`-----------------------------------`\n' splitter = '\n`-----------------------------------`\n'
class BaseDocumentExtractNode(IDocumentExtractNode): class BaseDocumentExtractNode(IDocumentExtractNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
self.context['content'] = details.get('content') self.context['content'] = details.get('content')
def execute(self, document, chat_id, **kwargs): def execute(self, document, chat_id, **kwargs):
get_buffer = FileBufferHandle().get_buffer get_buffer = FileBufferHandle().get_buffer
@ -61,18 +61,12 @@ class BaseDocumentExtractNode(IDocumentExtractNode):
'application_id': str(application.id) if application.id else None, 'application_id': str(application.id) if application.id else None,
'file_id': str(image.id) 'file_id': str(image.id)
} }
file_bytes = image.meta.pop('content') file = bytes_to_uploaded_file(image.image, image.image_name)
f = bytes_to_uploaded_file(file_bytes, image.file_name) FileSerializer(data={'file': file, 'meta': meta}).upload()
FileSerializer(data={
'file': f,
'meta': meta,
'source_id': meta['application_id'],
'source_type': FileSourceType.APPLICATION.value
}).upload()
for doc in document: for doc in document:
file = QuerySet(File).filter(id=doc['file_id']).first() file = QuerySet(File).filter(id=doc['file_id']).first()
buffer = io.BytesIO(file.get_bytes()) buffer = io.BytesIO(file.get_byte().tobytes())
buffer.name = doc['name'] # this is the important line buffer.name = doc['name'] # this is the important line
for split_handle in (parse_table_handle_list + split_handles): for split_handle in (parse_table_handle_list + split_handles):

View File

@ -11,14 +11,14 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class FormNodeParamsSerializer(serializers.Serializer): class FormNodeParamsSerializer(serializers.Serializer):
form_field_list = serializers.ListField(required=True, label=_("Form Configuration")) form_field_list = serializers.ListField(required=True, error_messages=ErrMessage.list(_("Form Configuration")))
form_content_format = serializers.CharField(required=True, label=_('Form output content')) form_content_format = serializers.CharField(required=True, error_messages=ErrMessage.char(_('Form output content')))
form_data = serializers.DictField(required=False, allow_null=True, label=_("Form Data")) form_data = serializers.DictField(required=False, allow_null=True, error_messages=ErrMessage.dict(_("Form Data")))
class IFormNode(INode): class IFormNode(INode):

View File

@ -9,34 +9,34 @@
from typing import Type from typing import Type
from django.db.models import QuerySet from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.field.common import ObjectField from common.field.common import ObjectField
from tools.models.tool import Tool from common.util.field_message import ErrMessage
from function_lib.models.function import FunctionLib
from django.utils.translation import gettext_lazy as _
class InputField(serializers.Serializer): class InputField(serializers.Serializer):
name = serializers.CharField(required=True, label=_('Variable Name')) name = serializers.CharField(required=True, error_messages=ErrMessage.char(_('Variable Name')))
value = ObjectField(required=True, label=_("Variable Value"), model_type_list=[str, list]) value = ObjectField(required=True, error_messages=ErrMessage.char(_("Variable Value")), model_type_list=[str, list])
class FunctionLibNodeParamsSerializer(serializers.Serializer): class FunctionLibNodeParamsSerializer(serializers.Serializer):
tool_lib_id = serializers.UUIDField(required=True, label=_('Library ID')) function_lib_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('Library ID')))
input_field_list = InputField(required=True, many=True) input_field_list = InputField(required=True, many=True)
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)
f_lib = QuerySet(Tool).filter(id=self.data.get('tool_lib_id')).first() f_lib = QuerySet(FunctionLib).filter(id=self.data.get('function_lib_id')).first()
if f_lib is None: if f_lib is None:
raise Exception(_('The function has been deleted')) raise Exception(_('The function has been deleted'))
class IToolLibNode(INode): class IFunctionLibNode(INode):
type = 'tool-lib-node' type = 'function-lib-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return FunctionLibNodeParamsSerializer return FunctionLibNodeParamsSerializer
@ -44,5 +44,5 @@ class IToolLibNode(INode):
def _run(self): def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data) return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
def execute(self, tool_lib_id, input_field_list, **kwargs) -> NodeResult: def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
pass pass

View File

@ -6,4 +6,4 @@
@date2024/8/8 17:48 @date2024/8/8 17:48
@desc: @desc:
""" """
from .base_tool_lib_node import BaseToolLibNodeNode from .base_function_lib_node import BaseFunctionLibNodeNode

View File

@ -14,15 +14,14 @@ from django.db.models import QuerySet
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.tool_lib_node.i_tool_lib_node import IToolLibNode from application.flow.step_node.function_lib_node.i_function_lib_node import IFunctionLibNode
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.utils.rsa_util import rsa_long_decrypt from common.util.function_code import FunctionExecutor
from common.utils.tool_code import ToolExecutor from common.util.rsa_util import rsa_long_decrypt
from maxkb.const import CONFIG from function_lib.models.function import FunctionLib
from tools.models import Tool from smartdoc.const import CONFIG
function_executor = ToolExecutor(CONFIG.get('SANDBOX')) function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
def write_context(step_variable: Dict, global_variable: Dict, node, workflow): def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
@ -46,47 +45,35 @@ def get_field_value(debug_field_list, name, is_required):
def valid_reference_value(_type, value, name): def valid_reference_value(_type, value, name):
try: if _type == 'int':
if _type == 'int': instance_type = int | float
instance_type = int | float elif _type == 'float':
elif _type == 'float': instance_type = float | int
instance_type = float | int elif _type == 'dict':
elif _type == 'dict': instance_type = dict
value = json.loads(value) if isinstance(value, str) else value elif _type == 'array':
instance_type = dict instance_type = list
elif _type == 'array': elif _type == 'string':
value = json.loads(value) if isinstance(value, str) else value instance_type = str
instance_type = list else:
elif _type == 'string': raise Exception(_('Field: {name} Type: {_type} Value: {value} Unsupported types').format(name=name,
instance_type = str _type=_type))
else:
raise Exception(_(
'Field: {name} Type: {_type} Value: {value} Unsupported types'
).format(name=name, _type=_type))
except:
return value
if not isinstance(value, instance_type): if not isinstance(value, instance_type):
raise Exception(_( raise Exception(
'Field: {name} Type: {_type} Value: {value} Type error' _('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
).format(name=name, _type=_type, value=value)) value=value))
return value
def convert_value(name: str, value, _type, is_required, source, node): def convert_value(name: str, value, _type, is_required, source, node):
if not is_required and (value is None or ((isinstance(value, str) or isinstance(value, list)) and len(value) == 0)): if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
return None
if not is_required and source == 'reference' and (value is None or len(value) == 0):
return None return None
if source == 'reference': if source == 'reference':
value = node.workflow_manage.get_reference_field( value = node.workflow_manage.get_reference_field(
value[0], value[0],
value[1:]) value[1:])
if value is None: valid_reference_value(_type, value, name)
if not is_required:
return None
else:
raise Exception(_(
'Field: {name} Type: {_type} is required'
).format(name=name, _type=_type))
value = valid_reference_value(_type, value, name)
if _type == 'int': if _type == 'int':
return int(value) return int(value)
if _type == 'float': if _type == 'float':
@ -114,28 +101,24 @@ def convert_value(name: str, value, _type, is_required, source, node):
value=value)) value=value))
def valid_function(tool_lib, workspace_id): def valid_function(function_lib, user_id):
if tool_lib is None: if function_lib is None:
raise Exception(_('Tool does not exist')) raise Exception(_('Function does not exist'))
get_authorized_tool = DatabaseModelManage.get_model("get_authorized_tool") if function_lib.permission_type == 'PRIVATE' and str(function_lib.user_id) != str(user_id):
if tool_lib and tool_lib.workspace_id != workspace_id and get_authorized_tool is not None: raise Exception(_('No permission to use this function {name}').format(name=function_lib.name))
tool_lib = get_authorized_tool(QuerySet(Tool).filter(id=tool_lib.id), workspace_id).first() if not function_lib.is_active:
if tool_lib is None: raise Exception(_('Function {name} is unavailable').format(name=function_lib.name))
raise Exception(_("Tool does not exist"))
if not tool_lib.is_active:
raise Exception(_("Tool is not active"))
class BaseToolLibNodeNode(IToolLibNode): class BaseFunctionLibNodeNode(IFunctionLibNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
self.context['result'] = details.get('result') self.context['result'] = details.get('result')
if self.node_params.get('is_result'): if self.node_params.get('is_result'):
self.answer_text = str(details.get('result')) self.answer_text = str(details.get('result'))
def execute(self, tool_lib_id, input_field_list, **kwargs) -> NodeResult: def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
workspace_id = self.workflow_manage.get_body().get('workspace_id') function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
tool_lib = QuerySet(Tool).filter(id=tool_lib_id).first() valid_function(function_lib, self.flow_params_serializer.data.get('user_id'))
valid_function(tool_lib, workspace_id)
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'), params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
field.get('is_required'), field.get('is_required'),
field.get('source'), self) field.get('source'), self)
@ -143,15 +126,15 @@ class BaseToolLibNodeNode(IToolLibNode):
[{'value': get_field_value(input_field_list, field.get('name'), field.get('is_required'), [{'value': get_field_value(input_field_list, field.get('name'), field.get('is_required'),
), **field} ), **field}
for field in for field in
tool_lib.input_field_list]} function_lib.input_field_list]}
self.context['params'] = params self.context['params'] = params
# 合并初始化参数 # 合并初始化参数
if tool_lib.init_params is not None: if function_lib.init_params is not None:
all_params = json.loads(rsa_long_decrypt(tool_lib.init_params)) | params all_params = json.loads(rsa_long_decrypt(function_lib.init_params)) | params
else: else:
all_params = params all_params = params
result = function_executor.exec_code(tool_lib.code, all_params) result = function_executor.exec_code(function_lib.code, all_params)
return NodeResult({'result': result}, {}, _write_context=write_context) return NodeResult({'result': result}, {}, _write_context=write_context)
def get_details(self, index: int, **kwargs): def get_details(self, index: int, **kwargs):

View File

@ -15,23 +15,23 @@ from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.field.common import ObjectField from common.field.common import ObjectField
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.utils.formatting import lazy_format from rest_framework.utils.formatting import lazy_format
class InputField(serializers.Serializer): class InputField(serializers.Serializer):
name = serializers.CharField(required=True, label=_('Variable Name')) name = serializers.CharField(required=True, error_messages=ErrMessage.char(_('Variable Name')))
is_required = serializers.BooleanField(required=True, label=_("Is this field required")) is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean(_("Is this field required")))
type = serializers.CharField(required=True, label=_("type"), validators=[ type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("type")), validators=[
validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"), validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"),
message=_("The field only supports string|int|dict|array|float"), code=500) message=_("The field only supports string|int|dict|array|float"), code=500)
]) ])
source = serializers.CharField(required=True, label=_("source"), validators=[ source = serializers.CharField(required=True, error_messages=ErrMessage.char(_("source")), validators=[
validators.RegexValidator(regex=re.compile("^custom|reference$"), validators.RegexValidator(regex=re.compile("^custom|reference$"),
message=_("The field only supports custom|reference"), code=500) message=_("The field only supports custom|reference"), code=500)
]) ])
value = ObjectField(required=True, label=_("Variable Value"), model_type_list=[str, list]) value = ObjectField(required=True, error_messages=ErrMessage.char(_("Variable Value")), model_type_list=[str, list])
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)
@ -43,16 +43,15 @@ class InputField(serializers.Serializer):
class FunctionNodeParamsSerializer(serializers.Serializer): class FunctionNodeParamsSerializer(serializers.Serializer):
input_field_list = InputField(required=True, many=True) input_field_list = InputField(required=True, many=True)
code = serializers.CharField(required=True, label=_("function")) code = serializers.CharField(required=True, error_messages=ErrMessage.char(_("function")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)
class IToolNode(INode): class IFunctionNode(INode):
type = 'tool-node' type = 'function-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return FunctionNodeParamsSerializer return FunctionNodeParamsSerializer

View File

@ -6,4 +6,4 @@
@date2024/8/13 11:19 @date2024/8/13 11:19
@desc: @desc:
""" """
from .base_tool_node import BaseToolNodeNode from .base_function_node import BaseFunctionNodeNode

View File

@ -8,16 +8,16 @@
""" """
import json import json
import time import time
from typing import Dict from typing import Dict
from django.utils.translation import gettext as _
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.tool_node.i_tool_node import IToolNode from application.flow.step_node.function_node.i_function_node import IFunctionNode
from common.utils.tool_code import ToolExecutor from common.exception.app_exception import AppApiException
from maxkb.const import CONFIG from common.util.function_code import FunctionExecutor
from smartdoc.const import CONFIG
function_executor = ToolExecutor(CONFIG.get('SANDBOX')) function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
def write_context(step_variable: Dict, global_variable: Dict, node, workflow): def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
@ -32,47 +32,30 @@ def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
def valid_reference_value(_type, value, name): def valid_reference_value(_type, value, name):
try: if _type == 'int':
if _type == 'int': instance_type = int | float
instance_type = int | float elif _type == 'float':
elif _type == 'float': instance_type = float | int
instance_type = float | int elif _type == 'dict':
elif _type == 'dict': instance_type = dict
value = json.loads(value) if isinstance(value, str) else value elif _type == 'array':
instance_type = dict instance_type = list
elif _type == 'array': elif _type == 'string':
value = json.loads(value) if isinstance(value, str) else value instance_type = str
instance_type = list else:
elif _type == 'string': raise Exception(500, f'字段:{name}类型:{_type} 不支持的类型')
instance_type = str
else:
raise Exception(_(
'Field: {name} Type: {_type} Value: {value} Unsupported types'
).format(name=name, _type=_type))
except:
return value
if not isinstance(value, instance_type): if not isinstance(value, instance_type):
raise Exception(_( raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
'Field: {name} Type: {_type} Value: {value} Type error'
).format(name=name, _type=_type, value=value))
return value
def convert_value(name: str, value, _type, is_required, source, node): def convert_value(name: str, value, _type, is_required, source, node):
if not is_required and (value is None or ((isinstance(value, str) or isinstance(value, list)) and len(value) == 0)): if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
return None return None
if source == 'reference': if source == 'reference':
value = node.workflow_manage.get_reference_field( value = node.workflow_manage.get_reference_field(
value[0], value[0],
value[1:]) value[1:])
if value is None: valid_reference_value(_type, value, name)
if not is_required:
return None
else:
raise Exception(_(
'Field: {name} Type: {_type} is required'
).format(name=name, _type=_type))
value = valid_reference_value(_type, value, name)
if _type == 'int': if _type == 'int':
return int(value) return int(value)
if _type == 'float': if _type == 'float':
@ -87,20 +70,18 @@ def convert_value(name: str, value, _type, is_required, source, node):
v = json.loads(value) v = json.loads(value)
if isinstance(v, dict): if isinstance(v, dict):
return v return v
raise Exception(_('type error')) raise Exception("类型错误")
if _type == 'array': if _type == 'array':
v = json.loads(value) v = json.loads(value)
if isinstance(v, list): if isinstance(v, list):
return v return v
raise Exception(_('type error')) raise Exception("类型错误")
return value return value
except Exception as e: except Exception as e:
raise Exception( raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
value=value))
class BaseToolNodeNode(IToolNode): class BaseFunctionNodeNode(IFunctionNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
self.context['result'] = details.get('result') self.context['result'] = details.get('result')
if self.node_params.get('is_result', False): if self.node_params.get('is_result', False):

View File

@ -2,31 +2,31 @@
from typing import Type from typing import Type
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _
class ImageGenerateNodeSerializer(serializers.Serializer): class ImageGenerateNodeSerializer(serializers.Serializer):
model_id = serializers.CharField(required=True, label=_("Model id")) model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
prompt = serializers.CharField(required=True, label=_("Prompt word (positive)")) prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word (positive)")))
negative_prompt = serializers.CharField(required=False, label=_("Prompt word (negative)"), negative_prompt = serializers.CharField(required=False, error_messages=ErrMessage.char(_("Prompt word (negative)")),
allow_null=True, allow_blank=True, ) allow_null=True, allow_blank=True, )
# 多轮对话数量 # 多轮对话数量
dialogue_number = serializers.IntegerField(required=False, default=0, dialogue_number = serializers.IntegerField(required=False, default=0,
label=_("Number of multi-round conversations")) error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
dialogue_type = serializers.CharField(required=False, default='NODE', dialogue_type = serializers.CharField(required=False, default='NODE',
label=_("Conversation storage type")) error_messages=ErrMessage.char(_("Conversation storage type")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
model_params_setting = serializers.JSONField(required=False, default=dict, model_params_setting = serializers.JSONField(required=False, default=dict,
label=_("Model parameter settings")) error_messages=ErrMessage.json(_("Model parameter settings")))
class IImageGenerateNode(INode): class IImageGenerateNode(INode):

View File

@ -7,10 +7,9 @@ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.image_generate_step_node.i_image_generate_node import IImageGenerateNode from application.flow.step_node.image_generate_step_node.i_image_generate_node import IImageGenerateNode
from common.utils.common import bytes_to_uploaded_file from common.util.common import bytes_to_uploaded_file
from knowledge.models import FileSourceType from dataset.serializers.file_serializers import FileSerializer
from oss.serializers.file import FileSerializer from setting.models_provider.tools import get_model_instance_by_model_user_id
from models_provider.tools import get_model_instance_by_model_workspace_id
class BaseImageGenerateNode(IImageGenerateNode): class BaseImageGenerateNode(IImageGenerateNode):
@ -24,10 +23,10 @@ class BaseImageGenerateNode(IImageGenerateNode):
model_params_setting, model_params_setting,
chat_record_id, chat_record_id,
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
print(model_params_setting)
application = self.workflow_manage.work_flow_post_handler.chat_info.application application = self.workflow_manage.work_flow_post_handler.chat_info.application
workspace_id = self.workflow_manage.get_body().get('workspace_id') tti_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
tti_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **model_params_setting)
**model_params_setting)
history_message = self.get_history_message(history_chat_record, dialogue_number) history_message = self.get_history_message(history_chat_record, dialogue_number)
self.context['history_message'] = history_message self.context['history_message'] = history_message
question = self.generate_prompt_question(prompt) question = self.generate_prompt_question(prompt)
@ -35,26 +34,19 @@ class BaseImageGenerateNode(IImageGenerateNode):
message_list = self.generate_message_list(question, history_message) message_list = self.generate_message_list(question, history_message)
self.context['message_list'] = message_list self.context['message_list'] = message_list
self.context['dialogue_type'] = dialogue_type self.context['dialogue_type'] = dialogue_type
self.context['negative_prompt'] = negative_prompt print(message_list)
image_urls = tti_model.generate_image(question, negative_prompt) image_urls = tti_model.generate_image(question, negative_prompt)
# 保存图片 # 保存图片
file_urls = [] file_urls = []
for image_url in image_urls: for image_url in image_urls:
file_name = 'generated_image.png' file_name = 'generated_image.png'
if isinstance(image_url, str) and image_url.startswith('http'): file = bytes_to_uploaded_file(requests.get(image_url).content, file_name)
image_url = requests.get(image_url).content
file = bytes_to_uploaded_file(image_url, file_name)
meta = { meta = {
'debug': False if application.id else True, 'debug': False if application.id else True,
'chat_id': chat_id, 'chat_id': chat_id,
'application_id': str(application.id) if application.id else None, 'application_id': str(application.id) if application.id else None,
} }
file_url = FileSerializer(data={ file_url = FileSerializer(data={'file': file, 'meta': meta}).upload()
'file': file,
'meta': meta,
'source_id': meta['application_id'],
'source_type': FileSourceType.APPLICATION.value
}).upload()
file_urls.append(file_url) file_urls.append(file_url)
self.context['image_list'] = [{'file_id': path.split('/')[-1], 'url': path} for path in file_urls] self.context['image_list'] = [{'file_id': path.split('/')[-1], 'url': path} for path in file_urls]
answer = ' '.join([f"![Image]({path})" for path in file_urls]) answer = ' '.join([f"![Image]({path})" for path in file_urls])
@ -126,6 +118,5 @@ class BaseImageGenerateNode(IImageGenerateNode):
'status': self.status, 'status': self.status,
'err_message': self.err_message, 'err_message': self.err_message,
'image_list': self.context.get('image_list'), 'image_list': self.context.get('image_list'),
'dialogue_type': self.context.get('dialogue_type'), 'dialogue_type': self.context.get('dialogue_type')
'negative_prompt': self.context.get('negative_prompt'),
} }

View File

@ -5,27 +5,26 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class ImageUnderstandNodeSerializer(serializers.Serializer): class ImageUnderstandNodeSerializer(serializers.Serializer):
model_id = serializers.CharField(required=True, label=_("Model id")) model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
system = serializers.CharField(required=False, allow_blank=True, allow_null=True, system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
label=_("Role Setting")) error_messages=ErrMessage.char(_("Role Setting")))
prompt = serializers.CharField(required=True, label=_("Prompt word")) prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
# 多轮对话数量 # 多轮对话数量
dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations")) dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
dialogue_type = serializers.CharField(required=True, label=_("Conversation storage type")) dialogue_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Conversation storage type")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
image_list = serializers.ListField(required=False, label=_("picture")) image_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("picture")))
model_params_setting = serializers.JSONField(required=False, default=dict, model_params_setting = serializers.JSONField(required=False, default=dict,
label=_("Model parameter settings")) error_messages=ErrMessage.json(_("Model parameter settings")))
class IImageUnderstandNode(INode): class IImageUnderstandNode(INode):

View File

@ -1,8 +1,8 @@
# coding=utf-8 # coding=utf-8
import base64 import base64
import os
import time import time
from functools import reduce from functools import reduce
from imghdr import what
from typing import List, Dict from typing import List, Dict
from django.db.models import QuerySet from django.db.models import QuerySet
@ -10,8 +10,9 @@ from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AI
from application.flow.i_step_node import NodeResult, INode from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.image_understand_step_node.i_image_understand_node import IImageUnderstandNode from application.flow.step_node.image_understand_step_node.i_image_understand_node import IImageUnderstandNode
from knowledge.models import File from dataset.models import File
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
from imghdr import what
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str): def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str):
@ -59,9 +60,9 @@ def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, wor
def file_id_to_base64(file_id: str): def file_id_to_base64(file_id: str):
file = QuerySet(File).filter(id=file_id).first() file = QuerySet(File).filter(id=file_id).first()
file_bytes = file.get_bytes() file_bytes = file.get_byte()
base64_image = base64.b64encode(file_bytes).decode("utf-8") base64_image = base64.b64encode(file_bytes).decode("utf-8")
return [base64_image, what(None, file_bytes)] return [base64_image, what(None, file_bytes.tobytes())]
class BaseImageUnderstandNode(IImageUnderstandNode): class BaseImageUnderstandNode(IImageUnderstandNode):
@ -79,9 +80,8 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
# 处理不正确的参数 # 处理不正确的参数
if image is None or not isinstance(image, list): if image is None or not isinstance(image, list):
image = [] image = []
workspace_id = self.workflow_manage.get_body().get('workspace_id') print(model_params_setting)
image_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, image_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'), **model_params_setting)
**model_params_setting)
# 执行详情中的历史消息不需要图片内容 # 执行详情中的历史消息不需要图片内容
history_message = self.get_history_message_for_details(history_chat_record, dialogue_number) history_message = self.get_history_message_for_details(history_chat_record, dialogue_number)
self.context['history_message'] = history_message self.context['history_message'] = history_message
@ -130,7 +130,7 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
file_id_list = [image.get('file_id') for image in image_list] file_id_list = [image.get('file_id') for image in image_list]
return HumanMessage(content=[ return HumanMessage(content=[
{'type': 'text', 'text': data['question']}, {'type': 'text', 'text': data['question']},
*[{'type': 'image_url', 'image_url': {'url': f'./oss/file/{file_id}'}} for file_id in file_id_list] *[{'type': 'image_url', 'image_url': {'url': f'/api/file/{file_id}'}} for file_id in file_id_list]
]) ])
return HumanMessage(content=chat_record.problem_text) return HumanMessage(content=chat_record.problem_text)
@ -155,8 +155,7 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
return HumanMessage( return HumanMessage(
content=[ content=[
{'type': 'text', 'text': data['question']}, {'type': 'text', 'text': data['question']},
*[{'type': 'image_url', *[{'type': 'image_url', 'image_url': {'url': f'data:image/{base64_image[1]};base64,{base64_image[0]}'}} for
'image_url': {'url': f'data:image/{base64_image[1]};base64,{base64_image[0]}'}} for
base64_image in image_base64_list] base64_image in image_base64_list]
]) ])
return HumanMessage(content=chat_record.problem_text) return HumanMessage(content=chat_record.problem_text)
@ -171,11 +170,10 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
for img in image: for img in image:
file_id = img['file_id'] file_id = img['file_id']
file = QuerySet(File).filter(id=file_id).first() file = QuerySet(File).filter(id=file_id).first()
image_bytes = file.get_bytes() image_bytes = file.get_byte()
base64_image = base64.b64encode(image_bytes).decode("utf-8") base64_image = base64.b64encode(image_bytes).decode("utf-8")
image_format = what(None, image_bytes) image_format = what(None, image_bytes.tobytes())
images.append( images.append({'type': 'image_url', 'image_url': {'url': f'data:image/{image_format};base64,{base64_image}'}})
{'type': 'image_url', 'image_url': {'url': f'data:image/{image_format};base64,{base64_image}'}})
messages = [HumanMessage( messages = [HumanMessage(
content=[ content=[
{'type': 'text', 'text': self.workflow_manage.generate_prompt(prompt)}, {'type': 'text', 'text': self.workflow_manage.generate_prompt(prompt)},

View File

@ -2,23 +2,24 @@
from typing import Type from typing import Type
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _
class McpNodeSerializer(serializers.Serializer): class McpNodeSerializer(serializers.Serializer):
mcp_servers = serializers.JSONField(required=True, mcp_servers = serializers.JSONField(required=True,
label=_("Mcp servers")) error_messages=ErrMessage.char(_("Mcp servers")))
mcp_server = serializers.CharField(required=True, mcp_server = serializers.CharField(required=True,
label=_("Mcp server")) error_messages=ErrMessage.char(_("Mcp server")))
mcp_tool = serializers.CharField(required=True, label=_("Mcp tool")) mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool")))
tool_params = serializers.DictField(required=True, tool_params = serializers.DictField(required=True,
label=_("Tool parameters")) error_messages=ErrMessage.char(_("Tool parameters")))
class IMcpNode(INode): class IMcpNode(INode):

View File

@ -14,18 +14,20 @@ class BaseMcpNode(IMcpNode):
self.context['result'] = details.get('result') self.context['result'] = details.get('result')
self.context['tool_params'] = details.get('tool_params') self.context['tool_params'] = details.get('tool_params')
self.context['mcp_tool'] = details.get('mcp_tool') self.context['mcp_tool'] = details.get('mcp_tool')
if self.node_params.get('is_result', False):
self.answer_text = details.get('result')
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult: def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
servers = json.loads(mcp_servers) servers = json.loads(mcp_servers)
params = json.loads(json.dumps(tool_params)) params = json.loads(json.dumps(tool_params))
params = self.handle_variables(params) params = self.handle_variables(params)
async def call_tool(t, a): async def call_tool(s, session, t, a):
client = MultiServerMCPClient(servers) async with MultiServerMCPClient(s) as client:
async with client.session(mcp_server) as s: s = await client.sessions[session].call_tool(t, a)
return await s.call_tool(t, a) return s
res = asyncio.run(call_tool(mcp_tool, params)) res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params))
return NodeResult( return NodeResult(
{'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {}) {'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {})

View File

@ -8,25 +8,23 @@
""" """
from typing import Type from typing import Type
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _
class QuestionNodeSerializer(serializers.Serializer): class QuestionNodeSerializer(serializers.Serializer):
model_id = serializers.CharField(required=True, label=_("Model id")) model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
system = serializers.CharField(required=False, allow_blank=True, allow_null=True, system = serializers.CharField(required=False, allow_blank=True, allow_null=True,
label=_("Role Setting")) error_messages=ErrMessage.char(_("Role Setting")))
prompt = serializers.CharField(required=True, label=_("Prompt word")) prompt = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Prompt word")))
# 多轮对话数量 # 多轮对话数量
dialogue_number = serializers.IntegerField(required=True, label= dialogue_number = serializers.IntegerField(required=True, error_messages=ErrMessage.integer(_("Number of multi-round conversations")))
_("Number of multi-round conversations"))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content')) model_params_setting = serializers.DictField(required=False, error_messages=ErrMessage.integer(_("Model parameter settings")))
model_params_setting = serializers.DictField(required=False,
label=_("Model parameter settings"))
class IQuestionNode(INode): class IQuestionNode(INode):

View File

@ -17,8 +17,9 @@ from langchain_core.messages import BaseMessage
from application.flow.i_step_node import NodeResult, INode from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.question_node.i_question_node import IQuestionNode from application.flow.step_node.question_node.i_question_node import IQuestionNode
from models_provider.models import Model from setting.models import Model
from models_provider.tools import get_model_instance_by_model_workspace_id, get_model_credential from setting.models_provider import get_model_credential
from setting.models_provider.tools import get_model_instance_by_model_user_id
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str): def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str):
@ -87,9 +88,8 @@ class BaseQuestionNode(IQuestionNode):
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
if model_params_setting is None: if model_params_setting is None:
model_params_setting = get_default_model_params_setting(model_id) model_params_setting = get_default_model_params_setting(model_id)
workspace_id = self.workflow_manage.get_body().get('workspace_id') chat_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **model_params_setting)
**model_params_setting)
history_message = self.get_history_message(history_chat_record, dialogue_number) history_message = self.get_history_message(history_chat_record, dialogue_number)
self.context['history_message'] = history_message self.context['history_message'] = history_message
question = self.generate_prompt_question(prompt) question = self.generate_prompt_question(prompt)

View File

@ -11,19 +11,19 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class RerankerSettingSerializer(serializers.Serializer): class RerankerSettingSerializer(serializers.Serializer):
# 需要查询的条数 # 需要查询的条数
top_n = serializers.IntegerField(required=True, top_n = serializers.IntegerField(required=True,
label=_("Reference segment number")) error_messages=ErrMessage.integer(_("Reference segment number")))
# 相似度 0-1之间 # 相似度 0-1之间
similarity = serializers.FloatField(required=True, max_value=2, min_value=0, similarity = serializers.FloatField(required=True, max_value=2, min_value=0,
label=_("Reference segment number")) error_messages=ErrMessage.float(_("Reference segment number")))
max_paragraph_char_number = serializers.IntegerField(required=True, max_paragraph_char_number = serializers.IntegerField(required=True,
label=_("Maximum number of words in a quoted segment")) error_messages=ErrMessage.float(_("Maximum number of words in a quoted segment")))
class RerankerStepNodeSerializer(serializers.Serializer): class RerankerStepNodeSerializer(serializers.Serializer):
@ -32,8 +32,6 @@ class RerankerStepNodeSerializer(serializers.Serializer):
question_reference_address = serializers.ListField(required=True) question_reference_address = serializers.ListField(required=True)
reranker_model_id = serializers.UUIDField(required=True) reranker_model_id = serializers.UUIDField(required=True)
reranker_reference_list = serializers.ListField(required=True, child=serializers.ListField(required=True)) reranker_reference_list = serializers.ListField(required=True, child=serializers.ListField(required=True))
show_knowledge = serializers.BooleanField(required=True,
label=_("The results are displayed in the knowledge sources"))
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)
@ -57,6 +55,6 @@ class IRerankerNode(INode):
reranker_list=reranker_list) reranker_list=reranker_list)
def execute(self, question, reranker_setting, reranker_list, reranker_model_id,show_knowledge, def execute(self, question, reranker_setting, reranker_list, reranker_model_id,
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
pass pass

View File

@ -12,7 +12,7 @@ from langchain_core.documents import Document
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.reranker_node.i_reranker_node import IRerankerNode from application.flow.step_node.reranker_node.i_reranker_node import IRerankerNode
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
def merge_reranker_list(reranker_list, result=None): def merge_reranker_list(reranker_list, result=None):
@ -24,9 +24,11 @@ def merge_reranker_list(reranker_list, result=None):
elif isinstance(document, dict): elif isinstance(document, dict):
content = document.get('title', '') + document.get('content', '') content = document.get('title', '') + document.get('content', '')
title = document.get("title") title = document.get("title")
dataset_name = document.get("dataset_name")
document_name = document.get('document_name')
result.append( result.append(
Document(page_content=str(document) if len(content) == 0 else content, Document(page_content=str(document) if len(content) == 0 else content,
metadata={'title': title, **document})) metadata={'title': title, 'dataset_name': dataset_name, 'document_name': document_name}))
else: else:
result.append(Document(page_content=str(document), metadata={})) result.append(Document(page_content=str(document), metadata={}))
return result return result
@ -61,12 +63,6 @@ def reset_result_list(result_list: List[Document], document_list: List[Document]
return r return r
def get_none_result(question):
return NodeResult(
{'document_list': [], 'question': question,
'result_list': [], 'result': ''}, {})
class BaseRerankerNode(IRerankerNode): class BaseRerankerNode(IRerankerNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
self.context['document_list'] = details.get('document_list', []) self.context['document_list'] = details.get('document_list', [])
@ -75,21 +71,16 @@ class BaseRerankerNode(IRerankerNode):
self.context['result_list'] = details.get('result_list') self.context['result_list'] = details.get('result_list')
self.context['result'] = details.get('result') self.context['result'] = details.get('result')
def execute(self, question, reranker_setting, reranker_list, reranker_model_id, show_knowledge, def execute(self, question, reranker_setting, reranker_list, reranker_model_id,
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
self.context['show_knowledge'] = show_knowledge
documents = merge_reranker_list(reranker_list) documents = merge_reranker_list(reranker_list)
documents = [d for d in documents if d.page_content and len(d.page_content) > 0]
if len(documents) == 0:
return get_none_result(question)
top_n = reranker_setting.get('top_n', 3) top_n = reranker_setting.get('top_n', 3)
self.context['document_list'] = [{'page_content': document.page_content, 'metadata': document.metadata} for self.context['document_list'] = [{'page_content': document.page_content, 'metadata': document.metadata} for
document in documents] document in documents]
self.context['question'] = question self.context['question'] = question
workspace_id = self.workflow_manage.get_body().get('workspace_id') reranker_model = get_model_instance_by_model_user_id(reranker_model_id,
reranker_model = get_model_instance_by_model_workspace_id(reranker_model_id, self.flow_params_serializer.data.get('user_id'),
workspace_id, top_n=top_n)
top_n=top_n)
result = reranker_model.compress_documents( result = reranker_model.compress_documents(
documents, documents,
question) question)
@ -101,7 +92,6 @@ class BaseRerankerNode(IRerankerNode):
def get_details(self, index: int, **kwargs): def get_details(self, index: int, **kwargs):
return { return {
'show_knowledge': self.context.get('show_knowledge'),
'name': self.node.properties.get('stepName'), 'name': self.node.properties.get('stepName'),
"index": index, "index": index,
'document_list': self.context.get('document_list'), 'document_list': self.context.get('document_list'),

View File

@ -13,37 +13,34 @@ from django.core import validators
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.utils.common import flat_map from common.util.common import flat_map
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class DatasetSettingSerializer(serializers.Serializer): class DatasetSettingSerializer(serializers.Serializer):
# 需要查询的条数 # 需要查询的条数
top_n = serializers.IntegerField(required=True, top_n = serializers.IntegerField(required=True,
label=_("Reference segment number")) error_messages=ErrMessage.integer(_("Reference segment number")))
# 相似度 0-1之间 # 相似度 0-1之间
similarity = serializers.FloatField(required=True, max_value=2, min_value=0, similarity = serializers.FloatField(required=True, max_value=2, min_value=0,
label=_('similarity')) error_messages=ErrMessage.float(_('similarity')))
search_mode = serializers.CharField(required=True, validators=[ search_mode = serializers.CharField(required=True, validators=[
validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"), validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"),
message=_("The type only supports embedding|keywords|blend"), code=500) message=_("The type only supports embedding|keywords|blend"), code=500)
], label=_("Retrieval Mode")) ], error_messages=ErrMessage.char(_("Retrieval Mode")))
max_paragraph_char_number = serializers.IntegerField(required=True, max_paragraph_char_number = serializers.IntegerField(required=True,
label=_("Maximum number of words in a quoted segment")) error_messages=ErrMessage.float(_("Maximum number of words in a quoted segment")))
class SearchDatasetStepNodeSerializer(serializers.Serializer): class SearchDatasetStepNodeSerializer(serializers.Serializer):
# 需要查询的数据集id列表 # 需要查询的数据集id列表
knowledge_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True), dataset_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True),
label=_("Dataset id list")) error_messages=ErrMessage.list(_("Dataset id list")))
knowledge_setting = DatasetSettingSerializer(required=True) dataset_setting = DatasetSettingSerializer(required=True)
question_reference_address = serializers.ListField(required=True) question_reference_address = serializers.ListField(required=True)
show_knowledge = serializers.BooleanField(required=True,
label=_("The results are displayed in the knowledge sources"))
def is_valid(self, *, raise_exception=False): def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True) super().is_valid(raise_exception=True)
@ -55,8 +52,8 @@ def get_paragraph_list(chat_record, node_id):
'paragraph_list', []) is not None and key == node_id]) 'paragraph_list', []) is not None and key == node_id])
class ISearchKnowledgeStepNode(INode): class ISearchDatasetStepNode(INode):
type = 'search-knowledge-node' type = 'search-dataset-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return SearchDatasetStepNodeSerializer return SearchDatasetStepNodeSerializer
@ -76,7 +73,7 @@ class ISearchKnowledgeStepNode(INode):
return self.execute(**self.node_params_serializer.data, question=str(question), return self.execute(**self.node_params_serializer.data, question=str(question),
exclude_paragraph_id_list=exclude_paragraph_id_list) exclude_paragraph_id_list=exclude_paragraph_id_list)
def execute(self, dataset_id_list, dataset_setting, question, show_knowledge, def execute(self, dataset_id_list, dataset_setting, question,
exclude_paragraph_id_list=None, exclude_paragraph_id_list=None,
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
pass pass

View File

@ -6,4 +6,4 @@
@date2024/6/11 15:35 @date2024/6/11 15:35
@desc: @desc:
""" """
from .base_search_knowledge_node import BaseSearchKnowledgeNode from .base_search_dataset_node import BaseSearchDatasetNode

View File

@ -9,28 +9,26 @@
import os import os
from typing import List, Dict from typing import List, Dict
from django.db import connection
from django.db.models import QuerySet from django.db.models import QuerySet
from django.db import connection
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.search_knowledge_node.i_search_knowledge_node import ISearchKnowledgeStepNode from application.flow.step_node.search_dataset_node.i_search_dataset_node import ISearchDatasetStepNode
from common.config.embedding_config import VectorStore from common.config.embedding_config import VectorStore
from common.constants.permission_constants import RoleConstants
from common.database_model_manage.database_model_manage import DatabaseModelManage
from common.db.search import native_search from common.db.search import native_search
from common.utils.common import get_file_content from common.util.file_util import get_file_content
from knowledge.models import Document, Paragraph, Knowledge, SearchMode from dataset.models import Document, Paragraph, DataSet
from maxkb.conf import PROJECT_DIR from embedding.models import SearchMode
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
from smartdoc.conf import PROJECT_DIR
def get_embedding_id(dataset_id_list): def get_embedding_id(dataset_id_list):
dataset_list = QuerySet(Knowledge).filter(id__in=dataset_id_list) dataset_list = QuerySet(DataSet).filter(id__in=dataset_id_list)
if len(set([dataset.embedding_model_id for dataset in dataset_list])) > 1: if len(set([dataset.embedding_mode_id for dataset in dataset_list])) > 1:
raise Exception("关联知识库的向量模型不一致,无法召回分段。") raise Exception("关联知识库的向量模型不一致,无法召回分段。")
if len(dataset_list) == 0: if len(dataset_list) == 0:
raise Exception("知识库设置错误,请重新设置知识库") raise Exception("知识库设置错误,请重新设置知识库")
return dataset_list[0].embedding_model_id return dataset_list[0].embedding_mode_id
def get_none_result(question): def get_none_result(question):
@ -46,10 +44,10 @@ def reset_title(title):
return f"#### {title}\n" return f"#### {title}\n"
class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode): class BaseSearchDatasetNode(ISearchDatasetStepNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
result = details.get('paragraph_list', []) result = details.get('paragraph_list', [])
knowledge_setting = self.node_params_serializer.data.get('knowledge_setting') dataset_setting = self.node_params_serializer.data.get('dataset_setting')
directly_return = '\n'.join( directly_return = '\n'.join(
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result if [f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result if
paragraph.get('is_hit_handling_method')]) paragraph.get('is_hit_handling_method')])
@ -59,34 +57,26 @@ class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode):
self.context['is_hit_handling_method_list'] = [row for row in result if row.get('is_hit_handling_method')] self.context['is_hit_handling_method_list'] = [row for row in result if row.get('is_hit_handling_method')]
self.context['data'] = '\n'.join( self.context['data'] = '\n'.join(
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in [f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in
result])[0:knowledge_setting.get('max_paragraph_char_number', 5000)] result])[0:dataset_setting.get('max_paragraph_char_number', 5000)]
self.context['directly_return'] = directly_return self.context['directly_return'] = directly_return
def execute(self, knowledge_id_list, knowledge_setting, question, show_knowledge, def execute(self, dataset_id_list, dataset_setting, question,
exclude_paragraph_id_list=None, exclude_paragraph_id_list=None,
**kwargs) -> NodeResult: **kwargs) -> NodeResult:
self.context['question'] = question self.context['question'] = question
self.context['show_knowledge'] = show_knowledge if len(dataset_id_list) == 0:
get_knowledge_list_of_authorized = DatabaseModelManage.get_model('get_knowledge_list_of_authorized')
chat_user_type = self.workflow_manage.get_body().get('chat_user_type')
if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type:
knowledge_id_list = get_knowledge_list_of_authorized(self.workflow_manage.get_body().get('chat_user_id'),
knowledge_id_list)
if len(knowledge_id_list) == 0:
return get_none_result(question) return get_none_result(question)
model_id = get_embedding_id(knowledge_id_list) model_id = get_embedding_id(dataset_id_list)
workspace_id = self.workflow_manage.get_body().get('workspace_id') embedding_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'))
embedding_model = get_model_instance_by_model_workspace_id(model_id, workspace_id)
embedding_value = embedding_model.embed_query(question) embedding_value = embedding_model.embed_query(question)
vector = VectorStore.get_embedding_vector() vector = VectorStore.get_embedding_vector()
exclude_document_id_list = [str(document.id) for document in exclude_document_id_list = [str(document.id) for document in
QuerySet(Document).filter( QuerySet(Document).filter(
knowledge_id__in=knowledge_id_list, dataset_id__in=dataset_id_list,
is_active=False)] is_active=False)]
embedding_list = vector.query(question, embedding_value, knowledge_id_list, exclude_document_id_list, embedding_list = vector.query(question, embedding_value, dataset_id_list, exclude_document_id_list,
exclude_paragraph_id_list, True, knowledge_setting.get('top_n'), exclude_paragraph_id_list, True, dataset_setting.get('top_n'),
knowledge_setting.get('similarity'), dataset_setting.get('similarity'), SearchMode(dataset_setting.get('search_mode')))
SearchMode(knowledge_setting.get('search_mode')))
# 手动关闭数据库连接 # 手动关闭数据库连接
connection.close() connection.close()
if embedding_list is None: if embedding_list is None:
@ -98,7 +88,7 @@ class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode):
'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')], 'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
'data': '\n'.join( 'data': '\n'.join(
[f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in [f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
result])[0:knowledge_setting.get('max_paragraph_char_number', 5000)], result])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
'directly_return': '\n'.join( 'directly_return': '\n'.join(
[paragraph.get('content') for paragraph in [paragraph.get('content') for paragraph in
result if result if
@ -121,7 +111,7 @@ class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode):
'update_time': paragraph.get('update_time').strftime("%Y-%m-%d %H:%M:%S"), 'update_time': paragraph.get('update_time').strftime("%Y-%m-%d %H:%M:%S"),
'create_time': paragraph.get('create_time').strftime("%Y-%m-%d %H:%M:%S"), 'create_time': paragraph.get('create_time').strftime("%Y-%m-%d %H:%M:%S"),
'id': str(paragraph.get('id')), 'id': str(paragraph.get('id')),
'knowledge_id': str(paragraph.get('knowledge_id')), 'dataset_id': str(paragraph.get('dataset_id')),
'document_id': str(paragraph.get('document_id')) 'document_id': str(paragraph.get('document_id'))
} }
@ -133,7 +123,7 @@ class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode):
paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list), paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list),
get_file_content( get_file_content(
os.path.join(PROJECT_DIR, "apps", "application", 'sql', os.path.join(PROJECT_DIR, "apps", "application", 'sql',
'list_knowledge_paragraph_by_paragraph_id.sql')), 'list_dataset_paragraph_by_paragraph_id.sql')),
with_table_name=True) with_table_name=True)
# 如果向量库中存在脏数据 直接删除 # 如果向量库中存在脏数据 直接删除
if len(paragraph_list) != len(paragraph_id_list): if len(paragraph_list) != len(paragraph_id_list):
@ -146,7 +136,6 @@ class BaseSearchKnowledgeNode(ISearchKnowledgeStepNode):
def get_details(self, index: int, **kwargs): def get_details(self, index: int, **kwargs):
return { return {
'name': self.node.properties.get('stepName'), 'name': self.node.properties.get('stepName'),
'show_knowledge': self.context.get('show_knowledge'),
'question': self.context.get('question'), 'question': self.context.get('question'),
"index": index, "index": index,
'run_time': self.context.get('run_time'), 'run_time': self.context.get('run_time'),

View File

@ -5,17 +5,16 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class SpeechToTextNodeSerializer(serializers.Serializer): class SpeechToTextNodeSerializer(serializers.Serializer):
stt_model_id = serializers.CharField(required=True, label=_("Model id")) stt_model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
audio_list = serializers.ListField(required=True, audio_list = serializers.ListField(required=True, error_messages=ErrMessage.list(_("The audio file cannot be empty")))
label=_("The audio file cannot be empty"))
class ISpeechToTextNode(INode): class ISpeechToTextNode(INode):
@ -29,8 +28,7 @@ class ISpeechToTextNode(INode):
self.node_params_serializer.data.get('audio_list')[1:]) self.node_params_serializer.data.get('audio_list')[1:])
for audio in res: for audio in res:
if 'file_id' not in audio: if 'file_id' not in audio:
raise ValueError( raise ValueError(_("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails"))
_("Parameter value error: The uploaded audio lacks file_id, and the audio upload fails"))
return self.execute(audio=res, **self.node_params_serializer.data, **self.flow_params_serializer.data) return self.execute(audio=res, **self.node_params_serializer.data, **self.flow_params_serializer.data)

View File

@ -1,28 +1,28 @@
# coding=utf-8 # coding=utf-8
import os import os
import tempfile import tempfile
from concurrent.futures import ThreadPoolExecutor import time
import io
from typing import List, Dict
from django.db.models import QuerySet from django.db.models import QuerySet
from pydub import AudioSegment
from application.flow.i_step_node import NodeResult from concurrent.futures import ThreadPoolExecutor
from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.speech_to_text_step_node.i_speech_to_text_node import ISpeechToTextNode from application.flow.step_node.speech_to_text_step_node.i_speech_to_text_node import ISpeechToTextNode
from common.utils.common import split_and_transcribe, any_to_mp3 from common.util.common import split_and_transcribe, any_to_mp3
from knowledge.models import File from dataset.models import File
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
class BaseSpeechToTextNode(ISpeechToTextNode): class BaseSpeechToTextNode(ISpeechToTextNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer') self.context['answer'] = details.get('answer')
self.context['result'] = details.get('answer')
if self.node_params.get('is_result', False): if self.node_params.get('is_result', False):
self.answer_text = details.get('answer') self.answer_text = details.get('answer')
def execute(self, stt_model_id, chat_id, audio, **kwargs) -> NodeResult: def execute(self, stt_model_id, chat_id, audio, **kwargs) -> NodeResult:
workspace_id = self.workflow_manage.get_body().get('workspace_id') stt_model = get_model_instance_by_model_user_id(stt_model_id, self.flow_params_serializer.data.get('user_id'))
stt_model = get_model_instance_by_model_workspace_id(stt_model_id, workspace_id)
audio_list = audio audio_list = audio
self.context['audio_list'] = audio self.context['audio_list'] = audio
@ -31,7 +31,7 @@ class BaseSpeechToTextNode(ISpeechToTextNode):
# 根据file_name 吧文件转成mp3格式 # 根据file_name 吧文件转成mp3格式
file_format = file.file_name.split('.')[-1] file_format = file.file_name.split('.')[-1]
with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{file_format}') as temp_file: with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{file_format}') as temp_file:
temp_file.write(file.get_bytes()) temp_file.write(file.get_byte().tobytes())
temp_file_path = temp_file.name temp_file_path = temp_file.name
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_amr_file: with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_amr_file:
temp_mp3_path = temp_amr_file.name temp_mp3_path = temp_amr_file.name

View File

@ -17,32 +17,24 @@ from application.flow.step_node.start_node.i_start_node import IStarNode
def get_default_global_variable(input_field_list: List): def get_default_global_variable(input_field_list: List):
return { return {item.get('variable'): item.get('default_value') for item in input_field_list if
item.get('variable') or item.get('field'): item.get('default_value') item.get('default_value', None) is not None}
for item in input_field_list
if item.get('default_value', None) is not None
}
def get_global_variable(node): def get_global_variable(node):
body = node.workflow_manage.get_body()
history_chat_record = node.flow_params_serializer.data.get('history_chat_record', []) history_chat_record = node.flow_params_serializer.data.get('history_chat_record', [])
history_context = [{'question': chat_record.problem_text, 'answer': chat_record.answer_text} for chat_record in history_context = [{'question': chat_record.problem_text, 'answer': chat_record.answer_text} for chat_record in
history_chat_record] history_chat_record]
chat_id = node.flow_params_serializer.data.get('chat_id') chat_id = node.flow_params_serializer.data.get('chat_id')
return {'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'start_time': time.time(), return {'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'start_time': time.time(),
'history_context': history_context, 'chat_id': str(chat_id), **node.workflow_manage.form_data, 'history_context': history_context, 'chat_id': str(chat_id), **node.workflow_manage.form_data}
'chat_user_id': body.get('chat_user_id'),
'chat_user_type': body.get('chat_user_type'),
'chat_user': body.get('chat_user')}
class BaseStartStepNode(IStarNode): class BaseStartStepNode(IStarNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
base_node = self.workflow_manage.get_base_node() base_node = self.workflow_manage.get_base_node()
default_global_variable = get_default_global_variable(base_node.properties.get('user_input_field_list', [])) default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
default_api_global_variable = get_default_global_variable(base_node.properties.get('api_input_field_list', [])) workflow_variable = {**default_global_variable, **get_global_variable(self)}
workflow_variable = {**default_global_variable, **default_api_global_variable, **get_global_variable(self)}
self.context['question'] = details.get('question') self.context['question'] = details.get('question')
self.context['run_time'] = details.get('run_time') self.context['run_time'] = details.get('run_time')
self.context['document'] = details.get('document_list') self.context['document'] = details.get('document_list')
@ -61,9 +53,8 @@ class BaseStartStepNode(IStarNode):
def execute(self, question, **kwargs) -> NodeResult: def execute(self, question, **kwargs) -> NodeResult:
base_node = self.workflow_manage.get_base_node() base_node = self.workflow_manage.get_base_node()
default_global_variable = get_default_global_variable(base_node.properties.get('user_input_field_list', [])) default_global_variable = get_default_global_variable(base_node.properties.get('input_field_list', []))
default_api_global_variable = get_default_global_variable(base_node.properties.get('api_input_field_list', [])) workflow_variable = {**default_global_variable, **get_global_variable(self)}
workflow_variable = {**default_global_variable, **default_api_global_variable, **get_global_variable(self)}
""" """
开始节点 初始化全局变量 开始节点 初始化全局变量
""" """
@ -73,9 +64,7 @@ class BaseStartStepNode(IStarNode):
'document': self.workflow_manage.document_list, 'document': self.workflow_manage.document_list,
'audio': self.workflow_manage.audio_list, 'audio': self.workflow_manage.audio_list,
'other': self.workflow_manage.other_list, 'other': self.workflow_manage.other_list,
} }
self.workflow_manage.chat_context = self.workflow_manage.get_chat_info().get_chat_variable()
return NodeResult(node_variable, workflow_variable) return NodeResult(node_variable, workflow_variable)
def get_details(self, index: int, **kwargs): def get_details(self, index: int, **kwargs):

View File

@ -5,19 +5,18 @@ from typing import Type
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
class TextToSpeechNodeSerializer(serializers.Serializer): class TextToSpeechNodeSerializer(serializers.Serializer):
tts_model_id = serializers.CharField(required=True, label=_("Model id")) tts_model_id = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Model id")))
is_result = serializers.BooleanField(required=False, is_result = serializers.BooleanField(required=False, error_messages=ErrMessage.boolean(_('Whether to return content')))
label=_('Whether to return content'))
content_list = serializers.ListField(required=True, label=_("Text content")) content_list = serializers.ListField(required=True, error_messages=ErrMessage.list(_("Text content")))
model_params_setting = serializers.DictField(required=False, model_params_setting = serializers.DictField(required=False,
label=_("Model parameter settings")) error_messages=ErrMessage.integer(_("Model parameter settings")))
class ITextToSpeechNode(INode): class ITextToSpeechNode(INode):

View File

@ -4,13 +4,12 @@ import mimetypes
from django.core.files.uploadedfile import InMemoryUploadedFile from django.core.files.uploadedfile import InMemoryUploadedFile
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.image_understand_step_node.i_image_understand_node import IImageUnderstandNode
from application.flow.step_node.text_to_speech_step_node.i_text_to_speech_node import ITextToSpeechNode from application.flow.step_node.text_to_speech_step_node.i_text_to_speech_node import ITextToSpeechNode
from common.utils.common import _remove_empty_lines from dataset.models import File
from knowledge.models import FileSourceType from dataset.serializers.file_serializers import FileSerializer
from models_provider.tools import get_model_instance_by_model_workspace_id from setting.models_provider.tools import get_model_instance_by_model_user_id
from oss.serializers.file import FileSerializer
from pydub import AudioSegment
def bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"): def bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"):
@ -38,78 +37,31 @@ def bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"):
class BaseTextToSpeechNode(ITextToSpeechNode): class BaseTextToSpeechNode(ITextToSpeechNode):
def save_context(self, details, workflow_manage): def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer') self.context['answer'] = details.get('answer')
self.context['result'] = details.get('result')
if self.node_params.get('is_result', False): if self.node_params.get('is_result', False):
self.answer_text = details.get('answer') self.answer_text = details.get('answer')
def execute(self, tts_model_id, chat_id, def execute(self, tts_model_id, chat_id,
content, model_params_setting=None, content, model_params_setting=None,
max_length=1024, **kwargs) -> NodeResult: **kwargs) -> NodeResult:
# 分割文本为合理片段 self.context['content'] = content
content = _remove_empty_lines(content) model = get_model_instance_by_model_user_id(tts_model_id, self.flow_params_serializer.data.get('user_id'),
content_chunks = [content[i:i + max_length] **model_params_setting)
for i in range(0, len(content), max_length)] audio_byte = model.text_to_speech(content)
# 需要把这个音频文件存储到数据库中
# 生成并收集所有音频片段 file_name = 'generated_audio.mp3'
audio_segments = [] file = bytes_to_uploaded_file(audio_byte, file_name)
temp_files = []
for i, chunk in enumerate(content_chunks):
self.context['content'] = chunk
workspace_id = self.workflow_manage.get_body().get('workspace_id')
model = get_model_instance_by_model_workspace_id(
tts_model_id, workspace_id, **model_params_setting)
audio_byte = model.text_to_speech(chunk)
# 保存为临时音频文件用于合并
temp_file = io.BytesIO(audio_byte)
audio_segment = AudioSegment.from_file(temp_file)
audio_segments.append(audio_segment)
temp_files.append(temp_file)
# 合并所有音频片段
combined_audio = AudioSegment.empty()
for segment in audio_segments:
combined_audio += segment
# 将合并后的音频转为字节流
output_buffer = io.BytesIO()
combined_audio.export(output_buffer, format="mp3")
combined_bytes = output_buffer.getvalue()
# 存储合并后的音频文件
file_name = 'combined_audio.mp3'
file = bytes_to_uploaded_file(combined_bytes, file_name)
application = self.workflow_manage.work_flow_post_handler.chat_info.application application = self.workflow_manage.work_flow_post_handler.chat_info.application
meta = { meta = {
'debug': False if application.id else True, 'debug': False if application.id else True,
'chat_id': chat_id, 'chat_id': chat_id,
'application_id': str(application.id) if application.id else None, 'application_id': str(application.id) if application.id else None,
} }
file_url = FileSerializer(data={'file': file, 'meta': meta}).upload()
file_url = FileSerializer(data={ # 拼接一个audio标签的src属性
'file': file, audio_label = f'<audio src="{file_url}" controls style = "width: 300px; height: 43px"></audio>'
'meta': meta,
'source_id': meta['application_id'],
'source_type': FileSourceType.APPLICATION.value
}).upload()
# 生成音频标签
audio_label = f'<audio src="{file_url}" controls style="width: 300px; height: 43px"></audio>'
file_id = file_url.split('/')[-1] file_id = file_url.split('/')[-1]
audio_list = [{'file_id': file_id, 'file_name': file_name, 'url': file_url}] audio_list = [{'file_id': file_id, 'file_name': file_name, 'url': file_url}]
return NodeResult({'answer': audio_label, 'result': audio_list}, {})
# 关闭所有临时文件
for temp_file in temp_files:
temp_file.close()
output_buffer.close()
return NodeResult({
'answer': audio_label,
'result': audio_list
}, {})
def get_details(self, index: int, **kwargs): def get_details(self, index: int, **kwargs):
return { return {

View File

@ -6,11 +6,12 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult from application.flow.i_step_node import INode, NodeResult
from common.util.field_message import ErrMessage
class VariableAssignNodeParamsSerializer(serializers.Serializer): class VariableAssignNodeParamsSerializer(serializers.Serializer):
variable_list = serializers.ListField(required=True, variable_list = serializers.ListField(required=True,
label=_("Reference Field")) error_messages=ErrMessage.list(_("Reference Field")))
class IVariableAssignNode(INode): class IVariableAssignNode(INode):

View File

@ -2,11 +2,8 @@
import json import json
from typing import List from typing import List
from django.db.models import QuerySet
from application.flow.i_step_node import NodeResult from application.flow.i_step_node import NodeResult
from application.flow.step_node.variable_assign_node.i_variable_assign_node import IVariableAssignNode from application.flow.step_node.variable_assign_node.i_variable_assign_node import IVariableAssignNode
from application.models import Chat
class BaseVariableAssignNode(IVariableAssignNode): class BaseVariableAssignNode(IVariableAssignNode):
@ -14,56 +11,40 @@ class BaseVariableAssignNode(IVariableAssignNode):
self.context['variable_list'] = details.get('variable_list') self.context['variable_list'] = details.get('variable_list')
self.context['result_list'] = details.get('result_list') self.context['result_list'] = details.get('result_list')
def global_evaluation(self, variable, value):
self.workflow_manage.context[variable['fields'][1]] = value
def chat_evaluation(self, variable, value):
self.workflow_manage.chat_context[variable['fields'][1]] = value
def handle(self, variable, evaluation):
result = {
'name': variable['name'],
'input_value': self.get_reference_content(variable['fields']),
}
if variable['source'] == 'custom':
if variable['type'] == 'json':
if isinstance(variable['value'], dict) or isinstance(variable['value'], list):
val = variable['value']
else:
val = json.loads(variable['value'])
evaluation(variable, val)
result['output_value'] = variable['value'] = val
elif variable['type'] == 'string':
# 变量解析 例如:{{global.xxx}}
val = self.workflow_manage.generate_prompt(variable['value'])
evaluation(variable, val)
result['output_value'] = val
else:
val = variable['value']
evaluation(variable, val)
result['output_value'] = val
else:
reference = self.get_reference_content(variable['reference'])
evaluation(variable, reference)
result['output_value'] = reference
return result
def execute(self, variable_list, stream, **kwargs) -> NodeResult: def execute(self, variable_list, stream, **kwargs) -> NodeResult:
# #
result_list = [] result_list = []
is_chat = False
for variable in variable_list: for variable in variable_list:
if 'fields' not in variable: if 'fields' not in variable:
continue continue
if 'global' == variable['fields'][0]: if 'global' == variable['fields'][0]:
result = self.handle(variable, self.global_evaluation) result = {
'name': variable['name'],
'input_value': self.get_reference_content(variable['fields']),
}
if variable['source'] == 'custom':
if variable['type'] == 'json':
if isinstance(variable['value'], dict) or isinstance(variable['value'], list):
val = variable['value']
else:
val = json.loads(variable['value'])
self.workflow_manage.context[variable['fields'][1]] = val
result['output_value'] = variable['value'] = val
elif variable['type'] == 'string':
# 变量解析 例如:{{global.xxx}}
val = self.workflow_manage.generate_prompt(variable['value'])
self.workflow_manage.context[variable['fields'][1]] = val
result['output_value'] = val
else:
val = variable['value']
self.workflow_manage.context[variable['fields'][1]] = val
result['output_value'] = val
else:
reference = self.get_reference_content(variable['reference'])
self.workflow_manage.context[variable['fields'][1]] = reference
result['output_value'] = reference
result_list.append(result) result_list.append(result)
if 'chat' == variable['fields'][0]:
result = self.handle(variable, self.chat_evaluation)
result_list.append(result)
is_chat = True
if is_chat:
self.workflow_manage.get_chat_info().set_chat_variable(self.workflow_manage.chat_context)
return NodeResult({'variable_list': variable_list, 'result_list': result_list}, {}) return NodeResult({'variable_list': variable_list, 'result_list': result_list}, {})
def get_reference_content(self, fields: List[str]): def get_reference_content(self, fields: List[str]):

View File

@ -13,7 +13,7 @@ from django.http import StreamingHttpResponse
from langchain_core.messages import BaseMessageChunk, BaseMessage from langchain_core.messages import BaseMessageChunk, BaseMessage
from application.flow.i_step_node import WorkFlowPostHandler from application.flow.i_step_node import WorkFlowPostHandler
from common.result import result from common.response import result
class Reasoning: class Reasoning:

View File

@ -15,21 +15,165 @@ from functools import reduce
from typing import List, Dict from typing import List, Dict
from django.db import close_old_connections from django.db import close_old_connections
from django.db.models import QuerySet
from django.utils import translation from django.utils import translation
from django.utils.translation import get_language from django.utils.translation import get_language
from django.utils.translation import gettext as _
from langchain_core.prompts import PromptTemplate from langchain_core.prompts import PromptTemplate
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import ErrorDetail, ValidationError
from application.flow import tools from application.flow import tools
from application.flow.common import Workflow
from application.flow.i_step_node import INode, WorkFlowPostHandler, NodeResult from application.flow.i_step_node import INode, WorkFlowPostHandler, NodeResult
from application.flow.step_node import get_node from application.flow.step_node import get_node
from common.exception.app_exception import AppApiException
from common.handle.base_to_response import BaseToResponse from common.handle.base_to_response import BaseToResponse
from common.handle.impl.response.system_to_response import SystemToResponse from common.handle.impl.response.system_to_response import SystemToResponse
from function_lib.models.function import FunctionLib
from setting.models import Model
from setting.models_provider import get_model_credential
executor = ThreadPoolExecutor(max_workers=200) executor = ThreadPoolExecutor(max_workers=200)
class Edge:
def __init__(self, _id: str, _type: str, sourceNodeId: str, targetNodeId: str, **keywords):
self.id = _id
self.type = _type
self.sourceNodeId = sourceNodeId
self.targetNodeId = targetNodeId
for keyword in keywords:
self.__setattr__(keyword, keywords.get(keyword))
class Node:
def __init__(self, _id: str, _type: str, x: int, y: int, properties: dict, **kwargs):
self.id = _id
self.type = _type
self.x = x
self.y = y
self.properties = properties
for keyword in kwargs:
self.__setattr__(keyword, kwargs.get(keyword))
end_nodes = ['ai-chat-node', 'reply-node', 'function-node', 'function-lib-node', 'application-node',
'image-understand-node', 'speech-to-text-node', 'text-to-speech-node', 'image-generate-node']
class Flow:
def __init__(self, nodes: List[Node], edges: List[Edge]):
self.nodes = nodes
self.edges = edges
@staticmethod
def new_instance(flow_obj: Dict):
nodes = flow_obj.get('nodes')
edges = flow_obj.get('edges')
nodes = [Node(node.get('id'), node.get('type'), **node)
for node in nodes]
edges = [Edge(edge.get('id'), edge.get('type'), **edge) for edge in edges]
return Flow(nodes, edges)
def get_start_node(self):
start_node_list = [node for node in self.nodes if node.id == 'start-node']
return start_node_list[0]
def get_search_node(self):
return [node for node in self.nodes if node.type == 'search-dataset-node']
def is_valid(self):
"""
校验工作流数据
"""
self.is_valid_model_params()
self.is_valid_start_node()
self.is_valid_base_node()
self.is_valid_work_flow()
@staticmethod
def is_valid_node_params(node: Node):
get_node(node.type)(node, None, None)
def is_valid_node(self, node: Node):
self.is_valid_node_params(node)
if node.type == 'condition-node':
branch_list = node.properties.get('node_data').get('branch')
for branch in branch_list:
source_anchor_id = f"{node.id}_{branch.get('id')}_right"
edge_list = [edge for edge in self.edges if edge.sourceAnchorId == source_anchor_id]
if len(edge_list) == 0:
raise AppApiException(500,
_('The branch {branch} of the {node} node needs to be connected').format(
node=node.properties.get("stepName"), branch=branch.get("type")))
else:
edge_list = [edge for edge in self.edges if edge.sourceNodeId == node.id]
if len(edge_list) == 0 and not end_nodes.__contains__(node.type):
raise AppApiException(500, _("{node} Nodes cannot be considered as end nodes").format(
node=node.properties.get("stepName")))
def get_next_nodes(self, node: Node):
edge_list = [edge for edge in self.edges if edge.sourceNodeId == node.id]
node_list = reduce(lambda x, y: [*x, *y],
[[node for node in self.nodes if node.id == edge.targetNodeId] for edge in edge_list],
[])
if len(node_list) == 0 and not end_nodes.__contains__(node.type):
raise AppApiException(500,
_("The next node that does not exist"))
return node_list
def is_valid_work_flow(self, up_node=None):
if up_node is None:
up_node = self.get_start_node()
self.is_valid_node(up_node)
next_nodes = self.get_next_nodes(up_node)
for next_node in next_nodes:
self.is_valid_work_flow(next_node)
def is_valid_start_node(self):
start_node_list = [node for node in self.nodes if node.id == 'start-node']
if len(start_node_list) == 0:
raise AppApiException(500, _('The starting node is required'))
if len(start_node_list) > 1:
raise AppApiException(500, _('There can only be one starting node'))
def is_valid_model_params(self):
node_list = [node for node in self.nodes if (node.type == 'ai-chat-node' or node.type == 'question-node')]
for node in node_list:
model = QuerySet(Model).filter(id=node.properties.get('node_data', {}).get('model_id')).first()
if model is None:
raise ValidationError(ErrorDetail(
_('The node {node} model does not exist').format(node=node.properties.get("stepName"))))
credential = get_model_credential(model.provider, model.model_type, model.model_name)
model_params_setting = node.properties.get('node_data', {}).get('model_params_setting')
model_params_setting_form = credential.get_model_params_setting_form(
model.model_name)
if model_params_setting is None:
model_params_setting = model_params_setting_form.get_default_form_data()
node.properties.get('node_data', {})['model_params_setting'] = model_params_setting
if node.properties.get('status', 200) != 200:
raise ValidationError(
ErrorDetail(_("Node {node} is unavailable").format(node.properties.get("stepName"))))
node_list = [node for node in self.nodes if (node.type == 'function-lib-node')]
for node in node_list:
function_lib_id = node.properties.get('node_data', {}).get('function_lib_id')
if function_lib_id is None:
raise ValidationError(ErrorDetail(
_('The library ID of node {node} cannot be empty').format(node=node.properties.get("stepName"))))
f_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
if f_lib is None:
raise ValidationError(ErrorDetail(_("The function library for node {node} is not available").format(
node=node.properties.get("stepName"))))
def is_valid_base_node(self):
base_node_list = [node for node in self.nodes if node.id == 'base-node']
if len(base_node_list) == 0:
raise AppApiException(500, _('Basic information node is required'))
if len(base_node_list) > 1:
raise AppApiException(500, _('There can only be one basic information node'))
class NodeResultFuture: class NodeResultFuture:
def __init__(self, r, e, status=200): def __init__(self, r, e, status=200):
self.r = r self.r = r
@ -90,7 +234,7 @@ class NodeChunkManage:
class WorkflowManage: class WorkflowManage:
def __init__(self, flow: Workflow, params, work_flow_post_handler: WorkFlowPostHandler, def __init__(self, flow: Flow, params, work_flow_post_handler: WorkFlowPostHandler,
base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None, base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None,
document_list=None, document_list=None,
audio_list=None, audio_list=None,
@ -117,7 +261,6 @@ class WorkflowManage:
self.params = params self.params = params
self.flow = flow self.flow = flow
self.context = {} self.context = {}
self.chat_context = {}
self.node_chunk_manage = NodeChunkManage(self) self.node_chunk_manage = NodeChunkManage(self)
self.work_flow_post_handler = work_flow_post_handler self.work_flow_post_handler = work_flow_post_handler
self.current_node = None self.current_node = None
@ -132,7 +275,6 @@ class WorkflowManage:
self.lock = threading.Lock() self.lock = threading.Lock()
self.field_list = [] self.field_list = []
self.global_field_list = [] self.global_field_list = []
self.chat_field_list = []
self.init_fields() self.init_fields()
if start_node_id is not None: if start_node_id is not None:
self.load_node(chat_record, start_node_id, start_node_data) self.load_node(chat_record, start_node_id, start_node_data)
@ -142,7 +284,6 @@ class WorkflowManage:
def init_fields(self): def init_fields(self):
field_list = [] field_list = []
global_field_list = [] global_field_list = []
chat_field_list = []
for node in self.flow.nodes: for node in self.flow.nodes:
properties = node.properties properties = node.properties
node_name = properties.get('stepName') node_name = properties.get('stepName')
@ -157,16 +298,10 @@ class WorkflowManage:
if global_fields is not None: if global_fields is not None:
for global_field in global_fields: for global_field in global_fields:
global_field_list.append({**global_field, 'node_id': node_id, 'node_name': node_name}) global_field_list.append({**global_field, 'node_id': node_id, 'node_name': node_name})
chat_fields = node_config.get('chatFields') field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
if chat_fields is not None: global_field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
for chat_field in chat_fields:
chat_field_list.append({**chat_field, 'node_id': node_id, 'node_name': node_name})
field_list.sort(key=lambda f: len(f.get('node_name') + f.get('value')), reverse=True)
global_field_list.sort(key=lambda f: len(f.get('node_name') + f.get('value')), reverse=True)
chat_field_list.sort(key=lambda f: len(f.get('node_name') + f.get('value')), reverse=True)
self.field_list = field_list self.field_list = field_list
self.global_field_list = global_field_list self.global_field_list = global_field_list
self.chat_field_list = chat_field_list
def append_answer(self, content): def append_answer(self, content):
self.answer += content self.answer += content
@ -232,7 +367,9 @@ class WorkflowManage:
'\n\n'.join([a.get('content') for a in answer]) for answer in '\n\n'.join([a.get('content') for a in answer]) for answer in
answer_text_list) answer_text_list)
answer_list = reduce(lambda pre, _n: [*pre, *_n], answer_text_list, []) answer_list = reduce(lambda pre, _n: [*pre, *_n], answer_text_list, [])
self.work_flow_post_handler.handler(self) self.work_flow_post_handler.handler(self.params['chat_id'], self.params['chat_record_id'],
answer_text,
self)
return self.base_to_response.to_block_response(self.params['chat_id'], return self.base_to_response.to_block_response(self.params['chat_id'],
self.params['chat_record_id'], answer_text, True self.params['chat_record_id'], answer_text, True
, message_tokens, answer_tokens, , message_tokens, answer_tokens,
@ -247,9 +384,6 @@ class WorkflowManage:
self.run_chain_async(current_node, node_result_future, language) self.run_chain_async(current_node, node_result_future, language)
return tools.to_stream_response_simple(self.await_result()) return tools.to_stream_response_simple(self.await_result())
def get_body(self):
return self.params
def is_run(self, timeout=0.5): def is_run(self, timeout=0.5):
future_list_len = len(self.future_list) future_list_len = len(self.future_list)
try: try:
@ -286,7 +420,9 @@ class WorkflowManage:
'message_tokens' in row and row.get('message_tokens') is not None]) 'message_tokens' in row and row.get('message_tokens') is not None])
answer_tokens = sum([row.get('answer_tokens') for row in details.values() if answer_tokens = sum([row.get('answer_tokens') for row in details.values() if
'answer_tokens' in row and row.get('answer_tokens') is not None]) 'answer_tokens' in row and row.get('answer_tokens') is not None])
self.work_flow_post_handler.handler(self) self.work_flow_post_handler.handler(self.params['chat_id'], self.params['chat_record_id'],
self.answer,
self)
yield self.base_to_response.to_stream_chunk_response(self.params['chat_id'], yield self.base_to_response.to_stream_chunk_response(self.params['chat_id'],
self.params['chat_record_id'], self.params['chat_record_id'],
'', '',
@ -454,9 +590,6 @@ class WorkflowManage:
return current_node.node_params.get('is_result', not self._has_next_node( return current_node.node_params.get('is_result', not self._has_next_node(
current_node, current_node_result)) if current_node.node_params is not None else False current_node, current_node_result)) if current_node.node_params is not None else False
def get_chat_info(self):
return self.work_flow_post_handler.chat_info
def get_chunk_content(self, chunk, is_end=False): def get_chunk_content(self, chunk, is_end=False):
return 'data: ' + json.dumps( return 'data: ' + json.dumps(
{'chat_id': self.params['chat_id'], 'id': self.params['chat_record_id'], 'operate': True, {'chat_id': self.params['chat_id'], 'id': self.params['chat_record_id'], 'operate': True,
@ -466,14 +599,15 @@ class WorkflowManage:
""" """
是否有下一个可运行的节点 是否有下一个可运行的节点
""" """
next_edge_node_list = self.flow.get_next_edge_nodes(current_node.id) or [] if node_result is not None and node_result.is_assertion_result():
for next_edge_node in next_edge_node_list: for edge in self.flow.edges:
if node_result is not None and node_result.is_assertion_result():
edge = next_edge_node.edge
if (edge.sourceNodeId == current_node.id and if (edge.sourceNodeId == current_node.id and
f"{edge.sourceNodeId}_{node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId): f"{edge.sourceNodeId}_{node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId):
return True return True
return len(next_edge_node_list) > 0 else:
for edge in self.flow.edges:
if edge.sourceNodeId == current_node.id:
return True
def has_next_node(self, node_result: NodeResult | None): def has_next_node(self, node_result: NodeResult | None):
""" """
@ -523,6 +657,26 @@ class WorkflowManage:
return [[]] return [[]]
return [[item.to_dict() for item in r] for r in result] return [[item.to_dict() for item in r] for r in result]
def get_next_node(self):
"""
获取下一个可运行的所有节点
"""
if self.current_node is None:
node = self.get_start_node()
node_instance = get_node(node.type)(node, self.params, self)
return node_instance
if self.current_result is not None and self.current_result.is_assertion_result():
for edge in self.flow.edges:
if (edge.sourceNodeId == self.current_node.id and
f"{edge.sourceNodeId}_{self.current_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId):
return self.get_node_cls_by_id(edge.targetNodeId)
else:
for edge in self.flow.edges:
if edge.sourceNodeId == self.current_node.id:
return self.get_node_cls_by_id(edge.targetNodeId)
return None
@staticmethod @staticmethod
def dependent_node(up_node_id, node): def dependent_node(up_node_id, node):
if not node.node_chunk.is_end(): if not node.node_chunk.is_end():
@ -559,14 +713,14 @@ class WorkflowManage:
if current_node_result.is_interrupt_exec(current_node): if current_node_result.is_interrupt_exec(current_node):
return [] return []
node_list = [] node_list = []
next_edge_node_list = self.flow.get_next_edge_nodes(current_node.id) or []
if current_node_result is not None and current_node_result.is_assertion_result(): if current_node_result is not None and current_node_result.is_assertion_result():
for edge_node in next_edge_node_list: for edge in self.flow.edges:
edge = edge_node.edge if (edge.sourceNodeId == current_node.id and
next_node = edge_node.node
if (
f"{edge.sourceNodeId}_{current_node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId): f"{edge.sourceNodeId}_{current_node_result.node_variable.get('branch_id')}_right" == edge.sourceAnchorId):
if next_node.properties.get('condition', "AND") == 'AND': next_node = [node for node in self.flow.nodes if node.id == edge.targetNodeId]
if len(next_node) == 0:
continue
if next_node[0].properties.get('condition', "AND") == 'AND':
if self.dependent_node_been_executed(edge.targetNodeId): if self.dependent_node_been_executed(edge.targetNodeId):
node_list.append( node_list.append(
self.get_node_cls_by_id(edge.targetNodeId, self.get_node_cls_by_id(edge.targetNodeId,
@ -576,19 +730,20 @@ class WorkflowManage:
self.get_node_cls_by_id(edge.targetNodeId, self.get_node_cls_by_id(edge.targetNodeId,
[*current_node.up_node_id_list, current_node.node.id])) [*current_node.up_node_id_list, current_node.node.id]))
else: else:
for edge_node in next_edge_node_list: for edge in self.flow.edges:
edge = edge_node.edge if edge.sourceNodeId == current_node.id:
next_node = edge_node.node next_node = [node for node in self.flow.nodes if node.id == edge.targetNodeId]
if next_node.properties.get('condition', "AND") == 'AND': if len(next_node) == 0:
if self.dependent_node_been_executed(edge.targetNodeId): continue
if next_node[0].properties.get('condition', "AND") == 'AND':
if self.dependent_node_been_executed(edge.targetNodeId):
node_list.append(
self.get_node_cls_by_id(edge.targetNodeId,
[*current_node.up_node_id_list, current_node.node.id]))
else:
node_list.append( node_list.append(
self.get_node_cls_by_id(edge.targetNodeId, self.get_node_cls_by_id(edge.targetNodeId,
[*current_node.up_node_id_list, current_node.node.id])) [*current_node.up_node_id_list, current_node.node.id]))
else:
node_list.append(
self.get_node_cls_by_id(edge.targetNodeId,
[*current_node.up_node_id_list, current_node.node.id]))
return node_list return node_list
def get_reference_field(self, node_id: str, fields: List[str]): def get_reference_field(self, node_id: str, fields: List[str]):
@ -599,18 +754,12 @@ class WorkflowManage:
""" """
if node_id == 'global': if node_id == 'global':
return INode.get_field(self.context, fields) return INode.get_field(self.context, fields)
elif node_id == 'chat':
return INode.get_field(self.chat_context, fields)
else: else:
node = self.get_node_by_id(node_id) return self.get_node_by_id(node_id).get_reference_field(fields)
if node:
return node.get_reference_field(fields)
return None
def get_workflow_content(self): def get_workflow_content(self):
context = { context = {
'global': self.context, 'global': self.context,
'chat': self.chat_context
} }
for node in self.node_context: for node in self.node_context:
@ -628,10 +777,6 @@ class WorkflowManage:
globeLabelNew = f"global.{field.get('value')}" globeLabelNew = f"global.{field.get('value')}"
globeValue = f"context.get('global').get('{field.get('value', '')}','')" globeValue = f"context.get('global').get('{field.get('value', '')}','')"
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue) prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
for field in self.chat_field_list:
chatLabel = f"chat.{field.get('value')}"
chatValue = f"context.get('chat').get('{field.get('value', '')}','')"
prompt = prompt.replace(chatLabel, chatValue)
return prompt return prompt
def generate_prompt(self, prompt: str): def generate_prompt(self, prompt: str):

View File

@ -1,20 +1,10 @@
# Generated by Django 5.2.4 on 2025-07-14 11:45 # Generated by Django 4.1.10 on 2024-03-18 16:02
from django.db.models import QuerySet
import application.models.application import application.models.application
import application.models.application_chat
import common.encoder.encoder
import django.contrib.postgres.fields import django.contrib.postgres.fields
import django.db.models.deletion
import mptt.fields
import uuid_utils.compat
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
import uuid
def insert_default_data(apps, schema_editor):
# 创建一个根模块(没有父节点)
QuerySet(application.models.application.ApplicationFolder).create(id='default', name='根目录',
user_id='f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', workspace_id='default')
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -22,8 +12,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('knowledge', '0001_initial'), ('dataset', '0001_initial'),
('models_provider', '0001_initial'), ('setting', '0001_initial'),
('users', '0001_initial'), ('users', '0001_initial'),
] ]
@ -31,196 +21,65 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Application', name='Application',
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')), ('name', models.CharField(max_length=128, verbose_name='应用名称')),
('is_publish', models.BooleanField(default=False, verbose_name='是否发布')),
('name', models.CharField(db_index=True, max_length=128, verbose_name='应用名称')),
('desc', models.CharField(default='', max_length=512, verbose_name='引用描述')), ('desc', models.CharField(default='', max_length=512, verbose_name='引用描述')),
('prologue', models.CharField(default='', max_length=40960, verbose_name='开场白')), ('prologue', models.CharField(default='', max_length=1024, verbose_name='开场白')),
('dialogue_number', models.IntegerField(default=0, verbose_name='会话数量')), ('dialogue_number', models.IntegerField(default=0, verbose_name='会话数量')),
('knowledge_setting', models.JSONField(default=application.models.application.get_dataset_setting_dict, verbose_name='数据集参数设置')), ('dataset_setting', models.JSONField(default=application.models.application.get_dataset_setting_dict, verbose_name='数据集参数设置')),
('model_setting', models.JSONField(default=application.models.application.get_model_setting_dict, verbose_name='模型参数相关设置')), ('model_setting', models.JSONField(default=application.models.application.get_model_setting_dict, verbose_name='模型参数相关设置')),
('model_params_setting', models.JSONField(default=dict, verbose_name='模型参数相关设置')),
('tts_model_params_setting', models.JSONField(default=dict, verbose_name='模型参数相关设置')),
('problem_optimization', models.BooleanField(default=False, verbose_name='问题优化')), ('problem_optimization', models.BooleanField(default=False, verbose_name='问题优化')),
('icon', models.CharField(default='./favicon.ico', max_length=256, verbose_name='应用icon')), ('model', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='setting.model')),
('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user')),
('type', models.CharField(choices=[('SIMPLE', '简易'), ('WORK_FLOW', '工作流')], default='SIMPLE', max_length=256, verbose_name='应用类型')),
('problem_optimization_prompt', models.CharField(blank=True, default='()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在<data></data>标签中', max_length=102400, null=True, verbose_name='问题优化提示词')),
('tts_model_enable', models.BooleanField(default=False, verbose_name='语音合成模型是否启用')),
('stt_model_enable', models.BooleanField(default=False, verbose_name='语音识别模型是否启用')),
('tts_type', models.CharField(default='BROWSER', max_length=20, verbose_name='语音播放类型')),
('tts_autoplay', models.BooleanField(default=False, verbose_name='自动播放')),
('stt_autosend', models.BooleanField(default=False, verbose_name='自动发送')),
('clean_time', models.IntegerField(default=180, verbose_name='清理时间')),
('publish_time', models.DateTimeField(blank=True, default=None, null=True, verbose_name='发布时间')),
('file_upload_enable', models.BooleanField(default=False, verbose_name='文件上传是否启用')),
('file_upload_setting', models.JSONField(default=dict, verbose_name='文件上传相关设置')),
('model', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='models_provider.model')),
('stt_model', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stt_model_id', to='models_provider.model')),
('tts_model', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tts_model_id', to='models_provider.model')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user')),
], ],
options={ options={
'db_table': 'application', 'db_table': 'application',
}, },
), ),
migrations.CreateModel(
name='ApplicationAccessToken',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='application.application', verbose_name='应用id')),
('access_token', models.CharField(max_length=128, unique=True, verbose_name='用户公开访问 认证token')),
('is_active', models.BooleanField(default=True, verbose_name='是否开启公开访问')),
('access_num', models.IntegerField(default=100, verbose_name='访问次数')),
('white_active', models.BooleanField(default=False, verbose_name='是否开启白名单')),
('white_list', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=128), default=list, size=None, verbose_name='白名单列表')),
('show_source', models.BooleanField(default=False, verbose_name='是否显示知识来源')),
('show_exec', models.BooleanField(default=False, verbose_name='是否显示执行详情')),
('authentication', models.BooleanField(default=False, verbose_name='是否需要认证')),
('authentication_value', models.JSONField(default=dict, verbose_name='认证的值')),
('language', models.CharField(default=None, max_length=10, null=True, verbose_name='语言')),
],
options={
'db_table': 'application_access_token',
},
),
migrations.CreateModel(
name='ApplicationApiKey',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('secret_key', models.CharField(max_length=1024, unique=True, verbose_name='秘钥')),
('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('is_active', models.BooleanField(default=True, verbose_name='是否开启')),
('allow_cross_domain', models.BooleanField(default=False, verbose_name='是否允许跨域')),
('cross_domain_list', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=128), default=list, size=None, verbose_name='跨域列表')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application', verbose_name='应用id')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')),
],
options={
'db_table': 'application_api_key',
},
),
migrations.CreateModel(
name='ApplicationFolder',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.CharField(editable=False, max_length=64, primary_key=True, serialize=False, verbose_name='主键id')),
('name', models.CharField(db_index=True, max_length=64, verbose_name='文件夹名称')),
('desc', models.CharField(blank=True, max_length=200, null=True, verbose_name='描述')),
('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='application.applicationfolder')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user')),
],
options={
'db_table': 'application_folder',
},
),
migrations.AddField(
model_name='application',
name='folder',
field=models.ForeignKey(default='default', on_delete=django.db.models.deletion.DO_NOTHING, to='application.applicationfolder', verbose_name='文件夹id'),
),
migrations.CreateModel(
name='ApplicationKnowledgeMapping',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='application.application')),
('knowledge', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='knowledge.knowledge')),
],
options={
'db_table': 'application_knowledge_mapping',
},
),
migrations.CreateModel(
name='ApplicationVersion',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('name', models.CharField(default='', max_length=128, verbose_name='版本名称')),
('publish_user_id', models.UUIDField(default=None, null=True, verbose_name='发布者id')),
('publish_user_name', models.CharField(default='', max_length=128, verbose_name='发布者名称')),
('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('application_name', models.CharField(max_length=128, verbose_name='应用名称')),
('desc', models.CharField(default='', max_length=512, verbose_name='引用描述')),
('prologue', models.CharField(default='', max_length=40960, verbose_name='开场白')),
('dialogue_number', models.IntegerField(default=0, verbose_name='会话数量')),
('model_id', models.UUIDField(blank=True, null=True, verbose_name='大语言模型')),
('knowledge_setting', models.JSONField(default=application.models.application.get_dataset_setting_dict, verbose_name='数据集参数设置')),
('model_setting', models.JSONField(default=application.models.application.get_model_setting_dict, verbose_name='模型参数相关设置')),
('model_params_setting', models.JSONField(default=dict, verbose_name='模型参数相关设置')),
('tts_model_params_setting', models.JSONField(default=dict, verbose_name='模型参数相关设置')),
('problem_optimization', models.BooleanField(default=False, verbose_name='问题优化')),
('icon', models.CharField(default='./favicon.ico', max_length=256, verbose_name='应用icon')),
('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')),
('type', models.CharField(choices=[('SIMPLE', '简易'), ('WORK_FLOW', '工作流')], default='SIMPLE', max_length=256, verbose_name='应用类型')),
('problem_optimization_prompt', models.CharField(blank=True, default='()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在<data></data>标签中', max_length=102400, null=True, verbose_name='问题优化提示词')),
('tts_model_id', models.UUIDField(blank=True, null=True, verbose_name='文本转语音模型id')),
('stt_model_id', models.UUIDField(blank=True, null=True, verbose_name='语音转文本模型id')),
('tts_model_enable', models.BooleanField(default=False, verbose_name='语音合成模型是否启用')),
('stt_model_enable', models.BooleanField(default=False, verbose_name='语音识别模型是否启用')),
('tts_type', models.CharField(default='BROWSER', max_length=20, verbose_name='语音播放类型')),
('tts_autoplay', models.BooleanField(default=False, verbose_name='自动播放')),
('stt_autosend', models.BooleanField(default=False, verbose_name='自动发送')),
('clean_time', models.IntegerField(default=180, verbose_name='清理时间')),
('file_upload_enable', models.BooleanField(default=False, verbose_name='文件上传是否启用')),
('file_upload_setting', models.JSONField(default=dict, verbose_name='文件上传相关设置')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.user')),
],
options={
'db_table': 'application_version',
},
),
migrations.CreateModel( migrations.CreateModel(
name='Chat', name='Chat',
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('abstract', models.CharField(max_length=1024, verbose_name='摘要')), ('abstract', models.CharField(max_length=256, verbose_name='摘要')),
('chat_user_id', models.CharField(default=None, null=True, verbose_name='对话用户id')),
('chat_user_type', models.CharField(choices=[('ANONYMOUS_USER', '匿名用户'), ('CHAT_USER', '对话用户'), ('SYSTEM_API_KEY', '系统API_KEY'), ('APPLICATION_API_KEY', '应用API_KEY'), ('PLATFORM_USER', '平台用户')], default='ANONYMOUS_USER', max_length=64, verbose_name='客户端类型')),
('is_deleted', models.BooleanField(default=False, verbose_name='逻辑删除')),
('asker', models.JSONField(default=application.models.application_chat.default_asker, encoder=common.encoder.encoder.SystemEncoder, verbose_name='访问者')),
('meta', models.JSONField(default=dict, verbose_name='元数据')),
('star_num', models.IntegerField(default=0, verbose_name='点赞数量')),
('trample_num', models.IntegerField(default=0, verbose_name='点踩数量')),
('chat_record_count', models.IntegerField(default=0, verbose_name='对话次数')),
('mark_sum', models.IntegerField(default=0, verbose_name='标记数量')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')), ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
], ],
options={ options={
'db_table': 'application_chat', 'db_table': 'application_chat',
}, },
), ),
migrations.CreateModel(
name='ApplicationAccessToken',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='application.application', verbose_name='应用id')),
('access_token', models.CharField(max_length=128, unique=True, verbose_name='用户公开访问 认证token')),
('is_active', models.BooleanField(default=True, verbose_name='是否开启公开访问')),
('access_num', models.IntegerField(default=100, verbose_name='访问次数')),
('white_active', models.BooleanField(default=False, verbose_name='是否开启白名单')),
('white_list', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=128), default=list, size=None, verbose_name='白名单列表')),
],
options={
'db_table': 'application_access_token',
},
),
migrations.CreateModel( migrations.CreateModel(
name='ChatRecord', name='ChatRecord',
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('vote_status', models.CharField(choices=[('-1', '未投票'), ('0', '赞同'), ('1', '反对')], default='-1', max_length=10, verbose_name='投票')), ('vote_status', models.CharField(choices=[('-1', '未投票'), ('0', '赞同'), ('1', '反对')], default='-1', max_length=10, verbose_name='投票')),
('problem_text', models.CharField(max_length=10240, verbose_name='问题')), ('problem_text', models.CharField(max_length=1024, verbose_name='问题')),
('answer_text', models.CharField(max_length=40960, verbose_name='答案')), ('answer_text', models.CharField(max_length=4096, verbose_name='答案')),
('answer_text_list', django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(), default=list, size=None, verbose_name='改进标注列表')),
('message_tokens', models.IntegerField(default=0, verbose_name='请求token数量')), ('message_tokens', models.IntegerField(default=0, verbose_name='请求token数量')),
('answer_tokens', models.IntegerField(default=0, verbose_name='响应token数量')), ('answer_tokens', models.IntegerField(default=0, verbose_name='响应token数量')),
('const', models.IntegerField(default=0, verbose_name='总费用')), ('const', models.IntegerField(default=0, verbose_name='总费用')),
('details', models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='对话详情')), ('details', models.JSONField(default=dict, verbose_name='对话详情')),
('improve_paragraph_id_list', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True), default=list, size=None, verbose_name='改进标注列表')), ('improve_paragraph_id_list', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(blank=True), default=list, size=None, verbose_name='改进标注列表')),
('run_time', models.FloatField(default=0, verbose_name='运行时长')), ('run_time', models.FloatField(default=0, verbose_name='运行时长')),
('index', models.IntegerField(verbose_name='对话下标')), ('index', models.IntegerField(verbose_name='对话下标')),
@ -231,21 +90,45 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ApplicationChatUserStats', name='ApplicationPublicAccessClient',
fields=[ fields=[
('create_time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='修改时间')), ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')), ('id', models.UUIDField(primary_key=True, serialize=False, verbose_name='公共访问链接客户端id')),
('chat_user_id', models.UUIDField(default=uuid_utils.compat.uuid7, verbose_name='对话用户id')),
('chat_user_type', models.CharField(choices=[('ANONYMOUS_USER', '匿名用户'), ('CHAT_USER', '对话用户'), ('SYSTEM_API_KEY', '系统API_KEY'), ('APPLICATION_API_KEY', '应用API_KEY'), ('PLATFORM_USER', '平台用户')], default='ANONYMOUS_USER', max_length=64, verbose_name='对话用户类型')),
('access_num', models.IntegerField(default=0, verbose_name='访问总次数次数')), ('access_num', models.IntegerField(default=0, verbose_name='访问总次数次数')),
('intraday_access_num', models.IntegerField(default=0, verbose_name='当日访问次数')), ('intraday_access_num', models.IntegerField(default=0, verbose_name='当日访问次数')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application', verbose_name='应用id')), ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application', verbose_name='应用id')),
], ],
options={ options={
'db_table': 'application_chat_user_stats', 'db_table': 'application_public_access_client',
'indexes': [models.Index(fields=['application_id', 'chat_user_id'], name='application_applica_1652ba_idx')], },
),
migrations.CreateModel(
name='ApplicationDatasetMapping',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dataset.dataset')),
],
options={
'db_table': 'application_dataset_mapping',
},
),
migrations.CreateModel(
name='ApplicationApiKey',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('secret_key', models.CharField(max_length=1024, unique=True, verbose_name='秘钥')),
('is_active', models.BooleanField(default=True, verbose_name='是否开启')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application', verbose_name='应用id')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')),
],
options={
'db_table': 'application_api_key',
}, },
), ),
migrations.RunPython(insert_default_data)
] ]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-03-28 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='chat',
name='client_id',
field=models.UUIDField(default=None, null=True, verbose_name='客户端id'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-04-23 11:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0002_chat_client_id'),
]
operations = [
migrations.AddField(
model_name='application',
name='icon',
field=models.CharField(default='/ui/favicon.ico', max_length=256, verbose_name='应用icon'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-04-25 11:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0003_application_icon'),
]
operations = [
migrations.AddField(
model_name='applicationaccesstoken',
name='show_source',
field=models.BooleanField(default=False, verbose_name='是否显示知识来源'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.13 on 2024-04-29 13:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0004_applicationaccesstoken_show_source'),
]
operations = [
migrations.AlterField(
model_name='chat',
name='abstract',
field=models.CharField(max_length=1024, verbose_name='摘要'),
),
migrations.AlterField(
model_name='chatrecord',
name='answer_text',
field=models.CharField(max_length=40960, verbose_name='答案'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.13 on 2024-05-08 13:57
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0005_alter_chat_abstract_alter_chatrecord_answer_text'),
]
operations = [
migrations.AddField(
model_name='applicationapikey',
name='allow_cross_domain',
field=models.BooleanField(default=False, verbose_name='是否允许跨域'),
),
migrations.AddField(
model_name='applicationapikey',
name='cross_domain_list',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(blank=True, max_length=128), default=list, size=None, verbose_name='跨域列表'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-05-24 11:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0006_applicationapikey_allow_cross_domain_and_more'),
]
operations = [
migrations.AlterField(
model_name='application',
name='prologue',
field=models.CharField(default='', max_length=4096, verbose_name='开场白'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.13 on 2024-06-13 11:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0007_alter_application_prologue'),
]
operations = [
migrations.AddField(
model_name='chat',
name='is_deleted',
field=models.BooleanField(default=False, verbose_name=''),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 4.1.13 on 2024-06-25 16:30
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('application', '0008_chat_is_deleted'),
]
operations = [
migrations.AddField(
model_name='application',
name='type',
field=models.CharField(choices=[('SIMPLE', '简易'), ('WORK_FLOW', '工作流')], default='SIMPLE', max_length=256, verbose_name='应用类型'),
),
migrations.AddField(
model_name='application',
name='work_flow',
field=models.JSONField(default=dict, verbose_name='工作流数据'),
),
migrations.CreateModel(
name='WorkFlowVersion',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('work_flow', models.JSONField(default=dict, verbose_name='工作流数据')),
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='application.application')),
],
options={
'db_table': 'application_work_flow_version',
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.13 on 2024-07-15 15:52
from django.db import migrations, models
import common.encoder.encoder
class Migration(migrations.Migration):
dependencies = [
('application', '0009_application_type_application_work_flow_and_more'),
]
operations = [
migrations.AlterField(
model_name='chatrecord',
name='details',
field=models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, verbose_name='对话详情'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-08-23 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0010_alter_chatrecord_details'),
]
operations = [
migrations.AddField(
model_name='application',
name='model_params_setting',
field=models.JSONField(default=dict, verbose_name='模型参数相关设置'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.15 on 2024-09-05 14:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('setting', '0006_alter_model_status'),
('application', '0011_application_model_params_setting'),
]
operations = [
migrations.AddField(
model_name='application',
name='stt_model',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stt_model_id', to='setting.model'),
),
migrations.AddField(
model_name='application',
name='stt_model_enable',
field=models.BooleanField(default=False, verbose_name='语音识别模型是否启用'),
),
migrations.AddField(
model_name='application',
name='tts_model',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tts_model_id', to='setting.model'),
),
migrations.AddField(
model_name='application',
name='tts_model_enable',
field=models.BooleanField(default=False, verbose_name='语音合成模型是否启用'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-09-12 11:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0012_application_stt_model_application_stt_model_enable_and_more'),
]
operations = [
migrations.AddField(
model_name='application',
name='tts_type',
field=models.CharField(default='BROWSER', max_length=20, verbose_name='语音播放类型'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-09-13 18:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0013_application_tts_type'),
]
operations = [
migrations.AddField(
model_name='application',
name='problem_optimization_prompt',
field=models.CharField(blank=True, default='()里面是用户问题,根据上下文回答揣测用户问题({question}) 要求: 输出一个补全问题,并且放在<data></data>标签中', max_length=102400, null=True, verbose_name='问题优化提示词'),
),
]

View File

@ -0,0 +1,63 @@
# Generated by Django 4.2.15 on 2024-09-18 16:14
import logging
import psycopg2
from django.db import migrations
from psycopg2 import extensions
from smartdoc.const import CONFIG
def get_connect(db_name):
conn_params = {
"dbname": db_name,
"user": CONFIG.get('DB_USER'),
"password": CONFIG.get('DB_PASSWORD'),
"host": CONFIG.get('DB_HOST'),
"port": CONFIG.get('DB_PORT')
}
# 建立连接
connect = psycopg2.connect(**conn_params)
return connect
def sql_execute(conn, reindex_sql: str, alter_database_sql: str):
"""
执行一条sql
@param reindex_sql:
@param conn:
@param alter_database_sql:
"""
conn.set_isolation_level(extensions.ISOLATION_LEVEL_AUTOCOMMIT)
with conn.cursor() as cursor:
cursor.execute(reindex_sql, [])
cursor.execute(alter_database_sql, [])
cursor.close()
def re_index(apps, schema_editor):
app_db_name = CONFIG.get('DB_NAME')
try:
re_index_database(app_db_name)
except Exception as e:
logging.error(f'reindex database {app_db_name}发送错误:{str(e)}')
try:
re_index_database('root')
except Exception as e:
logging.error(f'reindex database root 发送错误:{str(e)}')
def re_index_database(db_name):
db_conn = get_connect(db_name)
sql_execute(db_conn, f'REINDEX DATABASE "{db_name}";', f'ALTER DATABASE "{db_name}" REFRESH COLLATION VERSION;')
db_conn.close()
class Migration(migrations.Migration):
dependencies = [
('application', '0014_application_problem_optimization_prompt'),
]
operations = [
migrations.RunPython(re_index, atomic=False)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-09-26 13:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0015_re_database_index'),
]
operations = [
migrations.AlterField(
model_name='chatrecord',
name='problem_text',
field=models.CharField(max_length=10240, verbose_name='问题'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2024-10-16 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0016_alter_chatrecord_problem_text'),
]
operations = [
migrations.AddField(
model_name='application',
name='tts_model_params_setting',
field=models.JSONField(default=dict, verbose_name='模型参数相关设置'),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 4.2.15 on 2024-10-16 15:17
from django.db import migrations, models
sql = """
UPDATE "public".application_work_flow_version
SET "name" = TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS');
"""
class Migration(migrations.Migration):
dependencies = [
('application', '0017_application_tts_model_params_setting'),
]
operations = [
migrations.AddField(
model_name='application',
name='clean_time',
field=models.IntegerField(default=180, verbose_name='清理时间'),
),
migrations.AddField(
model_name='workflowversion',
name='name',
field=models.CharField(default='', max_length=128, verbose_name='版本名称'),
),
migrations.RunSQL(sql),
migrations.AddField(
model_name='workflowversion',
name='publish_user_id',
field=models.UUIDField(default=None, null=True, verbose_name='发布者id'),
),
migrations.AddField(
model_name='workflowversion',
name='publish_user_name',
field=models.CharField(default='', max_length=128, verbose_name='发布者名称'),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 4.2.15 on 2024-11-13 10:13
import django.contrib.postgres.fields
from django.db import migrations, models
sql = """
UPDATE application_chat_record
SET answer_text_list=ARRAY[jsonb_build_object('content',answer_text)]
"""
class Migration(migrations.Migration):
dependencies = [
('application', '0018_workflowversion_name'),
]
operations = [
migrations.AddField(
model_name='application',
name='file_upload_enable',
field=models.BooleanField(default=False, verbose_name='文件上传是否启用'),
),
migrations.AddField(
model_name='application',
name='file_upload_setting',
field=models.JSONField(default=dict, verbose_name='文件上传相关设置'),
),
migrations.AddField(
model_name='chatrecord',
name='answer_text_list',
field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(), default=list, size=None, verbose_name='改进标注列表')
),
migrations.RunSQL(sql)
]

View File

@ -0,0 +1,22 @@
from django.db import migrations, connection
batch_update_update_time = """
UPDATE application_chat ac
SET update_time = acr_max.max_update_time
FROM (
SELECT chat_id, MAX(update_time) AS max_update_time
FROM application_chat_record
GROUP BY chat_id
) acr_max
WHERE ac.id = acr_max.chat_id;
"""
class Migration(migrations.Migration):
dependencies = [
('application', '0019_application_file_upload_enable_and_more'),
]
operations = [
migrations.RunSQL(batch_update_update_time),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 4.2.15 on 2024-12-27 18:42
from django.db import migrations, models
import uuid
run_sql = """
UPDATE application_public_access_client
SET client_id="id"
"""
class Migration(migrations.Migration):
dependencies = [
('application', '0020_application_record_update_time'),
]
operations = [
migrations.AddField(
model_name='applicationpublicaccessclient',
name='client_id',
field=models.UUIDField(default=uuid.uuid1, verbose_name='公共访问链接客户端id'),
),
migrations.AlterField(
model_name='applicationpublicaccessclient',
name='id',
field=models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False,
verbose_name='主键id'),
),
migrations.AddIndex(
model_name='applicationpublicaccessclient',
index=models.Index(fields=['client_id'], name='application_client__4de9af_idx'),
),
migrations.RunSQL(run_sql)
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2025-01-03 14:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0021_applicationpublicaccessclient_client_id_and_more'),
]
operations = [
migrations.AddField(
model_name='application',
name='tts_autoplay',
field=models.BooleanField(default=False, verbose_name='自动播放'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2025-01-06 10:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0022_application_tts_autoplay'),
]
operations = [
migrations.AddField(
model_name='application',
name='stt_autosend',
field=models.BooleanField(default=False, verbose_name='自动发送'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.15 on 2025-01-20 06:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0023_application_stt_autosend'),
]
operations = [
migrations.AddField(
model_name='applicationaccesstoken',
name='language',
field=models.CharField(default=None, max_length=10, null=True, verbose_name='语言')
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.18 on 2025-01-22 09:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0024_applicationaccesstoken_language'),
]
operations = [
migrations.AlterField(
model_name='application',
name='prologue',
field=models.CharField(default='', max_length=40960, verbose_name='开场白'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.18 on 2025-03-18 06:05
import application.models.application
import common.encoder.encoder
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('application', '0025_alter_application_prologue'),
]
operations = [
migrations.AddField(
model_name='chat',
name='asker',
field=models.JSONField(default=application.models.application.default_asker, encoder=common.encoder.encoder.SystemEncoder, verbose_name='访问者'),
),
]

View File

@ -1,12 +1,9 @@
# coding=utf-8 # coding=utf-8
""" """
@project: MaxKB @project: maxkb
@Author @Author
@file __init__.py @file __init__.py
@date2025/5/7 15:14 @date2023/9/25 14:25
@desc: @desc:
""" """
from .application import * from .application import *
from .application_access_token import *
from .application_chat import *
from .application_api_key import *

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