Compare commits

..

403 Commits

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
maninhill ce7efd4758
chore: Update USE-CASES.md (#2850)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-10 17:11:50 +08:00
liqiang-fit2cloud 1695710cbe Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-10 16:42:08 +08:00
wangdan-fit2cloud b0366b18b6
fix: Optimize dialogue style (#2849) 2025-04-10 16:13:37 +08:00
wxg0103 a4f27249ed fix: openai error
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
2025-04-10 14:41:05 +08:00
liqiang-fit2cloud ebe8506c67 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-10 14:37:17 +08:00
shaohuzhang1 5243e42100
fix: MD editor table cannot have too many columns and cannot scroll horizontally (#2845) 2025-04-10 13:54:54 +08:00
wangdan-fit2cloud e5738f3b31
fix: History record error in dialogue basic mode (#2844) 2025-04-10 11:36:05 +08:00
liqiang-fit2cloud 3c3bd9884f Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-10 09:48:40 +08:00
shaohuzhang1 0213aff12a
fix: Historical user conversation data has been cleared (#2843)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-10 09:48:14 +08:00
shaohuzhang1 8dc793a128
fix: Historical user conversation data has been cleared (#2840)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-09 19:16:42 +08:00
shaohuzhang1 0861eb4cdc
fix: Invalid verification of dialogue user form (#2839)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-09 18:11:50 +08:00
wxg0103 d78c1459b7 refactor: oauth2 state 2025-04-09 17:56:45 +08:00
liqiang-fit2cloud f188383fea Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 17:33:23 +08:00
wangdan-fit2cloud 7421caba9b fix: Optimize details 2025-04-09 16:49:41 +08:00
liqiang-fit2cloud bbab359813 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 16:33:28 +08:00
shaohuzhang1 deb3844b4c
fix: Judgment tool drag and drop sorting (#2837) 2025-04-09 15:34:36 +08:00
maninhill 26f36ccee1
chore: Update README.md (#2835) 2025-04-09 15:15:25 +08:00
liqiang-fit2cloud 8381ca5287 Merge branch 'main' of https://github.com/maxkb-dev/maxkb 2025-04-09 15:08:32 +08:00
maninhill e8c1cdf959
chore: Update README.md (#2834) 2025-04-09 13:48:44 +08:00
wangdan-fit2cloud 675d2366da
fix: condition node supports drag and drop 2025-04-09 11:52:54 +08:00
wangdan-fit2cloud bbb63a5928
fix: chat avatar display issue fixed (#2832) 2025-04-09 10:56:58 +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 4ae02c8d3e security: fix reverse shell vulnerability in function library.
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-09 10:01:11 +08:00
liqiang-fit2cloud c0ffc0aaf5 security: fix reverse shell vulnerability in function library. 2025-04-09 09:57:11 +08:00
shaohuzhang1 3557ea50fa
fix: Missing parameters such as character length when creating a knowledge base (#2827)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-08 17:24:33 +08:00
shaohuzhang1 43702e42b8
fix: When switching application models, the model parameters should be set to the default values of the current model (#2826) 2025-04-08 16:43:22 +08:00
wangdan-fit2cloud 7a3a975645
fix: After modifying the application name and publishing , name was not updated(#2799) 2025-04-08 16:00:24 +08:00
shaohuzhang1 9da7a553bf
fix: The form recall node cannot respond (#2824) 2025-04-08 15:50:57 +08:00
wangdan-fit2cloud 2d6d16e046
feat: Optimize the interface when there are too many interface parameters.(#2795)
* perf: Optimization drag sorting condition nodes

* feat: Optimize the interface when there are too many interface parameters.(#2795)
2025-04-08 15:10:53 +08:00
wangdan-fit2cloud 919a3eee5d fix: Fix type errors 2025-04-08 10:53:47 +08:00
shaohuzhang1 1a704f1c25
fix: Workflow debugging for authorized applications will result in an error message indicating unauthorized access to the model (#2819) 2025-04-08 10:51:35 +08:00
wxg0103 7ca0a7bd02 fix: Missing fields in exported logs
--bug=1054500 --user=王孝刚 【应用】对话日志导出,没有携带用户信息 https://www.tapd.cn/57709429/s/1681984
2025-04-08 10:12:20 +08:00
shaohuzhang1 2681d02728
fix: When the discriminator is true, there is no problem with the value (#2814)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-07 18:56:58 +08:00
shaohuzhang1 5e43bb9d2a
fix: jieba 分词使用全模式 (#2813) 2025-04-07 18:51:32 +08:00
wxg0103 ecd5fafbaa fix: The same observable object cannot appear twice in the same tree
--bug=1054451 --user=王孝刚 【应用嵌套】高级编排中应用关联设置用户输入参数的应用,会报该应用不可用 https://www.tapd.cn/57709429/s/1681701
2025-04-07 18:46:32 +08:00
wangdan-fit2cloud 23b47657c0
feat: The workflow condition node supports drag and drop sorting (#2648) 2025-04-07 18:19:38 +08:00
shaohuzhang1 76d050bea4
feat: New condition for determining whether the discriminator is true or not (#2809)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-07 14:07:30 +08:00
wxg0103 add9d1bab8 fix: In the permission settings, the knowledge base list does not distinguish between knowledge base types, and all are marked with the icon of the general knowledge base.
--bug=1054142 --user=王孝刚 【团队成员】权限设置中,知识库列表没有区分知识库类型,都用通用知识库的图标标识 https://www.tapd.cn/57709429/s/1681104
2025-04-07 13:53:39 +08:00
shaohuzhang1 9c36d8f30a
fix: Quick issue, API parameters cannot be carried (#2808) 2025-04-07 11:57:36 +08:00
wxg0103 189e2a6c63 fix: When adding a large language model in Application-Settings-Add AI node, the Base Model is not listed
--bug=1054358 --user=王孝刚 【应用】应用-设置-添加AI 节点时添加大语言模型,没有列出Base Model https://www.tapd.cn/57709429/s/1681102
2025-04-07 11:37:25 +08:00
shaohuzhang1 867c53984b
fix: The local rearrangement model cannot be loaded (#2805) 2025-04-07 11:22:01 +08:00
CaptainB 560890f717 fix: limit chapter title length to 256 characters in pdf_split_handle.py
--bug=1054363 --user=刘瑞斌 【知识库】导入PDF文档,分段标题长度超长时,没有自动截断 https://www.tapd.cn/57709429/s/1681044
2025-04-07 10:54:59 +08:00
CaptainB 675adeeb63 fix: exclude macOS specific files from zip processing
--bug=1054264 --user=刘瑞斌 【知识库】QA问答对模式,导入在mac上压缩的zip文件,会出现2个乱码文档 https://www.tapd.cn/57709429/s/1681034
2025-04-07 10:37:06 +08:00
shaohuzhang1 6bc00eb869
fix: During the debugging process, the browser voice will automatically play after returning to other pages (#2794)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-04-03 11:34:26 +08:00
shaohuzhang1 1eccb54199
fix: Knowledge base retrieval segmentation sorted by relevance (#2791) 2025-04-03 10:57:12 +08:00
wangdan-fit2cloud 86e11baeb2
perf: Optimization user input auto close when send message 2025-04-03 10:34:51 +08:00
shaohuzhang1 69ae1cafab
fix: After the page is redirected, the voice playback does not stop (#2789) 2025-04-03 09:53:54 +08:00
shaohuzhang1 9d6451b95b
fix: The interface for obtaining model metadata cannot access public models (#2787)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-02 20:10:20 +08:00
shaohuzhang1 add1cba8cb
feat: Swagger document response for adding OpenAI interface (#2786) 2025-04-02 19:59:57 +08:00
CaptainB 927f0c784a fix: remove multiple file upload option from file input in index.vue 2025-04-02 19:04:22 +08:00
shaohuzhang1 ac5a9d01a8
fix: When other users download the public model, they cannot obtain the download data (#2785) 2025-04-02 18:29:25 +08:00
CaptainB 7b213f547d fix: update default voice type and options in VolcanicEngineTTSModelGeneralParams 2025-04-02 18:25:06 +08:00
shaohuzhang1 7e4e2e98bb
fix: When answering a question - click to stop answering - wait until the question is approximately answered, then click to continue - it will remain in a stopped answering state (#2784) 2025-04-02 18:10:42 +08:00
wangdan-fit2cloud 21d2a44090
perf: Optimize application setting switch button control 2025-04-02 18:09:47 +08:00
shaohuzhang1 e364d6e373
fix: remove_form_rander (#2782) 2025-04-02 17:52:31 +08:00
shaohuzhang1 15feca802a
fix: OpenAI Vector Model Using Openai Supplier (#2781) 2025-04-02 17:45:18 +08:00
wxg0103 2686e76c8a fix: xinference rerank error
--bug=1054256 --user=王孝刚 【模型】添加硅基流动的重排序模型失败 https://www.tapd.cn/57709429/s/1679612
2025-04-02 16:54:58 +08:00
shaohuzhang1 6cf91098d6
fix: Dialogue embedding to modify name (#2780) 2025-04-02 16:49:52 +08:00
wangdan-fit2cloud 678a5ae4a5
fix: optimization user input 2025-04-02 16:42:58 +08:00
shaohuzhang1 a71c844ef4
fix: Copy and delete form information (#2778) 2025-04-02 16:36:14 +08:00
shaohuzhang1 74d10b61bc
fix: Unfiltered special characters in voice playback (#2777) 2025-04-02 16:12:17 +08:00
CaptainB 27bc01d442 fix: skip macOS specific metadata directories and files in zip parsing
--bug=1054264 --user=刘瑞斌 【知识库】QA问答对模式,导入在mac上压缩的zip文件,会出现2个乱码文档 https://www.tapd.cn/57709429/s/1679674
2025-04-02 16:06:36 +08:00
shaohuzhang1 bd900118f4
refactor: Dialogue logic optimization (#2776) 2025-04-02 15:51:15 +08:00
wxg0103 44c1d35b1f fix: siliconCloud rerank error
--bug=1054256 --user=王孝刚 【模型】添加硅基流动的重排序模型失败 https://www.tapd.cn/57709429/s/1679612
2025-04-02 15:17:39 +08:00
wangdan-fit2cloud f6994e16b9 fix: Fallback to fix issues 2025-04-02 14:56:30 +08:00
wxg0103 6d7b5eb219 fix: Increase log output 2025-04-02 14:17:48 +08:00
shaohuzhang1 27d4603b02
fix: Embedding dialogue asker parameter does not take effect (#2773)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-02 14:00:28 +08:00
shaohuzhang1 339e18d837
fix: After trying voice playback three times and sending an error message, an error message pops up (#2772) 2025-04-02 13:41:14 +08:00
CaptainB fb0fdb9c85 refactor: enhance MCP server configuration tooltips and validation messages
--bug=1054206 --user=刘瑞斌 【应用】MCP节点不配置Server config,不选择工具,可以发布应用,对话时报错 https://www.tapd.cn/57709429/s/1679457
2025-04-02 11:31:33 +08:00
wxg0103 5b2baaf04d fix: i18n error
--bug=1054232 --user=王孝刚 系统设置:操作菜单中的操作在中文模式下显示英文 https://www.tapd.cn/57709429/s/1679409
2025-04-02 10:46:39 +08:00
maninhill 739b977ce3
chore: Update README_CN.md (#2771) 2025-04-02 09:01:18 +08:00
maninhill 0315fe91df
chore: Update README.md (#2769) 2025-04-02 08:39:34 +08:00
wangdan-fit2cloud 02611588fc
fix: optimization chart operation style
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-01 19:41:01 +08:00
shaohuzhang1 2991f0b640
perf: Optimize word segmentation retrieval (#2767) 2025-04-01 19:11:16 +08:00
wxg0103 6fde8ec80f fix: openai need streaming 2025-04-01 16:58:08 +08:00
wangdan-fit2cloud 081fcab7eb
fix: Token conflict issue between two different applications embedded in the same system(#2753) 2025-04-01 16:19:13 +08:00
CaptainB 0e98c7783b refactor: add initialization parameters to function library update 2025-04-01 14:39:29 +08:00
wangdan-fit2cloud 812dc142c8
perf: Optimize some style issues 2025-04-01 14:21:44 +08:00
CaptainB 26946d0afb refactor: convert getList and getUserList functions to async for improved handling of asynchronous operations
--bug=1054152 --user=刘瑞斌 【函数库】筛选“我的”函数后刷新页面,还是显示的全部函数 https://www.tapd.cn/57709429/s/1678876
2025-04-01 13:03:22 +08:00
CaptainB ec6657177a refactor: conditionally render dropdown item based on init_field_list length
--bug=1054160 --user=刘瑞斌 【函数库】没有启用参数的函数,不展示启动参数按钮 https://www.tapd.cn/57709429/s/1678874
2025-04-01 13:03:22 +08:00
shaohuzhang1 4b9cecd4d1
feat: Knowledge base generation problem (#2760)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-04-01 12:46:30 +08:00
CaptainB 06867d33cb refactor: update MCP server config label for improved clarity 2025-04-01 10:55:48 +08:00
CaptainB b53a933327 refactor: update input source type from 'custom' to 'referencing'
--bug=1054147 --user=刘瑞斌 【MCP调用节点】工具参数默认方式是引用变量 https://www.tapd.cn/57709429/s/1678769
2025-04-01 10:33:02 +08:00
wangdan-fit2cloud 596b13711f
feat: function field form support sort
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-31 19:34:45 +08:00
shaohuzhang1 e7c3169898
feat: doc auth (#2752) 2025-03-31 19:02:56 +08:00
wxg0103 2612894557 refactor: oidc add state
--bug=1054135 --user=王孝刚 【认证】maxkb在调用okta的时候少个state参数,可以加上state参数 https://www.tapd.cn/57709429/s/1678654
2025-03-31 18:47:08 +08:00
CaptainB 7afc1da0af fix: handle non-string message types in function_lib_serializer 2025-03-31 18:29:01 +08:00
wxg0103 6aa0e9b5e4 refactor: i18n
--bug=1054121 --user=王孝刚 操作日志:修改应用的访问限制在操作日志中显示为修改应用的access token https://www.tapd.cn/57709429/s/1678535
2025-03-31 17:35:41 +08:00
CaptainB 24763427eb refactor: change mcp workflow node name 2025-03-31 17:29:00 +08:00
wangdan-fit2cloud da03442be2
fix: chat avatar display problem 2025-03-31 16:31:07 +08:00
shaohuzhang1 9750c6d605
fix: garbled zip import file names (#2747) 2025-03-31 16:22:39 +08:00
wxg0103 a2b6620b10 refactor: azure llm params 2025-03-31 15:31:52 +08:00
wangdan-fit2cloud b0a4e9e78f perf: Optimize copywriting 2025-03-31 15:23:02 +08:00
shaohuzhang1 36809b3314
fix: When uploading files during application dialogue, there are special whitespace characters and file name parsing errors #2738 (#2746) 2025-03-31 14:59:32 +08:00
wangdan-fit2cloud 165c27164e
fix: knowledge icon error 2025-03-31 14:40:52 +08:00
shaohuzhang1 1566ae7fbe
fix: Application deleted, workflow page error reported (#2743) 2025-03-31 14:31:53 +08:00
shaohuzhang1 b3feb243d3
fix: After setting the function library to private, the workflow used should report an error stating that the current function is not authorized to be used (#2742) 2025-03-31 13:58:38 +08:00
CaptainB cb104cc211 chore: fix typo
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-31 11:34:43 +08:00
shaohuzhang1 7a99c78840
fix: Processing for document export sheet with more than 32 characters (#2740) 2025-03-31 11:17:25 +08:00
shaohuzhang1 a07df46f9d
fix: Fix some knowledge base import errors (#2739) 2025-03-31 10:29:54 +08:00
CaptainB 4fa3fec103 ci: update some deps version
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-03-29 12:53:03 +08:00
CaptainB a2265cf357 chore: add tmp directory to .gitignore
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-28 18:45:35 +08:00
wxg0103 0b1990f8b3 refactor: lark 2025-03-28 18:25:36 +08:00
shaohuzhang1 bf91579b4e
fix: Fix some phones unable to play audio (#2728) 2025-03-28 18:24:00 +08:00
wxg0103 cc2789f37f fix: import add warning
--bug=1054090 --user=王孝刚 【知识库】飞书知识库-导入文档-未选择导入文件时,导入有提示信息 https://www.tapd.cn/57709429/s/1678039
2025-03-28 18:15:50 +08:00
wangdan-fit2cloud 3fe47e0fb3 perf: Optimize style 2025-03-28 18:09:19 +08:00
CaptainB 46e464b126 fix: conditionally render tooltip based on TTS availability in ChatOperationButton
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
--bug=1054047 --user=刘瑞斌 【应用】应用基本信息中未开启语音播放,演示应用时可以语音播放(播放有时候正确) https://www.tapd.cn/57709429/s/1678099
2025-03-28 17:22:00 +08:00
CaptainB 54c4293482 fix: ensure init_field_list defaults to an empty list in function_lib_serializer
--bug=1054092 --user=刘瑞斌 【函数库】导入历史版本的函数失败 https://www.tapd.cn/57709429/s/1678044
2025-03-28 15:53:06 +08:00
CaptainB a799026d52 fix: ensure init_field_list defaults to an empty list in function_lib_serializer
--bug=1054092 --user=刘瑞斌 【函数库】导入历史版本的函数失败 https://www.tapd.cn/57709429/s/1678044
2025-03-28 15:51:48 +08:00
wangdan-fit2cloud 5ceb8a7ff9
feat: Internal function can modify names 2025-03-28 15:33:44 +08:00
wxg0103 e7e4570aeb refactor: log add hover
--bug=1054088 --user=王孝刚 【操作日志】优化-操作详情建议一行展示,提供hover功能,hover后展示全部详情 https://www.tapd.cn/57709429/s/1678006
2025-03-28 15:01:27 +08:00
wxg0103 f69346f2bc refactor: i18n 2025-03-28 14:25:21 +08:00
shaohuzhang1 dcc80a4dca
fix: Every time a vectorized document is generated, the entire vectorized data of the document is deleted (#2721) 2025-03-28 14:15:48 +08:00
wxg0103 2fe4248785 refactor: qwq model 2025-03-28 14:05:33 +08:00
shaohuzhang1 bdaeb1bec4
perf: Optimization of recording function on mobile devices (#2719) 2025-03-28 13:52:54 +08:00
wangdan-fit2cloud 89be3e317d
fix: Optimize copywriting and style 2025-03-28 11:54:08 +08:00
CaptainB 2b079a4144 fix: update function names and descriptions for clarity in function_lib 2025-03-28 11:52:20 +08:00
wxg0103 4ffec85e9b refactor: Optimization of operation menu
--bug=1054069 --user=王孝刚 【操作日志】-操作菜单优化建议 https://www.tapd.cn/57709429/s/1677900
2025-03-28 11:45:51 +08:00
wangdan-fit2cloud bd098e68e5
fix: Fix markdown text 2025-03-28 10:28:51 +08:00
wxg0103 d282795644 refactor: i18n
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-28 09:31:48 +08:00
shaohuzhang1 5ba802482f
perf: Optimize voice recording (#2707)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-27 19:53:35 +08:00
wangdan-fit2cloud 378de21fa2
feat: User input optimization
* fix: Fix img 404

* feat: User input optimization
2025-03-27 19:42:01 +08:00
wxg0103 5eee6bfb6c fix: Modify download template prompt
--bug=1054044 --user=王孝刚 【知识库】上传文档-QA问答对-下载Excel/CSV模版-显示问题答案最长不超过4096个字符,实际上可以导入超过4096个字符的数据 https://www.tapd.cn/57709429/s/1677605
2025-03-27 18:50:12 +08:00
wxg0103 d47295aa36 fix: Defects that can be saved multiple times
--bug=1054039 --user=王孝刚 【飞书知识库】-导入文档时,快速点击开始导入按钮,会触发多次import请求,并且会跳离文档列表页 https://www.tapd.cn/57709429/s/1677588
2025-03-27 18:50:01 +08:00
CaptainB 0a3b9ee02b fix: refactor form item rendering for improved readability and structure
--bug=1054022 --user=刘瑞斌 【应用】MCP节点工具列表建议支持搜索 https://www.tapd.cn/57709429/s/1677517
--bug=1054029 --user=刘瑞斌 【应用】MCP工具参数必填时,参数置空可以发布应用 https://www.tapd.cn/57709429/s/1677571
2025-03-27 18:20:52 +08:00
CaptainB 97fb4a5cea fix: format MCP tool messages with detailed JSON representation
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-27 16:51:56 +08:00
wxg0103 80cd3ff7ec refactor: log menu 2025-03-27 16:24:45 +08:00
wxg0103 c66f79ad5a refactor: log menu 2025-03-27 16:14:24 +08:00
shaohuzhang1 3797613182
feat: Support converting text prompts (#2702) 2025-03-27 12:08:25 +08:00
CaptainB 7145f303da fix: update option list search logic to handle string and array form_data values
--bug=1053919 --user=刘瑞斌 【函数库】非必填启动参数开启启动参数后再次编辑多选置空 https://www.tapd.cn/57709429/s/1677136
2025-03-27 12:07:05 +08:00
wangdan-fit2cloud 754f7cb87c perf: dialog style 2025-03-27 11:42:59 +08:00
CaptainB 8a7e41be61 fix: enhance init_params check to ensure default values are present in init_field_list
--bug=1054001 --user=刘瑞斌 【函数库】创建时设置了默认启动参数,但依然弹出启动参数弹窗 https://www.tapd.cn/57709429/s/1677100
2025-03-27 11:12:21 +08:00
wangdan-fit2cloud 532ea4941a fix: radio style 2025-03-27 11:02:08 +08:00
CaptainB 286552d54b fix: update InitParamDrawer to handle is_active state in open method
--bug=1053998 --user=刘瑞斌 【函数库】首次开启函数并设置启动参数失败 https://www.tapd.cn/57709429/s/1677086
2025-03-27 11:00:23 +08:00
wxg0103 f82ba3c4b8 fix: qwq-plus only supported stream 2025-03-27 10:18:58 +08:00
CaptainB 0e29ce28cf chore: remove unused MCP response function from base_chat_node.py 2025-03-27 10:05:37 +08:00
wangdan-fit2cloud eed4d6857e
fix: Update lark icon and dialog style
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-26 20:46:14 +08:00
CaptainB 601b03d84e fix: ensure tool parameters are properly handled by reloading JSON
--bug=1053980 --user=刘瑞斌 【应用】MCP节点的工具参数引用参数时,对话过程中参数值发生变化,MCP节点没有获取最新的参数值 https://www.tapd.cn/57709429/s/1676830
2025-03-26 19:15:30 +08:00
wangdan-fit2cloud 8252febe22
fix: Fix style confusion 2025-03-26 18:56:11 +08:00
wangdan-fit2cloud 1a57e5897d
fix: Fix style confusion and update markdown file and icon
* fix: Fix style confusion

* fix: Update markdown file and icon
2025-03-26 18:55:39 +08:00
CaptainB 1e8e3a90aa fix: handle audio generation failure by raising an exception 2025-03-26 17:15:23 +08:00
CaptainB 2971406909 feat: add placeholder for MCP server configuration in form fields 2025-03-26 16:45:33 +08:00
shaohuzhang1 db772b1d1c
fix: Segmented filtering of paragraphs with empty parent title content (#2693)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-26 16:02:26 +08:00
wangdan-fit2cloud ca12d653a6
perf: Optimize pressing enter to line breaks on mobile 2025-03-26 16:00:19 +08:00
shaohuzhang1 a0ee5c9441
fix: Audit log login did not record user (#2691) 2025-03-26 15:41:11 +08:00
wxg0103 b1aa1f7a53 refactor: i18n 2025-03-26 15:35:03 +08:00
wangdan-fit2cloud 579604bd81
perf: Prompt word alignment issue(#2537) 2025-03-26 15:24:19 +08:00
wxg0103 a303f24974 refactor: i18n 2025-03-26 14:45:40 +08:00
wangdan-fit2cloud fadbd2fde0 fix: Update fx files 2025-03-26 14:25:34 +08:00
CaptainB bd7dbb13f3 fix: update icon paths in function library to use correct UI directory structure 2025-03-26 14:09:20 +08:00
shaohuzhang1 394d96980b
fix: Voice playback may cause playback issues (#2689) 2025-03-26 13:49:30 +08:00
CaptainB d27e5a4c01 chore: fix warning 2025-03-26 13:35:29 +08:00
CaptainB a6703c9889 fix: update icon paths in function library to use correct directory structure 2025-03-26 13:35:29 +08:00
wangdan-fit2cloud 545e80d655
fix: Static image loading path issue and Mobile setting information error 2025-03-26 12:55:59 +08:00
CaptainB 436e43dd04 feat: enhance form handling with dynamic input fields and reference content retrieval 2025-03-26 12:43:51 +08:00
CaptainB b8562ec736 fix: reset selectUserId on tab change in function library tabs
--bug=1053743 --user=刘瑞斌 【需求转缺陷】【内置函数】- 去掉按创建用户的搜索选项 https://www.tapd.cn/57709429/s/1676289
2025-03-26 11:00:30 +08:00
CaptainB 1b6b021226 fix: set is_active to False and update init_params handling in function_lib_serializer
--bug=1053923 --user=刘瑞斌 【函数库】创建函数后函数状态为启用,并非是禁用 https://www.tapd.cn/57709429/s/1676263

--bug=1053928 --user=刘瑞斌 【函数库】设置过启动参数的函数,删除启动参数,但实际依然有保留 https://www.tapd.cn/57709429/s/1676269
2025-03-26 10:55:35 +08:00
shaohuzhang1 55cdd0a708
fix: Zip with title cannot be parsed (#2683) 2025-03-26 10:31:31 +08:00
shaohuzhang1 f9d536f5a2
feat: Audit log add operation object (#2681)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-25 19:07:02 +08:00
wangdan-fit2cloud ba4e55d3e8
perf: refine user input style 2025-03-25 18:33:14 +08:00
CaptainB 3594fdadfa feat: add show_default_value attribute to input field in UserFieldFormDialog 2025-03-25 17:57:59 +08:00
CaptainB 83140b5f1d refactor: update label for API Key in input fields across function library
--bug=1053890 --user=刘瑞斌 【函数库】博查、Google、LangSearch查询函数的启动参数中,apikey建议改为 API Key https://www.tapd.cn/57709429/s/1675986
2025-03-25 17:34:15 +08:00
CaptainB 26cb55adfb fix: increase maximum length for input field in TextInputConstructor 2025-03-25 17:09:57 +08:00
wangdan-fit2cloud 440e2ba695
MCP node and internal function style
* feat: function

* feat: MCP node and internal  function style
2025-03-25 16:36:16 +08:00
CaptainB f19316639e chore: fix typo.
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-25 15:26:23 +08:00
CaptainB 564e781ba2 chore: fix typo. 2025-03-25 15:25:31 +08:00
wxg0103 9b46d29e73 refactor: lark document 2025-03-25 14:54:53 +08:00
CaptainB 6630589e8e fix: prevent default form submission in FunctionDebugDrawer
--bug=1053876 --user=刘瑞斌 【函数库】调试输入框回车后弹出新的浏览器tab页 https://www.tapd.cn/57709429/s/1675686
2025-03-25 13:23:02 +08:00
CaptainB 2f20868ca6 fix: handle empty message case in encryption function
--bug=1053881 --user=刘瑞斌 【函数库】-编辑函数,修改启动参数的必填选项后,保存函数报错 https://www.tapd.cn/57709429/s/1675682
2025-03-25 13:12:02 +08:00
CaptainB 64df9cf437 fix: improve required field validation for database connection parameters
--bug=1053849 --user=刘瑞斌 【函数库】- PgSQL 启动参数的先后顺序调整 https://www.tapd.cn/57709429/s/1675675
2025-03-25 12:53:46 +08:00
CaptainB 66b84a77b9 fix: conditionally display MCP Server Config field based on MCP enable status
--bug=1053873 --user=刘瑞斌 【AI对话节点】-工具设置中MCP关闭时,不显示server config配置 https://www.tapd.cn/57709429/s/1675665
2025-03-25 12:43:45 +08:00
wxg0103 d058d6d176 fix: function add required
--bug=1053859 --user=王孝刚 【函数库】函数内容给出必填提示 https://www.tapd.cn/57709429/s/1675529
2025-03-25 11:21:59 +08:00
wxg0103 5f289885f7 fix: lark document icon error
--bug=1053854 --user=王孝刚 【飞书】-创建完飞书知识库后,面板中飞书的icon未变 https://www.tapd.cn/57709429/s/1675539
2025-03-25 11:21:49 +08:00
wxg0103 82b06d130a fix: lark document i18n error
--bug=1053850 --user=王孝刚 [知识库]英文语言模式-添加飞书文档页面未国际化&提示的翻译错误 https://www.tapd.cn/57709429/s/1675482
2025-03-25 10:43:17 +08:00
CaptainB 0f0b6b976e feat: add MCP server config validation and user tips 2025-03-25 10:06:24 +08:00
ivy f681cb9b23 fix: Update characters 2025-03-24 20:53:46 +08:00
wangdan-fit2cloud ee5c8a455d feat: Setting avatar show or hide 2025-03-24 20:38:32 +08:00
wangdan-fit2cloud b01172b242 fix: lark document 2025-03-24 19:09:18 +08:00
shaohuzhang1 19b9e52a45
fix: Limit the number of retries for text to speech conversion (#2670) 2025-03-24 18:49:24 +08:00
CaptainB 563516f835 feat: AI chat node mcp server config 2025-03-24 18:38:54 +08:00
CaptainB 2d6ac806ff feat: add MCP server tools integration and UI components 2025-03-24 18:38:54 +08:00
CaptainB f9c4e96f97 fix: enhance validation for required form fields
--bug=1052705 --user=刘瑞斌 【工作流】添加”开关”按钮参数, 默认值为关闭,保存之后页面还会提示“请填写所填字段” https://www.tapd.cn/57709429/s/1675323
2025-03-24 18:10:42 +08:00
shaohuzhang1 66f674f651
fix: Voice playback is not played in order (#2668) 2025-03-24 17:54:45 +08:00
CaptainB 98ef6e5775 fix: improve error handling for MySQL connection
--bug=1053828 --user=刘瑞斌 【函数】内置的MySQL查询函数,在启动参数配错后,应用调用的返回结果为None,没有输出异常信息 https://www.tapd.cn/57709429/s/1675168
2025-03-24 17:50:02 +08:00
shaohuzhang1 ad452afa52
fix: When asker is empty, do not pass the current parameter (#2667) 2025-03-24 17:41:18 +08:00
CaptainB 0a148bff4b fix: always render subtitle for item username in template
--bug=1053754 --user=刘瑞斌 【内置函数】添加到函数库中后,未显示创建者 https://www.tapd.cn/57709429/s/1675159
2025-03-24 17:40:24 +08:00
CaptainB 9189a2ff2b fix: conditionally render user selection for public function type
--bug=1053743 --user=刘瑞斌 【需求转缺陷】【内置函数】- 去掉按创建用户的搜索选项 https://www.tapd.cn/57709429/s/1675143
2025-03-24 17:35:20 +08:00
liqiang-fit2cloud c0255beca2 build(deps): bump the pip group across 1 directory with 2 updates (#2654) 2025-03-24 16:50:23 +08:00
shaohuzhang1 c526a27796
build: build_error (#2662) 2025-03-24 16:02:43 +08:00
dependabot[bot] 13ce966caa
build(deps): bump the pip group across 1 directory with 2 updates (#2654)
Updates the requirements on [torch](https://github.com/pytorch/pytorch) and [gunicorn](https://github.com/benoitc/gunicorn) to permit the latest version.

Updates `torch` to 2.6.0
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.2.1...v2.6.0)

Updates `gunicorn` to 23.0.0
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/22.0.0...23.0.0)

---
updated-dependencies:
- dependency-name: torch
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: gunicorn
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 16:02:31 +08:00
shaohuzhang1 dcee1b6d55
feat: Text to speech support streaming playback (#2661) 2025-03-24 14:21:29 +08:00
wxg0103 0ce6dd0795 fix: Optimize the creation of models
--bug=1053742 --user=王孝刚 【模型管理】- 创建模型时,若在选择供应商列表选择了模型类型,进入供应商表单时,携带模型类型 https://www.tapd.cn/57709429/s/1674767
2025-03-24 11:50:38 +08:00
wxg0103 259d1c872b fix: type check error 2025-03-24 10:26:33 +08:00
ivy e7c2c9710a perf: Delete unnecessary files 2025-03-21 22:46:54 +08:00
wangdan-fit2cloud 0eebbb094c
feat: Optimize the mobile voice interaction experience
* fix: Optimize small screen dialogue style

* feat: Mobile voice conversation new UI

* feat: Optimize the mobile voice interaction experience

* feat: Optimize the mobile voice interaction experience
2025-03-21 16:57:04 +08:00
wxg0103 2faabbe392 refactor: bailian 2025-03-21 11:02:43 +08:00
wxg0103 e22fc95ee9 refactor: Optimization of Q&A page
--story=1018176 --user=王孝刚 【问答页面】优化对话页面显示 https://www.tapd.cn/57709429/s/1674146
2025-03-21 10:07:07 +08:00
CaptainB 72398408c5 ci: update langchain version 2025-03-20 18:55:43 +08:00
junjun 18e4647211 feat: add operate log ui 2025-03-20 16:53:03 +08:00
junjun 642a284b33 feat: export operate log 2025-03-20 16:41:59 +08:00
wxg0103 3951a15e9d refactor: i18n
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-03-20 14:49:23 +08:00
CaptainB 27561410e5 fix: handle param value is blank 2025-03-20 14:21:31 +08:00
CaptainB 47849fc1a5 fix: Add FlibInstance to allowed classes and use restricted_loads for deserialization 2025-03-20 13:53:37 +08:00
shaohuzhang1 f19ad24907
fix: Application import deserialization vulnerability #2604 (#2633) 2025-03-20 12:10:06 +08:00
shaohuzhang1 5baa141b89
fix: type-check (#2629) 2025-03-20 10:12:15 +08:00
wxg0103 ea0ab5e3d2 feat: support lark document
--story=1017908 --user=王孝刚 【知识库】 - X-Pack支持对接飞书文档 https://www.tapd.cn/57709429/s/1673069
2025-03-20 10:05:32 +08:00
junjun e995a663c8 feat: add operate log 2025-03-19 19:12:17 +08:00
shaohuzhang1 4a681297e0
fix: Simple application user did not successfully set up (#2625) 2025-03-19 18:23:05 +08:00
CaptainB a65fc39ae4 chore: mysql icon 2025-03-19 17:07:23 +08:00
junjun bcd41d0c19 feat: add operate log 2025-03-19 17:03:08 +08:00
wangdan-fit2cloud 96562b9f16
fix: Optimize small screen dialogue style 2025-03-19 16:07:02 +08:00
shaohuzhang1 470105f895
feat: Support application、setting、user log recorder (#2617) 2025-03-19 15:45:10 +08:00
wangdan-fit2cloud 7b51d08ea5
feat: Add mobile mode to the conversation, support ctrl/shift/cmd/opt+enter for new line (#2615) 2025-03-19 15:31:27 +08:00
CaptainB 9e62e81158 feat: Add functionality to create and manage internal functions with MySQL and PostgreSQL queries 2025-03-19 14:34:54 +08:00
shaohuzhang1 b7accc54e7
fix: Logging error for logged in user (#2613)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-19 12:07:46 +08:00
CaptainB 4b4b84a220 feat: Add functionality to create and manage internal functions with MySQL and PostgreSQL queries 2025-03-19 12:06:33 +08:00
shaohuzhang1 5ec94860b2
perf: Enhance Word parsing (#2612) 2025-03-19 12:04:43 +08:00
junjun 263c18ebca feat: Add Operate Log 2025-03-18 20:19:26 +08:00
junjun 4f06cfe1ab feat: Add Operate Log 2025-03-18 20:19:26 +08:00
shaohuzhang1 8b52927b4f
fix: The float values collected by the form will be treated as ints when the decimal part is 0, resulting in a type error (#2601)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-18 16:40:15 +08:00
shaohuzhang1 1a7f484a62
feat: User information in the application conversation log. (#2599) 2025-03-18 15:50:30 +08:00
CaptainB aab6a2cdf3 refactor: Improve parameter handling in function execution 2025-03-18 14:03:48 +08:00
wangdan-fit2cloud 3709d34343 fix: Fix chat question 2025-03-18 13:51:10 +08:00
CaptainB bf6cfface6 chore: add some encoder type 2025-03-18 13:16:28 +08:00
CaptainB 075646481d chore: fix some log recorder 2025-03-17 19:13:16 +08:00
CaptainB f6586a481b refactor: Add mysql function log recorder 2025-03-17 17:53:44 +08:00
CaptainB a3cd92c503 refactor: Add mysql function lib 2025-03-17 17:11:04 +08:00
CaptainB 11f63b4556 refactor: Add postgresql function lib 2025-03-17 17:11:04 +08:00
shaohuzhang1 000a3970e0
fix: Multi process startup document modification executed multiple times (#2581) 2025-03-17 17:00:51 +08:00
shaohuzhang1 be31989ab9
fix: Log annotation acquisition details file type data cannot be serialized (#2579) 2025-03-17 16:45:06 +08:00
wangdan-fit2cloud 460de60019
feat: Add import document view 2025-03-17 16:33:52 +08:00
dependabot[bot] 3a9780dc4f
build(deps): bump axios (#2553)
Bumps the npm_and_yarn group with 1 update in the /ui directory: [axios](https://github.com/axios/axios).


Updates `axios` from 0.28.1 to 1.8.3
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.28.1...v1.8.3)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-17 16:03:47 +08:00
CaptainB 9620817a8f feat: Support dataset log recorder 2025-03-17 15:34:41 +08:00
CaptainB 03a705fc93 feat: Support function lib log recorder 2025-03-17 15:34:41 +08:00
shaohuzhang1 d7ef3ba67b feat: Audit Log 2025-03-17 15:34:41 +08:00
wangdan-fit2cloud 80e7dac0c8
fix: Adjustment of workflow node name auto increment rule(#2562) 2025-03-17 14:41:18 +08:00
wangdan-fit2cloud 03274d9ee5
feat: Update function setting style 2025-03-17 14:04:25 +08:00
wxg0103 6a8d2c1f9c refactor: add enum 2025-03-17 10:33:51 +08:00
CaptainB 510fae6bf1 feat: Support internal functionlib
--story=1017939 --user=刘瑞斌 【函数库】- 支持 系统内置函数 https://www.tapd.cn/57709429/s/1665943
2025-03-17 10:30:25 +08:00
CaptainB ad0b032384 feat: Support functionlib icon and init_fields
--story=1017947 --user=刘瑞斌 【函数库】- 函数支持配置参数及操作优化 https://www.tapd.cn/57709429/s/1664936
2025-03-17 10:30:25 +08:00
wangdan-fit2cloud a09f5c0577
perf: User input interaction style optimization 2025-03-14 19:06:03 +08:00
wxg0103 b8960d57c8 fix: schema access failure to https 2025-03-14 16:26:15 +08:00
wxg0103 f6e089daee feat: support three-party password-free login
--story=1018017 --user=王孝刚 【登录认证】-X-Pack 支持三方应用(企业微信、钉钉、飞书)免密登录 https://www.tapd.cn/57709429/s/1669142
2025-03-13 11:49:05 +08:00
wangdan-fit2cloud 7bd1dfbdaa
fix: Optimization style and fit setting avatar save prompt error(#2523)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
* fix: Setting avatar save prompt error(#2523)

* perf: optimization style
2025-03-12 11:22:52 +08:00
wangdan-fit2cloud 175a80191e
fix: Setting avatar save prompt error(#2523) (#2535) 2025-03-11 15:55:14 +08:00
wangdan-fit2cloud 518202ae0e
perf: Form node support adjusting the order (#2533) 2025-03-11 14:51:57 +08:00
maninhill f1a1c40724
chore: Update USE-CASES.md (#2517)
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled
2025-03-07 22:02:41 +08:00
shaohuzhang1 e11c550fc2
fix: Simple application model dialogue model error not returned (#2514)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run
Typos Check / Spell Check with Typos (push) Waiting to run
2025-03-07 14:15:26 +08:00
shaohuzhang1 82b566d580
fix: Having the same suffix for function node names can result in incorrect value retrieval (#2513) 2025-03-07 13:48:52 +08:00
shaohuzhang1 8b9998a53d
fix: When importing documents from a web knowledge base, if a failed document is encountered, the import will be interrupted (#2511) 2025-03-07 10:19:57 +08:00
wxg0103 96ab89adf3 fix: ui logo error 2025-03-06 18:35:37 +08:00
liqiang-fit2cloud d99c9ad1ee build: update build-and-push.yml. 2025-03-06 17:32:49 +08:00
liqiang-fit2cloud d918f96c66 build: update build-and-push.yml. 2025-03-06 17:31:15 +08:00
liqiang-fit2cloud 8b17afc6b3 build: update build-and-push.yml. 2025-03-06 16:36:51 +08:00
liqiang-fit2cloud c5e4bbf2ce build: update build-and-push.yml. 2025-03-06 16:15:43 +08:00
402 changed files with 23871 additions and 13202 deletions

17
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
timezone: "Asia/Shanghai"
day: "friday"
target-branch: "v2"
groups:
python-dependencies:
patterns:
- "*"
# ignore:
# - dependency-name: "pymupdf"
# versions: ["*"]

View File

@ -7,7 +7,7 @@ on:
inputs:
dockerImageTag:
description: 'Image Tag'
default: 'v1.10.0-dev'
default: 'v1.10.7-dev'
required: true
dockerImageTagWithLatest:
description: '是否发布latest tag正式发版时选择测试版本切勿选择'
@ -36,7 +36,7 @@ on:
jobs:
build-and-push-to-fit2cloud-registry:
if: ${{ contains(github.event.inputs.registry, 'fit2cloud') }}
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Check Disk Space
run: df -h
@ -64,18 +64,15 @@ jobs:
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
else
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
fi
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
--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=${GITHUB_SHA::8} --no-cache \
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 \
${DOCKER_IMAGE_TAGS} .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
# Until https://github.com/tonistiigi/binfmt/issues/215
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
@ -92,11 +89,12 @@ jobs:
password: ${{ secrets.FIT2CLOUD_REGISTRY_PASSWORD }}
- name: Docker Buildx (build-and-push)
run: |
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile
build-and-push-to-dockerhub:
if: ${{ contains(github.event.inputs.registry, 'dockerhub') }}
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Check Disk Space
run: df -h
@ -124,18 +122,15 @@ jobs:
TAG_NAME=${{ github.event.inputs.dockerImageTag }}
TAG_NAME_WITH_LATEST=${{ github.event.inputs.dockerImageTagWithLatest }}
if [[ ${TAG_NAME_WITH_LATEST} == 'true' ]]; then
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:latest"
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME} --tag ${DOCKER_IMAGE}:${TAG_NAME%%.*}"
else
DOCKER_IMAGE_TAGS="--tag ${DOCKER_IMAGE}:${TAG_NAME}"
fi
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
--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=${GITHUB_SHA::8} --no-cache \
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 \
${DOCKER_IMAGE_TAGS} .
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
# Until https://github.com/tonistiigi/binfmt/issues/215
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
@ -151,4 +146,5 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker Buildx (build-and-push)
run: |
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches && free -m
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} -f installer/Dockerfile

3
.gitignore vendored
View File

@ -183,4 +183,5 @@ apps/xpack
data
.dev
poetry.lock
apps/setting/models_provider/impl/*/icon/
apps/setting/models_provider/impl/*/icon/
tmp/

View File

@ -1,6 +1,6 @@
<p align="center"><img src= "https://github.com/1Panel-dev/maxkb/assets/52996290/c0694996-0eed-40d8-b369-322bf2a380bf" alt="MaxKB" width="300" /></p>
<h3 align="center">Ready-to-use, flexible RAG Chatbot</h3>
<h3 align="center">基于大模型和 RAG 的开源知识库问答系统</h3>
<h3 align="center">Open-source platform for building enterprise-grade agents</h3>
<h3 align="center">强大易用的企业级智能体平台</h3>
<p align="center"><a href="https://trendshift.io/repositories/9113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9113" alt="1Panel-dev%2FMaxKB | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
<p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb?color=%231890FF" alt="License: GPL v3"></a>
@ -11,10 +11,10 @@
</p>
<hr/>
MaxKB = Max Knowledge Base, it is a chatbot based on Large Language Models (LLM) and Retrieval-Augmented Generation (RAG). 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.
- **Ready-to-Use**: Supports direct uploading of documents / automatic crawling of online documents, with features for automatic text splitting, vectorization, and RAG (Retrieval-Augmented Generation). This effectively reduces hallucinations in large models, providing a superior smart Q&A interaction experience.
- **Flexible Orchestration**: Equipped with a powerful workflow engine and function library, enabling the orchestration of AI processes to meet the needs of complex business scenarios.
- **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.
- **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.).
- **Multi Modal**: Native support for input and output text, image, audio and video.
@ -56,8 +56,6 @@ Access MaxKB web interface at `http://your_server_ip:8080` with default admin cr
## Feature Comparison
MaxKB is positioned as an Ready-to-use RAG (Retrieval-Augmented Generation) intelligent Q&A application, rather than a middleware platform for building large model applications. The following table is merely a comparison from a functional perspective.
<table style="width: 100%;">
<tr>
<th align="center">Feature</th>

View File

@ -1,25 +1,25 @@
<p align="center"><img src= "https://github.com/1Panel-dev/maxkb/assets/52996290/c0694996-0eed-40d8-b369-322bf2a380bf" alt="MaxKB" width="300" /></p>
<h3 align="center">基于大模型和 RAG 的知识库问答系统</h3>
<h4 align="center">Ready-to-use, flexible RAG Chatbot</h4>
<h3 align="center">强大易用的企业级智能体平台</h3>
<p align="center">
<a href="https://trendshift.io/repositories/9113" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9113" alt="1Panel-dev%2FMaxKB | Trendshift" style="width: 250px; height: auto;" /></a>
<a href="https://market.aliyun.com/products/53690006/cmjj00067609.html?userCode=kmemb8jp" target="_blank"><img src="https://img.alicdn.com/imgextra/i2/O1CN01H5JIwY1rZ0OobDjnJ_!!6000000005644-2-tps-1000-216.png" alt="1Panel-dev%2FMaxKB | Aliyun" style="width: 250px; height: auto;" /></a>
</p>
<p align="center">
<a href="README_EN.md"><img src="https://img.shields.io/badge/English_README-blue" alt="English README"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb" alt="License: GPL v3"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0.html#license-text"><img src="https://img.shields.io/github/license/1Panel-dev/maxkb?color=%231890FF" alt="License: GPL v3"></a>
<a href="https://github.com/1Panel-dev/maxkb/releases/latest"><img src="https://img.shields.io/github/v/release/1Panel-dev/maxkb" alt="Latest release"></a>
<a href="https://github.com/1Panel-dev/maxkb"><img src="https://img.shields.io/github/stars/1Panel-dev/maxkb?style=flat-square" alt="Stars"></a>
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a>
<a href="https://github.com/1Panel-dev/maxkb"><img src="https://img.shields.io/github/stars/1Panel-dev/maxkb?style=flat-square" alt="Stars"></a>
<a href="https://hub.docker.com/r/1panel/maxkb"><img src="https://img.shields.io/docker/pulls/1panel/maxkb?label=downloads" alt="Download"></a>
<a href="https://gitee.com/fit2cloud-feizhiyun/MaxKB"><img src="https://gitee.com/fit2cloud-feizhiyun/MaxKB/badge/star.svg?theme=gvp" alt="Gitee Stars"></a>
<a href="https://gitcode.com/feizhiyun/MaxKB"><img src="https://gitcode.com/feizhiyun/MaxKB/star/badge.svg" alt="GitCode Stars"></a>
</p>
<hr/>
MaxKB = Max Knowledge Base是一款基于大语言模型和 RAG 的开源知识库问答系统,广泛应用于智能客服、企业内部知识库、学术研究与教育等场景。
MaxKB = Max Knowledge Brain是一款强大易用的企业级智能体平台支持 RAG 检索增强生成、工作流编排、MCP 工具调用能力。MaxKB 支持对接各种主流大语言模型,广泛应用于智能客服、企业内部知识库问答、员工助手、学术研究与教育等场景。
- **开箱即用**:支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化和 RAG检索增强生成有效减少大模型幻觉智能问答交互体验好
- **模型中立**支持对接各种大模型包括本地私有大模型DeepSeek R1 / Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等和国外公共大模型OpenAI / Claude / Gemini 等)
- **灵活编排**:内置强大的工作流引擎和函数库,支持编排 AI 工作过程,满足复杂业务场景下的需求
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度
- **RAG 检索增强生成**:高效搭建本地 AI 知识库,支持直接上传文档 / 自动爬取在线文档,支持文本自动拆分、向量化,有效减少大模型幻觉,提升问答效果
- **灵活编排**:内置强大的工作流引擎、函数库和 MCP 工具调用能力,支持编排 AI 工作过程,满足复杂业务场景下的需求
- **无缝嵌入**:支持零编码快速嵌入到第三方业务系统,让已有系统快速拥有智能问答能力,提高用户满意度
- **模型中立**支持对接各种大模型包括本地私有大模型DeepSeek R1 / Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等和国外公共大模型OpenAI / Claude / Gemini 等)
MaxKB 三分钟视频介绍https://www.bilibili.com/video/BV18JypYeEkj/
@ -39,7 +39,7 @@ docker run -d --name=maxkb --restart=always -p 8080:8080 -v C:/maxkb:/var/lib/po
- 你也可以通过 [1Panel 应用商店](https://apps.fit2cloud.com/1panel) 快速部署 MaxKB
- 如果是内网环境,推荐使用 [离线安装包](https://community.fit2cloud.com/#/products/maxkb/downloads) 进行安装部署;
- MaxKB 产品版本分为社区版和专业版,详情请参见:[MaxKB 产品版本对比](https://maxkb.cn/pricing.html)
- 如果您需要向团队介绍 MaxKB可以使用这个 [官方 PPT 材料](https://maxkb.cn/download/introduce-maxkb_202501.pdf)。
- 如果您需要向团队介绍 MaxKB可以使用这个 [官方 PPT 材料](https://maxkb.cn/download/introduce-maxkb_202503.pdf)。
如你有更多问题,可以查看使用手册,或者通过论坛与我们交流。

View File

@ -18,3 +18,22 @@
- [MaxKB 应用案例产品使用指南AI助手新手小白也能轻松搞定](https://www.bilibili.com/video/BV1Yz6gYtEqX)
- [MaxKB 应用案例生物医药AI客服智能体验!](https://www.bilibili.com/video/BV13JzvYsE3e)
- [MaxKB 应用案例高校行政管理AI小助手](https://www.bilibili.com/video/BV1yvBMYvEdy)
- [MaxKB 应用案例:岳阳市人民医院-OA小助手](https://mp.weixin.qq.com/s/O94Qo3UH-MiUtDdWCVg8sQ)
- [MaxKB 应用案例:常熟市第一人民医院](https://mp.weixin.qq.com/s/s5XXGTR3_MUo41NbJ8WzZQ)
- [MaxKB 应用案例:华北水利水电大学](https://mp.weixin.qq.com/s/PoOFAcMCr9qJdvSj8c08qg)
- [MaxKB 应用案例:唐山海事局-“小海”AI语音助手](https://news.qq.com/rain/a/20250223A030BE00)
- [MaxKB 应用案例:湖南汉寿政务](http://hsds.hsdj.gov.cn:19999/ui/chat/a2c976736739aadc)
- [MaxKB 应用案例:广州市妇女儿童医疗中心-AI医疗数据分类分级小助手](https://mp.weixin.qq.com/s/YHUMkUOAaUomBV8bswpK3g)
- [MaxKB 应用案例:苏州热工研究院有限公司-维修大纲评估质量自查AI小助手](https://mp.weixin.qq.com/s/Ts5FQdnv7Tu9Jp7bvofCVA)
- [MaxKB 应用案例:国核自仪系统工程有限公司-NuCON AI帮](https://mp.weixin.qq.com/s/HNPc7u5xVfGLJr8IQz3vjQ)
- [MaxKB 应用案例深圳通开启Deep Seek智能应用新篇章](https://mp.weixin.qq.com/s/SILN0GSescH9LyeQqYP0VQ)
- [MaxKB 应用案例南通智慧出行领跑长三角首款接入DeepSeek的"畅行南通"APP上线AI新场景](https://mp.weixin.qq.com/s/WEC9UQ6msY0VS8LhTZh-Ew)
- [MaxKB 应用案例:中船动力人工智能"智慧动力云助手"及首批数字员工正式上线](https://mp.weixin.qq.com/s/OGcEkjh9DzGO1Tkc9nr7qg)
- [MaxKB 应用案例AI+矿山DeepSeek助力绿色智慧矿山智慧“升级”](https://mp.weixin.qq.com/s/SZstxTvVoLZg0ECbZbfpIA)
- [MaxKB 应用案例DeepSeek落地弘盛铜业国产大模型点亮"黑灯工厂"新引擎](https://mp.weixin.qq.com/s/Eczdx574MS5RMF7WfHN7_A)
- [MaxKB 应用案例:拥抱智能时代!中国五矿以 “AI+”赋能企业发展](https://mp.weixin.qq.com/s/D5vBtlX2E81pWE3_2OgWSw)
- [MaxKB 应用案例DeepSeek赋能中冶武勘AI智能体](https://mp.weixin.qq.com/s/8m0vxGcWXNdZazziQrLyxg)
- [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/HThGSnND3qDF8ySEqiM4jw)
- [MaxKB 应用案例一起DeepSeek福建设计以AI大模型开启新篇章](https://mp.weixin.qq.com/s/m67e-H7iQBg3d24NM82UjA)

View File

@ -124,9 +124,11 @@ def event_content(response,
request_token = 0
response_token = 0
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,
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',
[], '', True,
request_token, response_token,
@ -135,16 +137,21 @@ def event_content(response,
add_access_num(client_id, client_type, manage.context.get('application_id'))
except Exception as e:
logging.getLogger("max_kb_error").error(f'{str(e)}:{traceback.format_exc()}')
all_text = '异常' + str(e)
all_text = 'Exception:' + str(e)
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,
all_text, manage, step, padding_problem_text, client_id)
all_text, manage, step, padding_problem_text, client_id, reasoning_content='',
asker=asker)
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), all_text,
'ai-chat-node',
[], True, 0, 0,
{'node_is_end': True, 'view_type': 'many_view',
'node_type': 'ai-chat-node'})
yield manage.get_base_to_response().to_stream_chunk_response(chat_id, str(chat_record_id), 'ai-chat-node',
[], all_text,
False,
0, 0, {'node_is_end': False,
'view_type': 'many_view',
'node_type': 'ai-chat-node',
'real_node_id': 'ai-chat-node',
'reasoning_content': ''})
class BaseChatStep(IChatStep):
@ -300,9 +307,11 @@ class BaseChatStep(IChatStep):
else:
reasoning_content = reasoning_result.get('reasoning_content') + reasoning_result_end.get(
'reasoning_content')
asker = manage.context.get('form_data', {}).get('asker', None)
post_response_handler.handler(chat_id, chat_record_id, paragraph_list, problem_text,
content, manage, self, 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)
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),
content, True,
@ -316,8 +325,10 @@ class BaseChatStep(IChatStep):
except Exception as e:
all_text = 'Exception:' + str(e)
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,
all_text, manage, self, padding_problem_text, client_id)
all_text, manage, self, padding_problem_text, client_id, reasoning_content='',
asker=asker)
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,
0, _status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@ -84,7 +84,8 @@ class WorkFlowPostHandler:
answer_text_list=answer_text_list,
run_time=time.time() - workflow.context['start_time'],
index=0)
self.chat_info.append_chat_record(chat_record, self.client_id)
asker = workflow.context.get('asker', None)
self.chat_info.append_chat_record(chat_record, self.client_id, asker)
# 重新设置缓存
chat_cache.set(chat_id,
self.chat_info, timeout=60 * 30)

View File

@ -25,13 +25,14 @@ from .speech_to_text_step_node import BaseSpeechToTextNode
from .start_node import *
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
from .variable_assign_node import BaseVariableAssignNode
from .mcp_node import BaseMcpNode
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode,
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
BaseDocumentExtractNode,
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
BaseImageGenerateNode, BaseVariableAssignNode]
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
def get_node(node_type):

View File

@ -33,6 +33,9 @@ class ChatNodeSerializer(serializers.Serializer):
error_messages=ErrMessage.dict('Model settings'))
dialogue_type = serializers.CharField(required=False, allow_blank=True, allow_null=True,
error_messages=ErrMessage.char(_("Context Type")))
mcp_enable = serializers.BooleanField(required=False,
error_messages=ErrMessage.boolean(_("Whether to enable MCP")))
mcp_servers = serializers.JSONField(required=False, error_messages=ErrMessage.list(_("MCP Server")))
class IChatNode(INode):
@ -49,5 +52,7 @@ class IChatNode(INode):
model_params_setting=None,
dialogue_type=None,
model_setting=None,
mcp_enable=False,
mcp_servers=None,
**kwargs) -> NodeResult:
pass

View File

@ -6,14 +6,19 @@
@date2024/6/4 14:30
@desc:
"""
import asyncio
import json
import re
import time
from functools import reduce
from types import AsyncGeneratorType
from typing import List, Dict
from django.db.models import QuerySet
from langchain.schema import HumanMessage, SystemMessage
from langchain_core.messages import BaseMessage, AIMessage
from langchain_core.messages import BaseMessage, AIMessage, AIMessageChunk, ToolMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from application.flow.i_step_node import NodeResult, INode
from application.flow.step_node.ai_chat_step_node.i_chat_node import IChatNode
@ -22,6 +27,19 @@ from setting.models import Model
from setting.models_provider import get_model_credential
from setting.models_provider.tools import get_model_instance_by_model_user_id
tool_message_template = """
<details>
<summary>
<strong>Called MCP Tool: <em>%s</em></strong>
</summary>
```json
%s
```
</details>
"""
def _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str,
reasoning_content: str):
@ -56,6 +74,7 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
reasoning = Reasoning(model_setting.get('reasoning_content_start', '<think>'),
model_setting.get('reasoning_content_end', '</think>'))
response_reasoning_content = False
for chunk in response:
reasoning_chunk = reasoning.get_reasoning_content(chunk)
content_chunk = reasoning_chunk.get('content')
@ -84,6 +103,39 @@ def write_context_stream(node_variable: Dict, workflow_variable: Dict, node: INo
_write_context(node_variable, workflow_variable, node, workflow, answer, reasoning_content)
async def _yield_mcp_response(chat_model, message_list, mcp_servers):
async with MultiServerMCPClient(json.loads(mcp_servers)) as client:
agent = create_react_agent(chat_model, client.get_tools())
response = agent.astream({"messages": message_list}, stream_mode='messages')
async for chunk in response:
if isinstance(chunk[0], ToolMessage):
content = tool_message_template % (chunk[0].name, chunk[0].content)
chunk[0].content = content
yield chunk[0]
if isinstance(chunk[0], AIMessageChunk):
yield chunk[0]
def mcp_response_generator(chat_model, message_list, mcp_servers):
loop = asyncio.new_event_loop()
try:
async_gen = _yield_mcp_response(chat_model, message_list, mcp_servers)
while True:
try:
chunk = loop.run_until_complete(anext_async(async_gen))
yield chunk
except StopAsyncIteration:
break
except Exception as e:
print(f'exception: {e}')
finally:
loop.close()
async def anext_async(agen):
return await agen.__anext__()
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
"""
写入上下文数据
@ -136,12 +188,15 @@ class BaseChatNode(IChatNode):
self.context['answer'] = details.get('answer')
self.context['question'] = details.get('question')
self.context['reasoning_content'] = details.get('reasoning_content')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
model_params_setting=None,
dialogue_type=None,
model_setting=None,
mcp_enable=False,
mcp_servers=None,
**kwargs) -> NodeResult:
if dialogue_type is None:
dialogue_type = 'WORKFLOW'
@ -163,6 +218,14 @@ class BaseChatNode(IChatNode):
self.context['system'] = system
message_list = self.generate_message_list(system, prompt, history_message)
self.context['message_list'] = message_list
if mcp_enable and mcp_servers is not None and '"stdio"' not in mcp_servers:
r = mcp_response_generator(chat_model, message_list, mcp_servers)
return NodeResult(
{'result': r, 'chat_model': chat_model, 'message_list': message_list,
'history_message': history_message, 'question': question.content}, {},
_write_context=write_context_stream)
if stream:
r = chat_model.stream(message_list)
return NodeResult({'result': r, 'chat_model': chat_model, 'message_list': message_list,

View File

@ -168,7 +168,8 @@ class BaseApplicationNode(IApplicationNode):
self.context['question'] = details.get('question')
self.context['type'] = details.get('type')
self.context['reasoning_content'] = details.get('reasoning_content')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, application_id, message, chat_id, chat_record_id, stream, re_chat, client_id, client_type,
app_document_list=None, app_image_list=None, app_audio_list=None, child_node=None, node_data=None,
@ -178,7 +179,8 @@ class BaseApplicationNode(IApplicationNode):
current_chat_id = string_to_uuid(chat_id + application_id)
Chat.objects.get_or_create(id=current_chat_id, defaults={
'application_id': application_id,
'abstract': message[0:1024]
'abstract': message[0:1024],
'client_id': client_id,
})
if app_document_list is None:
app_document_list = []

View File

@ -9,20 +9,22 @@
from .contain_compare import *
from .equal_compare import *
from .gt_compare import *
from .ge_compare import *
from .gt_compare import *
from .is_not_null_compare import *
from .is_not_true import IsNotTrueCompare
from .is_null_compare import *
from .is_true import IsTrueCompare
from .le_compare import *
from .lt_compare import *
from .len_equal_compare import *
from .len_ge_compare import *
from .len_gt_compare import *
from .len_le_compare import *
from .len_lt_compare import *
from .len_equal_compare import *
from .is_not_null_compare import *
from .is_null_compare import *
from .lt_compare import *
from .not_contain_compare import *
compare_handle_list = [GECompare(), GTCompare(), ContainCompare(), EqualCompare(), LTCompare(), LECompare(),
LenLECompare(), LenGECompare(), LenEqualCompare(), LenGTCompare(), LenLTCompare(),
IsNullCompare(),
IsNotNullCompare(), NotContainCompare()]
IsNotNullCompare(), NotContainCompare(), IsTrueCompare(), IsNotTrueCompare()]

View File

@ -0,0 +1,24 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file is_not_true.py
@date2025/4/7 13:44
@desc:
"""
from typing import List
from application.flow.step_node.condition_node.compare import Compare
class IsNotTrueCompare(Compare):
def support(self, node_id, fields: List[str], source_value, compare, target_value):
if compare == 'is_not_true':
return True
def compare(self, source_value, compare, target_value):
try:
return source_value is False
except Exception as e:
return False

View File

@ -0,0 +1,24 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file IsTrue.py
@date2025/4/7 13:38
@desc:
"""
from typing import List
from application.flow.step_node.condition_node.compare import Compare
class IsTrueCompare(Compare):
def support(self, node_id, fields: List[str], source_value, compare, target_value):
if compare == 'is_true':
return True
def compare(self, source_value, compare, target_value):
try:
return source_value is True
except Exception as e:
return False

View File

@ -40,7 +40,11 @@ class BaseConditionNode(IConditionNode):
value = self.workflow_manage.generate_prompt(value)
except Exception as e:
pass
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
field_value = None
try:
field_value = self.workflow_manage.get_reference_field(field_list[0], field_list[1:])
except Exception as e:
pass
for compare_handler in compare_handle_list:
if compare_handler.support(field_list[0], field_list[1:], field_value, compare, value):
return compare_handler.compare(field_value, compare, value)

View File

@ -15,7 +15,9 @@ from application.flow.step_node.direct_reply_node.i_reply_node import IReplyNode
class BaseReplyNode(IReplyNode):
def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, reply_type, stream, fields=None, content=None, **kwargs) -> NodeResult:
if reply_type == 'referencing':
result = self.get_reference_content(fields)

View File

@ -38,7 +38,8 @@ class BaseFormNode(IFormNode):
self.context['start_time'] = details.get('start_time')
self.context['form_data'] = form_data
self.context['is_submit'] = details.get('is_submit')
self.answer_text = details.get('result')
if self.node_params.get('is_result', False):
self.answer_text = details.get('result')
if form_data is not None:
for key in form_data:
self.context[key] = form_data[key]
@ -70,7 +71,7 @@ class BaseFormNode(IFormNode):
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
'form_data': self.context.get('form_data', {}),
"is_submit": self.context.get("is_submit", False)}
form = f'<form_rander>{json.dumps(form_setting,ensure_ascii=False)}</form_rander>'
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
context = self.workflow_manage.get_workflow_content()
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')
@ -85,7 +86,7 @@ class BaseFormNode(IFormNode):
"chat_record_id": self.flow_params_serializer.data.get("chat_record_id"),
'form_data': self.context.get('form_data', {}),
"is_submit": self.context.get("is_submit", False)}
form = f'<form_rander>{json.dumps(form_setting,ensure_ascii=False)}</form_rander>'
form = f'<form_rander>{json.dumps(form_setting, ensure_ascii=False)}</form_rander>'
context = self.workflow_manage.get_workflow_content()
form_content_format = self.workflow_manage.reset_prompt(form_content_format)
prompt_template = PromptTemplate.from_template(form_content_format, template_format='jinja2')

View File

@ -11,11 +11,13 @@ import time
from typing import Dict
from django.db.models import QuerySet
from django.utils.translation import gettext as _
from application.flow.i_step_node import NodeResult
from application.flow.step_node.function_lib_node.i_function_lib_node import IFunctionLibNode
from common.exception.app_exception import AppApiException
from common.util.function_code import FunctionExecutor
from common.util.rsa_util import rsa_long_decrypt
from function_lib.models.function import FunctionLib
from smartdoc.const import CONFIG
@ -38,15 +40,15 @@ def get_field_value(debug_field_list, name, is_required):
if len(result) > 0:
return result[-1]['value']
if is_required:
raise AppApiException(500, f"{name}字段未设置值")
raise AppApiException(500, _('Field: {name} No value set').format(name=name))
return None
def valid_reference_value(_type, value, name):
if _type == 'int':
instance_type = int
instance_type = int | float
elif _type == 'float':
instance_type = float
instance_type = float | int
elif _type == 'dict':
instance_type = dict
elif _type == 'array':
@ -54,13 +56,16 @@ def valid_reference_value(_type, value, name):
elif _type == 'string':
instance_type = str
else:
raise Exception(500, f'字段:{name}类型:{_type} 不支持的类型')
raise Exception(_('Field: {name} Type: {_type} Value: {value} Unsupported types').format(name=name,
_type=_type))
if not isinstance(value, instance_type):
raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
raise Exception(
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
value=value))
def convert_value(name: str, value, _type, is_required, source, node):
if not is_required and value is None:
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
@ -69,6 +74,10 @@ def convert_value(name: str, value, _type, is_required, source, node):
value[0],
value[1:])
valid_reference_value(_type, value, name)
if _type == 'int':
return int(value)
if _type == 'float':
return float(value)
return value
try:
if _type == 'int':
@ -79,26 +88,37 @@ def convert_value(name: str, value, _type, is_required, source, node):
v = json.loads(value)
if isinstance(v, dict):
return v
raise Exception("类型错误")
raise Exception(_('type error'))
if _type == 'array':
v = json.loads(value)
if isinstance(v, list):
return v
raise Exception("类型错误")
raise Exception(_('type error'))
return value
except Exception as e:
raise Exception(f'字段:{name}类型:{_type}值:{value}类型错误')
raise Exception(
_('Field: {name} Type: {_type} Value: {value} Type error').format(name=name, _type=_type,
value=value))
def valid_function(function_lib, user_id):
if function_lib is None:
raise Exception(_('Function does not exist'))
if function_lib.permission_type == 'PRIVATE' and str(function_lib.user_id) != str(user_id):
raise Exception(_('No permission to use this function {name}').format(name=function_lib.name))
if not function_lib.is_active:
raise Exception(_('Function {name} is unavailable').format(name=function_lib.name))
class BaseFunctionLibNodeNode(IFunctionLibNode):
def save_context(self, details, workflow_manage):
self.context['result'] = details.get('result')
self.answer_text = str(details.get('result'))
if self.node_params.get('is_result'):
self.answer_text = str(details.get('result'))
def execute(self, function_lib_id, input_field_list, **kwargs) -> NodeResult:
function_lib = QuerySet(FunctionLib).filter(id=function_lib_id).first()
if not function_lib.is_active:
raise Exception(f'函数:{function_lib.name} 不可用')
valid_function(function_lib, self.flow_params_serializer.data.get('user_id'))
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),
field.get('is_required'),
field.get('source'), self)
@ -107,8 +127,14 @@ class BaseFunctionLibNodeNode(IFunctionLibNode):
), **field}
for field in
function_lib.input_field_list]}
self.context['params'] = params
result = function_executor.exec_code(function_lib.code, params)
# 合并初始化参数
if function_lib.init_params is not None:
all_params = json.loads(rsa_long_decrypt(function_lib.init_params)) | params
else:
all_params = params
result = function_executor.exec_code(function_lib.code, all_params)
return NodeResult({'result': result}, {}, _write_context=write_context)
def get_details(self, index: int, **kwargs):

View File

@ -33,9 +33,9 @@ def write_context(step_variable: Dict, global_variable: Dict, node, workflow):
def valid_reference_value(_type, value, name):
if _type == 'int':
instance_type = int
instance_type = int | float
elif _type == 'float':
instance_type = float
instance_type = float | int
elif _type == 'dict':
instance_type = dict
elif _type == 'array':
@ -49,13 +49,17 @@ def valid_reference_value(_type, value, name):
def convert_value(name: str, value, _type, is_required, source, node):
if not is_required and value is None:
if not is_required and (value is None or (isinstance(value, str) and len(value) == 0)):
return None
if source == 'reference':
value = node.workflow_manage.get_reference_field(
value[0],
value[1:])
valid_reference_value(_type, value, name)
if _type == 'int':
return int(value)
if _type == 'float':
return float(value)
return value
try:
if _type == 'int':
@ -80,7 +84,8 @@ def convert_value(name: str, value, _type, is_required, source, node):
class BaseFunctionNodeNode(IFunctionNode):
def save_context(self, details, workflow_manage):
self.context['result'] = details.get('result')
self.answer_text = str(details.get('result'))
if self.node_params.get('is_result', False):
self.answer_text = str(details.get('result'))
def execute(self, input_field_list, code, **kwargs) -> NodeResult:
params = {field.get('name'): convert_value(field.get('name'), field.get('value'), field.get('type'),

View File

@ -16,7 +16,8 @@ class BaseImageGenerateNode(IImageGenerateNode):
def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer')
self.context['question'] = details.get('question')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, model_id, prompt, negative_prompt, dialogue_number, dialogue_type, history_chat_record, chat_id,
model_params_setting,
@ -24,7 +25,8 @@ class BaseImageGenerateNode(IImageGenerateNode):
**kwargs) -> NodeResult:
print(model_params_setting)
application = self.workflow_manage.work_flow_post_handler.chat_info.application
tti_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'), **model_params_setting)
tti_model = get_model_instance_by_model_user_id(model_id, self.flow_params_serializer.data.get('user_id'),
**model_params_setting)
history_message = self.get_history_message(history_chat_record, dialogue_number)
self.context['history_message'] = history_message
question = self.generate_prompt_question(prompt)

View File

@ -69,7 +69,8 @@ class BaseImageUnderstandNode(IImageUnderstandNode):
def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer')
self.context['question'] = details.get('question')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, model_id, system, prompt, dialogue_number, dialogue_type, history_chat_record, stream, chat_id,
model_params_setting,

View File

@ -0,0 +1,3 @@
# coding=utf-8
from .impl import *

View File

@ -0,0 +1,35 @@
# coding=utf-8
from typing import Type
from rest_framework import serializers
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):
mcp_servers = serializers.JSONField(required=True,
error_messages=ErrMessage.char(_("Mcp servers")))
mcp_server = serializers.CharField(required=True,
error_messages=ErrMessage.char(_("Mcp server")))
mcp_tool = serializers.CharField(required=True, error_messages=ErrMessage.char(_("Mcp tool")))
tool_params = serializers.DictField(required=True,
error_messages=ErrMessage.char(_("Tool parameters")))
class IMcpNode(INode):
type = 'mcp-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return McpNodeSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
def execute(self, mcp_servers, mcp_server, mcp_tool, tool_params, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,3 @@
# coding=utf-8
from .base_mcp_node import BaseMcpNode

View File

@ -0,0 +1,61 @@
# coding=utf-8
import asyncio
import json
from typing import List
from langchain_mcp_adapters.client import MultiServerMCPClient
from application.flow.i_step_node import NodeResult
from application.flow.step_node.mcp_node.i_mcp_node import IMcpNode
class BaseMcpNode(IMcpNode):
def save_context(self, details, workflow_manage):
self.context['result'] = details.get('result')
self.context['tool_params'] = details.get('tool_params')
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:
servers = json.loads(mcp_servers)
params = json.loads(json.dumps(tool_params))
params = self.handle_variables(params)
async def call_tool(s, session, t, a):
async with MultiServerMCPClient(s) as client:
s = await client.sessions[session].call_tool(t, a)
return s
res = asyncio.run(call_tool(servers, mcp_server, mcp_tool, params))
return NodeResult(
{'result': [content.text for content in res.content], 'tool_params': params, 'mcp_tool': mcp_tool}, {})
def handle_variables(self, tool_params):
# 处理参数中的变量
for k, v in tool_params.items():
if type(v) == str:
tool_params[k] = self.workflow_manage.generate_prompt(tool_params[k])
if type(v) == dict:
self.handle_variables(v)
if (type(v) == list) and (type(v[0]) == str):
tool_params[k] = self.get_reference_content(v)
return tool_params
def get_reference_content(self, fields: List[str]):
return str(self.workflow_manage.get_reference_field(
fields[0],
fields[1:]))
def get_details(self, index: int, **kwargs):
return {
'name': self.node.properties.get('stepName'),
"index": index,
'run_time': self.context.get('run_time'),
'status': self.status,
'err_message': self.err_message,
'type': self.node.type,
'mcp_tool': self.context.get('mcp_tool'),
'tool_params': self.context.get('tool_params'),
'result': self.context.get('result'),
}

View File

@ -80,7 +80,8 @@ class BaseQuestionNode(IQuestionNode):
self.context['answer'] = details.get('answer')
self.context['message_tokens'] = details.get('message_tokens')
self.context['answer_tokens'] = details.get('answer_tokens')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, model_id, system, prompt, dialogue_number, history_chat_record, stream, chat_id, chat_record_id,
model_params_setting=None,

View File

@ -88,7 +88,7 @@ class BaseSearchDatasetNode(ISearchDatasetStepNode):
'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
'data': '\n'.join(
[f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
paragraph_list])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
result])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
'directly_return': '\n'.join(
[paragraph.get('content') for paragraph in
result if

View File

@ -18,7 +18,8 @@ class BaseSpeechToTextNode(ISpeechToTextNode):
def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, stt_model_id, chat_id, audio, **kwargs) -> NodeResult:
stt_model = get_model_instance_by_model_user_id(stt_model_id, self.flow_params_serializer.data.get('user_id'))

View File

@ -40,10 +40,13 @@ class BaseStartStepNode(IStarNode):
self.context['document'] = details.get('document_list')
self.context['image'] = details.get('image_list')
self.context['audio'] = details.get('audio_list')
self.context['other'] = details.get('other_list')
self.status = details.get('status')
self.err_message = details.get('err_message')
for key, value in workflow_variable.items():
workflow_manage.context[key] = value
for item in details.get('global_fields', []):
workflow_manage.context[item.get('key')] = item.get('value')
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
pass
@ -59,7 +62,8 @@ class BaseStartStepNode(IStarNode):
'question': question,
'image': self.workflow_manage.image_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,
}
return NodeResult(node_variable, workflow_variable)
@ -83,5 +87,6 @@ class BaseStartStepNode(IStarNode):
'image_list': self.context.get('image'),
'document_list': self.context.get('document'),
'audio_list': self.context.get('audio'),
'other_list': self.context.get('other'),
'global_fields': global_fields
}

View File

@ -37,7 +37,8 @@ def bytes_to_uploaded_file(file_bytes, file_name="generated_audio.mp3"):
class BaseTextToSpeechNode(ITextToSpeechNode):
def save_context(self, details, workflow_manage):
self.context['answer'] = details.get('answer')
self.answer_text = details.get('answer')
if self.node_params.get('is_result', False):
self.answer_text = details.get('answer')
def execute(self, tts_model_id, chat_id,
content, model_params_setting=None,

View File

@ -238,6 +238,7 @@ class WorkflowManage:
base_to_response: BaseToResponse = SystemToResponse(), form_data=None, image_list=None,
document_list=None,
audio_list=None,
other_list=None,
start_node_id=None,
start_node_data=None, chat_record=None, child_node=None):
if form_data is None:
@ -248,12 +249,15 @@ class WorkflowManage:
document_list = []
if audio_list is None:
audio_list = []
if other_list is None:
other_list = []
self.start_node_id = start_node_id
self.start_node = None
self.form_data = form_data
self.image_list = image_list
self.document_list = document_list
self.audio_list = audio_list
self.other_list = other_list
self.params = params
self.flow = flow
self.context = {}
@ -269,11 +273,36 @@ class WorkflowManage:
self.child_node = child_node
self.future_list = []
self.lock = threading.Lock()
self.field_list = []
self.global_field_list = []
self.init_fields()
if start_node_id is not None:
self.load_node(chat_record, start_node_id, start_node_data)
else:
self.node_context = []
def init_fields(self):
field_list = []
global_field_list = []
for node in self.flow.nodes:
properties = node.properties
node_name = properties.get('stepName')
node_id = node.id
node_config = properties.get('config')
if node_config is not None:
fields = node_config.get('fields')
if fields is not None:
for field in fields:
field_list.append({**field, 'node_id': node_id, 'node_name': node_name})
global_fields = node_config.get('globalFields')
if global_fields is not None:
for global_field in global_fields:
global_field_list.append({**global_field, 'node_id': node_id, 'node_name': node_name})
field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
global_field_list.sort(key=lambda f: len(f.get('node_name')), reverse=True)
self.field_list = field_list
self.global_field_list = global_field_list
def append_answer(self, content):
self.answer += content
self.answer_list[-1] += content
@ -739,23 +768,15 @@ class WorkflowManage:
def reset_prompt(self, prompt: str):
placeholder = "{}"
for node in self.flow.nodes:
properties = node.properties
node_config = properties.get('config')
if node_config is not None:
fields = node_config.get('fields')
if fields is not None:
for field in fields:
globeLabel = f"{properties.get('stepName')}.{field.get('value')}"
globeValue = f"context.get('{node.id}',{placeholder}).get('{field.get('value', '')}','')"
prompt = prompt.replace(globeLabel, globeValue)
global_fields = node_config.get('globalFields')
if global_fields is not None:
for field in global_fields:
globeLabel = f"全局变量.{field.get('value')}"
globeLabelNew = f"global.{field.get('value')}"
globeValue = f"context.get('global').get('{field.get('value', '')}','')"
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
for field in self.field_list:
globeLabel = f"{field.get('node_name')}.{field.get('value')}"
globeValue = f"context.get('{field.get('node_id')}',{placeholder}).get('{field.get('value', '')}','')"
prompt = prompt.replace(globeLabel, globeValue)
for field in self.global_field_list:
globeLabel = f"全局变量.{field.get('value')}"
globeLabelNew = f"global.{field.get('value')}"
globeValue = f"context.get('global').get('{field.get('value', '')}','')"
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
return prompt
def generate_prompt(self, prompt: str):

View File

@ -1,11 +1,11 @@
# Generated by Django 4.2.13 on 2024-07-15 15:52
import application.models.application
from django.db import migrations, models
import common.encoder.encoder
class Migration(migrations.Migration):
dependencies = [
('application', '0009_application_type_application_work_flow_and_more'),
]
@ -14,6 +14,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='chatrecord',
name='details',
field=models.JSONField(default=dict, encoder=application.models.application.DateEncoder, verbose_name='对话详情'),
field=models.JSONField(default=dict, encoder=common.encoder.encoder.SystemEncoder, 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

@ -6,15 +6,13 @@
@date2023/9/25 14:24
@desc:
"""
import datetime
import decimal
import json
import uuid
from django.contrib.postgres.fields import ArrayField
from django.db import models
from langchain.schema import HumanMessage, AIMessage
from django.utils.translation import gettext as _
from common.encoder.encoder import SystemEncoder
from common.mixins.app_model_mixin import AppModelMixin
from dataset.models.data_set import DataSet
from setting.models.model_management import Model
@ -117,10 +115,15 @@ class ApplicationDatasetMapping(AppModelMixin):
db_table = "application_dataset_mapping"
def default_asker():
return {'user_name': '游客'}
class Chat(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
application = models.ForeignKey(Application, on_delete=models.CASCADE)
abstract = models.CharField(max_length=1024, verbose_name="摘要")
asker = models.JSONField(verbose_name="访问者", default=default_asker, encoder=SystemEncoder)
client_id = models.UUIDField(verbose_name="客户端id", default=None, null=True)
is_deleted = models.BooleanField(verbose_name="", default=False)
@ -135,18 +138,6 @@ class VoteChoices(models.TextChoices):
TRAMPLE = 1, '反对'
class DateEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, uuid.UUID):
return str(obj)
if isinstance(obj, datetime.datetime):
return obj.strftime("%Y-%m-%d %H:%M:%S")
if isinstance(obj, decimal.Decimal):
return float(obj)
else:
return json.JSONEncoder.default(self, obj)
class ChatRecord(AppModelMixin):
"""
对话日志 详情
@ -163,7 +154,7 @@ class ChatRecord(AppModelMixin):
message_tokens = models.IntegerField(verbose_name="请求token数量", default=0)
answer_tokens = models.IntegerField(verbose_name="响应token数量", default=0)
const = models.IntegerField(verbose_name="总费用", default=0)
details = models.JSONField(verbose_name="对话详情", default=dict, encoder=DateEncoder)
details = models.JSONField(verbose_name="对话详情", default=dict, encoder=SystemEncoder)
improve_paragraph_id_list = ArrayField(verbose_name="改进标注列表",
base_field=models.UUIDField(max_length=128, blank=True)
, default=list)
@ -176,7 +167,11 @@ class ChatRecord(AppModelMixin):
return HumanMessage(content=self.problem_text)
def get_ai_message(self):
return AIMessage(content=self.answer_text)
answer_text = self.answer_text
if answer_text is None or len(str(answer_text).strip()) == 0:
answer_text = _(
'Sorry, no relevant content was found. Please re-describe your problem or provide more information. ')
return AIMessage(content=answer_text)
def get_node_details_runtime_node_id(self, runtime_node_id):
return self.details.get(runtime_node_id, None)

View File

@ -6,6 +6,7 @@
@date2023/11/7 10:02
@desc:
"""
import asyncio
import datetime
import hashlib
import json
@ -23,6 +24,8 @@ from django.db.models import QuerySet
from django.db.models.expressions import RawSQL
from django.http import HttpResponse
from django.template import Template, Context
from langchain_mcp_adapters.client import MultiServerMCPClient
from mcp.client.sse import sse_client
from rest_framework import serializers, status
from rest_framework.utils.formatting import lazy_format
@ -39,13 +42,13 @@ from common.exception.app_exception import AppApiException, NotFound404, AppUnau
from common.field.common import UploadedImageField, UploadedFileField
from common.models.db_model_manage import DBModelManage
from common.response import result
from common.util.common import valid_license, password_encrypt
from common.util.common import valid_license, password_encrypt, restricted_loads
from common.util.field_message import ErrMessage
from common.util.file_util import get_file_content
from dataset.models import DataSet, Document, Image
from dataset.serializers.common_serializers import list_paragraph, get_embedding_model_by_dataset_id_list
from embedding.models import SearchMode
from function_lib.models.function import FunctionLib, PermissionType
from function_lib.models.function import FunctionLib, PermissionType, FunctionType
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer, FunctionLibModelSerializer
from setting.models import AuthOperate, TeamMemberPermission
from setting.models.model_management import Model
@ -60,6 +63,7 @@ chat_cache = cache.caches['chat_cache']
class MKInstance:
def __init__(self, application: dict, function_lib_list: List[dict], version: str):
self.application = application
self.function_lib_list = function_lib_list
@ -144,10 +148,12 @@ class ModelSettingSerializer(serializers.Serializer):
error_messages=ErrMessage.char(_("Thinking process switch")))
reasoning_content_start = serializers.CharField(required=False, allow_null=True, default="<think>",
allow_blank=True, max_length=256,
trim_whitespace=False,
error_messages=ErrMessage.char(
_("The thinking process begins to mark")))
reasoning_content_end = serializers.CharField(required=False, allow_null=True, allow_blank=True, default="</think>",
max_length=256,
trim_whitespace=False,
error_messages=ErrMessage.char(_("End of thinking process marker")))
@ -158,7 +164,7 @@ class ApplicationWorkflowSerializer(serializers.Serializer):
max_length=256, min_length=1,
error_messages=ErrMessage.char(_("Application Description")))
work_flow = serializers.DictField(required=False, error_messages=ErrMessage.dict(_("Workflow Objects")))
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=4096,
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=102400,
error_messages=ErrMessage.char(_("Opening remarks")))
@staticmethod
@ -221,7 +227,7 @@ class ApplicationSerializer(serializers.Serializer):
min_value=0,
max_value=1024,
error_messages=ErrMessage.integer(_("Historical chat records")))
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=4096,
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=102400,
error_messages=ErrMessage.char(_("Opening remarks")))
dataset_id_list = serializers.ListSerializer(required=False, child=serializers.UUIDField(required=True),
allow_null=True,
@ -328,7 +334,8 @@ class ApplicationSerializer(serializers.Serializer):
for field in input_field_list:
if field['assignment_method'] == 'api_input' and field['variable'] in params:
query += f"&{field['variable']}={params[field['variable']]}"
if 'asker' in params:
query += f"&asker={params.get('asker')}"
return query
class AccessTokenSerializer(serializers.Serializer):
@ -488,7 +495,7 @@ class ApplicationSerializer(serializers.Serializer):
min_value=0,
max_value=1024,
error_messages=ErrMessage.integer(_("Historical chat records")))
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=4096,
prologue = serializers.CharField(required=False, allow_null=True, allow_blank=True, max_length=102400,
error_messages=ErrMessage.char(_("Opening remarks")))
dataset_id_list = serializers.ListSerializer(required=False, child=serializers.UUIDField(required=True),
error_messages=ErrMessage.list(_("Related Knowledge Base"))
@ -727,7 +734,7 @@ class ApplicationSerializer(serializers.Serializer):
user_id = self.data.get('user_id')
mk_instance_bytes = self.data.get('file').read()
try:
mk_instance = pickle.loads(mk_instance_bytes)
mk_instance = restricted_loads(mk_instance_bytes)
except Exception as e:
raise AppApiException(1001, _("Unsupported file format"))
application = mk_instance.application
@ -810,8 +817,10 @@ class ApplicationSerializer(serializers.Serializer):
if with_valid:
self.is_valid(raise_exception=True)
application = QuerySet(Application).filter(id=self.data.get("application_id")).first()
return FunctionLibSerializer.Query(data={'user_id': application.user_id, 'is_active': True}).list(
with_valid=True)
return FunctionLibSerializer.Query(
data={'user_id': application.user_id, 'is_active': True,
'function_type': FunctionType.PUBLIC}
).list(with_valid=True)
def get_function_lib(self, function_lib_id, with_valid=True):
if with_valid:
@ -981,6 +990,7 @@ class ApplicationSerializer(serializers.Serializer):
'draggable': application_setting.draggable,
'show_guide': application_setting.show_guide,
'avatar': application_setting.avatar,
'show_avatar': application_setting.show_avatar,
'float_icon': application_setting.float_icon,
'authentication': application_setting.authentication,
'authentication_type': application_setting.authentication_value.get(
@ -989,6 +999,7 @@ class ApplicationSerializer(serializers.Serializer):
'disclaimer_value': application_setting.disclaimer_value,
'custom_theme': application_setting.custom_theme,
'user_avatar': application_setting.user_avatar,
'show_user_avatar': application_setting.show_user_avatar,
'float_location': application_setting.float_location}
return ApplicationSerializer.Query.reset_application(
{**ApplicationSerializer.ApplicationModel(application).data,
@ -1001,7 +1012,8 @@ class ApplicationSerializer(serializers.Serializer):
'stt_autosend': application.stt_autosend,
'file_upload_enable': application.file_upload_enable,
'file_upload_setting': application.file_upload_setting,
'work_flow': application.work_flow,
'work_flow': {'nodes': [node for node in ((application.work_flow or {}).get('nodes', []) or []) if
node.get('id') == 'base-node']},
'show_source': application_access_token.show_source,
'language': application_access_token.language,
**application_setting_dict})
@ -1062,6 +1074,7 @@ class ApplicationSerializer(serializers.Serializer):
for update_key in update_keys:
if update_key in instance and instance.get(update_key) is not None:
application.__setattr__(update_key, instance.get(update_key))
print(application.name)
application.save()
if 'dataset_id_list' in instance:
@ -1080,6 +1093,7 @@ class ApplicationSerializer(serializers.Serializer):
chat_cache.clear_by_application_id(application_id)
application_access_token = QuerySet(ApplicationAccessToken).filter(application_id=application_id).first()
# 更新缓存数据
print(application.name)
get_application_access_token(application_access_token.access_token, False)
return self.one(with_valid=False)
@ -1132,6 +1146,8 @@ class ApplicationSerializer(serializers.Serializer):
instance['file_upload_enable'] = node_data['file_upload_enable']
if 'file_upload_setting' in node_data:
instance['file_upload_setting'] = node_data['file_upload_setting']
if 'name' in node_data:
instance['name'] = node_data['name']
break
def speech_to_text(self, file, with_valid=True):
@ -1201,7 +1217,9 @@ class ApplicationSerializer(serializers.Serializer):
self.is_valid(raise_exception=True)
if with_valid:
self.is_valid()
embed_application = QuerySet(Application).get(id=app_id)
embed_application = QuerySet(Application).filter(id=app_id).first()
if embed_application is None:
raise AppApiException(500, _('Application does not exist'))
if embed_application.type == ApplicationTypeChoices.WORK_FLOW:
work_flow_version = QuerySet(WorkFlowVersion).filter(application_id=embed_application.id).order_by(
'-create_time')[0:1].first()
@ -1300,3 +1318,29 @@ class ApplicationSerializer(serializers.Serializer):
application_api_key.save()
# 写入缓存
get_application_api_key(application_api_key.secret_key, False)
class McpServers(serializers.Serializer):
mcp_servers = serializers.JSONField(required=True)
def get_mcp_servers(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
if '"stdio"' in self.data.get('mcp_servers'):
raise AppApiException(500, _('stdio is not supported'))
servers = json.loads(self.data.get('mcp_servers'))
async def get_mcp_tools(servers):
async with MultiServerMCPClient(servers) as client:
return client.get_tools()
tools = []
for server in servers:
tools += [
{
'server': server,
'name': tool.name,
'description': tool.description,
'args_schema': tool.args_schema,
}
for tool in asyncio.run(get_mcp_tools({server: servers[server]}))]
return tools

View File

@ -116,13 +116,15 @@ class ChatInfo:
}
def to_pipeline_manage_params(self, problem_text: str, post_response_handler: PostResponseHandler,
exclude_paragraph_id_list, client_id: str, client_type, stream=True):
exclude_paragraph_id_list, client_id: str, client_type, stream=True, form_data=None):
if form_data is None:
form_data = {}
params = self.to_base_pipeline_manage_params()
return {**params, 'problem_text': problem_text, 'post_response_handler': post_response_handler,
'exclude_paragraph_id_list': exclude_paragraph_id_list, 'stream': stream, 'client_id': client_id,
'client_type': client_type}
'client_type': client_type, 'form_data': form_data}
def append_chat_record(self, chat_record: ChatRecord, client_id=None):
def append_chat_record(self, chat_record: ChatRecord, client_id=None, asker=None):
chat_record.problem_text = chat_record.problem_text[0:10240] if chat_record.problem_text is not None else ""
chat_record.answer_text = chat_record.answer_text[0:40960] if chat_record.problem_text is not None else ""
is_save = True
@ -137,8 +139,17 @@ class ChatInfo:
if self.application.id is not None:
# 插入数据库
if not QuerySet(Chat).filter(id=self.chat_id).exists():
asker_dict = {'user_name': '游客'}
if asker is not None:
if isinstance(asker, str):
asker_dict = {
'user_name': asker
}
elif isinstance(asker, dict):
asker_dict = asker
Chat(id=self.chat_id, application_id=self.application.id, abstract=chat_record.problem_text[0:1024],
client_id=client_id, update_time=datetime.now()).save()
client_id=client_id, asker=asker_dict, update_time=datetime.now()).save()
else:
Chat.objects.filter(id=self.chat_id).update(update_time=datetime.now())
# 插入会话记录
@ -171,7 +182,8 @@ def get_post_handler(chat_info: ChatInfo):
answer_text_list=answer_list,
run_time=manage.context['run_time'],
index=len(chat_info.chat_record_list) + 1)
chat_info.append_chat_record(chat_record, client_id)
asker = kwargs.get("asker", None)
chat_info.append_chat_record(chat_record, client_id, asker=asker)
# 重新设置缓存
chat_cache.set(chat_id,
chat_info, timeout=60 * 30)
@ -201,12 +213,21 @@ class OpenAIChatSerializer(serializers.Serializer):
return instance.get('messages')[-1].get('content')
@staticmethod
def generate_chat(chat_id, application_id, message, client_id):
def generate_chat(chat_id, application_id, message, client_id, asker=None):
if chat_id is None:
chat_id = str(uuid.uuid1())
chat = QuerySet(Chat).filter(id=chat_id).first()
if chat is None:
Chat(id=chat_id, application_id=application_id, abstract=message[0:1024], client_id=client_id).save()
asker_dict = {'user_name': '游客'}
if asker is not None:
if isinstance(asker, str):
asker_dict = {
'user_name': asker
}
elif isinstance(asker, dict):
asker_dict = asker
Chat(id=chat_id, application_id=application_id, abstract=message[0:1024], client_id=client_id,
asker=asker_dict).save()
return chat_id
def chat(self, instance: Dict, with_valid=True):
@ -220,7 +241,8 @@ class OpenAIChatSerializer(serializers.Serializer):
application_id = self.data.get('application_id')
client_id = self.data.get('client_id')
client_type = self.data.get('client_type')
chat_id = self.generate_chat(chat_id, application_id, message, client_id)
chat_id = self.generate_chat(chat_id, application_id, message, client_id,
asker=instance.get('form_data', {}).get("asker"))
return ChatMessageSerializer(
data={
'chat_id': chat_id, 'message': message,
@ -233,6 +255,7 @@ class OpenAIChatSerializer(serializers.Serializer):
'image_list': instance.get('image_list', []),
'document_list': instance.get('document_list', []),
'audio_list': instance.get('audio_list', []),
'other_list': instance.get('other_list', []),
}
).chat(base_to_response=OpenaiToResponse())
@ -262,6 +285,7 @@ class ChatMessageSerializer(serializers.Serializer):
image_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("picture")))
document_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("document")))
audio_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Audio")))
other_list = serializers.ListField(required=False, error_messages=ErrMessage.list(_("Other")))
child_node = serializers.DictField(required=False, allow_null=True,
error_messages=ErrMessage.dict(_("Child Nodes")))
@ -310,6 +334,7 @@ class ChatMessageSerializer(serializers.Serializer):
stream = self.data.get('stream')
client_id = self.data.get('client_id')
client_type = self.data.get('client_type')
form_data = self.data.get("form_data")
pipeline_manage_builder = PipelineManage.builder()
# 如果开启了问题优化,则添加上问题优化步骤
if chat_info.application.problem_optimization:
@ -331,7 +356,7 @@ class ChatMessageSerializer(serializers.Serializer):
exclude_paragraph_id_list = list(set(paragraph_id_list))
# 构建运行参数
params = chat_info.to_pipeline_manage_params(message, get_post_handler(chat_info), exclude_paragraph_id_list,
client_id, client_type, stream)
client_id, client_type, stream, form_data)
# 运行流水线作业
pipeline_message.run(params)
return pipeline_message.context['chat_result']
@ -359,6 +384,7 @@ class ChatMessageSerializer(serializers.Serializer):
image_list = self.data.get('image_list')
document_list = self.data.get('document_list')
audio_list = self.data.get('audio_list')
other_list = self.data.get('other_list')
user_id = chat_info.application.user_id
chat_record_id = self.data.get('chat_record_id')
chat_record = None
@ -375,7 +401,7 @@ class ChatMessageSerializer(serializers.Serializer):
'client_id': client_id,
'client_type': client_type,
'user_id': user_id}, WorkFlowPostHandler(chat_info, client_id, client_type),
base_to_response, form_data, image_list, document_list, audio_list,
base_to_response, form_data, image_list, document_list, audio_list, other_list,
self.data.get('runtime_node_id'),
self.data.get('node_data'), chat_record, self.data.get('child_node'))
r = work_flow_manage.run()

View File

@ -174,7 +174,14 @@ class ChatSerializers(serializers.Serializer):
condition = base_condition & min_trample_query
else:
condition = base_condition
return query_set.filter(condition).order_by("-application_chat.update_time")
inner_queryset = QuerySet(Chat).filter(application_id=self.data.get("application_id"))
if 'abstract' in self.data and self.data.get('abstract') is not None:
inner_queryset = inner_queryset.filter(abstract__icontains=self.data.get('abstract'))
return {
'inner_queryset': inner_queryset,
'default_queryset': query_set.filter(condition).order_by("-application_chat.update_time")
}
def list(self, with_valid=True):
if with_valid:
@ -216,6 +223,7 @@ class ChatSerializers(serializers.Serializer):
"\n".join([
f"{improve_paragraph_list[index].get('title')}\n{improve_paragraph_list[index].get('content')}"
for index in range(len(improve_paragraph_list))]),
row.get('asker').get('user_name'),
row.get('message_tokens') + row.get('answer_tokens'), row.get('run_time'),
str(row.get('create_time').astimezone(pytz.timezone(TIME_ZONE)).strftime('%Y-%m-%d %H:%M:%S')
)]
@ -242,7 +250,8 @@ class ChatSerializers(serializers.Serializer):
gettext('answer'), gettext('User feedback'),
gettext('Reference segment number'),
gettext('Section title + content'),
gettext('Annotation'), gettext('Consuming tokens'), gettext('Time consumed (s)'),
gettext('Annotation'), gettext('USER'), gettext('Consuming tokens'),
gettext('Time consumed (s)'),
gettext('Question Time')]
for col_idx, header in enumerate(headers, 1):
cell = worksheet.cell(row=1, column=col_idx)

View File

@ -10,7 +10,8 @@ SELECT
application_chat_record_temp."index" as "index",
application_chat_record_temp.improve_paragraph_list as improve_paragraph_list,
application_chat_record_temp.vote_status as vote_status,
application_chat_record_temp.create_time as create_time
application_chat_record_temp.create_time as create_time,
to_json(application_chat.asker) as asker
FROM
application_chat application_chat
LEFT JOIN (
@ -22,6 +23,8 @@ FROM
chat_id
FROM
application_chat_record
WHERE chat_id IN (
SELECT id FROM application_chat ${inner_queryset})
GROUP BY
application_chat_record.chat_id
) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id
@ -34,4 +37,5 @@ FROM
END as improve_paragraph_list
FROM
application_chat_record application_chat_record
) application_chat_record_temp ON application_chat_record_temp.chat_id = application_chat."id"
) application_chat_record_temp ON application_chat_record_temp.chat_id = application_chat."id"
${default_queryset}

View File

@ -1,5 +1,5 @@
SELECT
*
*,to_json(asker) as asker
FROM
application_chat application_chat
LEFT JOIN (
@ -11,6 +11,9 @@ FROM
chat_id
FROM
application_chat_record
WHERE chat_id IN (
SELECT id FROM application_chat ${inner_queryset})
GROUP BY
application_chat_record.chat_id
) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id
) chat_record_temp ON application_chat."id" = chat_record_temp.chat_id
${default_queryset}

View File

@ -38,6 +38,15 @@ class ApplicationApi(ApiMixin):
}
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_STRING,
title=_("Application authentication token"),
description=_("Application authentication token"),
default="token"
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
@ -133,6 +142,27 @@ class ApplicationApi(ApiMixin):
}
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Primary key id"),
description=_("Primary key id")),
'secret_key': openapi.Schema(type=openapi.TYPE_STRING, title=_("Secret key"),
description=_("Secret key")),
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Is activation"),
description=_("Is activation")),
'application_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application ID"),
description=_("Application ID")),
'allow_cross_domain': openapi.Schema(type=openapi.TYPE_BOOLEAN,
title=_("Is cross-domain allowed"),
description=_("Is cross-domain allowed")),
'cross_domain_list': openapi.Schema(type=openapi.TYPE_ARRAY, title=_('Cross-domain list'),
items=openapi.Schema(type=openapi.TYPE_STRING))
}
)
class AccessToken(ApiMixin):
@staticmethod
def get_request_params_api():
@ -171,6 +201,37 @@ class ApplicationApi(ApiMixin):
}
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
required=[],
properties={
'id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Primary key id"),
description=_("Primary key id")),
'access_token': openapi.Schema(type=openapi.TYPE_STRING, title=_("Access Token"),
description=_("Access Token")),
'access_token_reset': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Reset Token"),
description=_("Reset Token")),
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Is activation"),
description=_("Is activation")),
'access_num': openapi.Schema(type=openapi.TYPE_NUMBER, title=_("Number of visits"),
description=_("Number of visits")),
'white_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Whether to enable whitelist"),
description=_("Whether to enable whitelist")),
'white_list': openapi.Schema(type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING), title=_("Whitelist"),
description=_("Whitelist")),
'show_source': openapi.Schema(type=openapi.TYPE_BOOLEAN,
title=_("Whether to display knowledge sources"),
description=_("Whether to display knowledge sources")),
'language': openapi.Schema(type=openapi.TYPE_STRING,
title=_("language"),
description=_("language"))
}
)
class Edit(ApiMixin):
@staticmethod
def get_request_body_api():
@ -367,6 +428,56 @@ class ApplicationApi(ApiMixin):
}
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['id', 'name', 'desc', 'model_id', 'dialogue_number', 'dataset_setting', 'model_setting',
'problem_optimization', 'stt_model_enable', 'stt_model_enable', 'tts_type',
'work_flow'],
properties={
'id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Primary key id"),
description=_("Primary key id")),
'name': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application Name"),
description=_("Application Name")),
'desc': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application Description"),
description=_("Application Description")),
'model_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Model id"),
description=_("Model id")),
"dialogue_number": openapi.Schema(type=openapi.TYPE_NUMBER,
title=_("Number of multi-round conversations"),
description=_("Number of multi-round conversations")),
'prologue': openapi.Schema(type=openapi.TYPE_STRING, title=_("Opening remarks"),
description=_("Opening remarks")),
'dataset_id_list': openapi.Schema(type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
title=_("List of associated knowledge base IDs"),
description=_("List of associated knowledge base IDs")),
'dataset_setting': ApplicationApi.DatasetSetting.get_request_body_api(),
'model_setting': ApplicationApi.ModelSetting.get_request_body_api(),
'problem_optimization': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_("Problem Optimization"),
description=_("Problem Optimization"), default=True),
'type': openapi.Schema(type=openapi.TYPE_STRING, title=_("Application Type"),
description=_("Application Type SIMPLE | WORK_FLOW")),
'problem_optimization_prompt': openapi.Schema(type=openapi.TYPE_STRING,
title=_('Question optimization tips'),
description=_("Question optimization tips"),
default=_(
"() 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")),
'tts_model_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Text-to-speech model ID"),
description=_("Text-to-speech model ID")),
'stt_model_id': openapi.Schema(type=openapi.TYPE_STRING, title=_("Speech-to-text model id"),
description=_("Speech-to-text model id")),
'stt_model_enable': openapi.Schema(type=openapi.TYPE_STRING, title=_("Is speech-to-text enabled"),
description=_("Is speech-to-text enabled")),
'tts_model_enable': openapi.Schema(type=openapi.TYPE_STRING, title=_("Is text-to-speech enabled"),
description=_("Is text-to-speech enabled")),
'tts_type': openapi.Schema(type=openapi.TYPE_STRING, title=_("Text-to-speech type"),
description=_("Text-to-speech type")),
'work_flow': ApplicationApi.WorkFlow.get_request_body_api(),
}
)
class Query(ApiMixin):
@staticmethod
def get_request_params_api():

View File

@ -53,6 +53,85 @@ class ChatClientHistoryApi(ApiMixin):
class OpenAIChatApi(ApiMixin):
@staticmethod
def get_response_body_api():
return openapi.Responses(responses={
200: openapi.Response(description=_('response parameters'),
schema=openapi.Schema(type=openapi.TYPE_OBJECT,
required=['id',
'choices'],
properties={
'id': openapi.Schema(
type=openapi.TYPE_STRING,
title=_(
"Conversation ID")),
'choices': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=[
'message'],
properties={
'finish_reason': openapi.Schema(
type=openapi.TYPE_STRING, ),
'index': openapi.Schema(
type=openapi.TYPE_INTEGER),
'answer_list': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=[
'content'],
properties={
'content': openapi.Schema(
type=openapi.TYPE_STRING),
'view_type': openapi.Schema(
type=openapi.TYPE_STRING),
'runtime_node_id': openapi.Schema(
type=openapi.TYPE_STRING),
'chat_record_id': openapi.Schema(
type=openapi.TYPE_STRING),
'reasoning_content': openapi.Schema(
type=openapi.TYPE_STRING),
}
)),
'message': openapi.Schema(
type=openapi.TYPE_OBJECT,
required=[
'content'],
properties={
'content': openapi.Schema(
type=openapi.TYPE_STRING),
'role': openapi.Schema(
type=openapi.TYPE_STRING)
}),
}
)),
'created': openapi.Schema(
type=openapi.TYPE_INTEGER),
'model': openapi.Schema(
type=openapi.TYPE_STRING),
'object': openapi.Schema(
type=openapi.TYPE_STRING),
'usage': openapi.Schema(
type=openapi.TYPE_OBJECT,
required=[
'completion_tokens',
'prompt_tokens',
'total_tokens'],
properties={
'completion_tokens': openapi.Schema(
type=openapi.TYPE_INTEGER),
'prompt_tokens': openapi.Schema(
type=openapi.TYPE_INTEGER),
'total_tokens': openapi.Schema(
type=openapi.TYPE_INTEGER)
})
}))})
@staticmethod
def get_request_body_api():
return openapi.Schema(type=openapi.TYPE_OBJECT,
@ -240,6 +319,15 @@ class ChatApi(ApiMixin):
}
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_STRING,
title=_("Conversation ID"),
description=_("Conversation ID"),
default="chat_id"
)
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='application_id',

View File

@ -9,6 +9,7 @@ urlpatterns = [
path('application/profile', views.Application.Profile.as_view(), name='application/profile'),
path('application/embed', views.Application.Embed.as_view()),
path('application/authentication', views.Application.Authentication.as_view()),
path('application/mcp_servers', views.Application.McpServers.as_view()),
path('application/<str:application_id>/publish', views.Application.Publish.as_view()),
path('application/<str:application_id>/edit_icon', views.Application.EditIcon.as_view()),
path('application/<str:application_id>/export', views.Application.Export.as_view()),

View File

@ -13,9 +13,11 @@ from rest_framework.views import APIView
from application.serializers.application_version_serializers import ApplicationVersionSerializer
from application.swagger_api.application_version_api import ApplicationVersionApi
from application.views import get_application_operation_object
from common.auth import has_permissions, TokenAuth
from common.constants.permission_constants import PermissionConstants, CompareConstants, ViewPermission, RoleConstants, \
Permission, Group, Operate
from common.log.log import log
from common.response import result
from django.utils.translation import gettext_lazy as _
@ -82,6 +84,8 @@ class ApplicationVersionView(APIView):
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
@log(menu='Application', operate="Modify application version information",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str, work_flow_version_id: str):
return result.success(
ApplicationVersionSerializer.Operate(

View File

@ -9,8 +9,9 @@
from django.core import cache
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, gettext
from drf_yasg.utils import swagger_auto_schema
from langchain_core.prompts import PromptTemplate
from rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
@ -20,10 +21,12 @@ from application.serializers.application_serializers import ApplicationSerialize
from application.serializers.application_statistics_serializers import ApplicationStatisticsSerializer
from application.swagger_api.application_api import ApplicationApi
from application.swagger_api.application_statistics_api import ApplicationStatisticsApi
from application.views.common import get_application_operation_object
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import CompareConstants, PermissionConstants, Permission, Group, Operate, \
ViewPermission, RoleConstants
from common.exception.app_exception import AppAuthenticationFailed
from common.log.log import log
from common.response import result
from common.swagger_api.common_api import CommonApi
from common.util.common import query_params_to_single_dict
@ -152,6 +155,8 @@ class Application(APIView):
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND), PermissionConstants.APPLICATION_EDIT,
compare=CompareConstants.AND)
@log(menu='Application', operate="Modify application icon",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str):
return result.success(
ApplicationSerializer.IconOperate(
@ -168,6 +173,7 @@ class Application(APIView):
tags=[_("Application")]
)
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
@log(menu='Application', operate="Import Application")
def post(self, request: Request):
return result.success(ApplicationSerializer.Import(
data={'user_id': request.user.id, 'file': request.FILES.get('file')}).import_())
@ -182,6 +188,8 @@ class Application(APIView):
)
@has_permissions(lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id')))
@log(menu='Application', operate="Export Application",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def get(self, request: Request, application_id: str):
return ApplicationSerializer.Operate(
data={'application_id': application_id, 'user_id': request.user.id}).export()
@ -335,6 +343,8 @@ class Application(APIView):
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
@log(menu='Application', operate="Add ApiKey",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def post(self, request: Request, application_id: str):
return result.success(
ApplicationSerializer.ApplicationKeySerializer(
@ -363,13 +373,16 @@ class Application(APIView):
operation_id=_("Modify application API_KEY"),
tags=[_('Application/API_KEY')],
manual_parameters=ApplicationApi.ApiKey.Operate.get_request_params_api(),
request_body=ApplicationApi.ApiKey.Operate.get_request_body_api())
request_body=ApplicationApi.ApiKey.Operate.get_request_body_api(),
responses=result.get_api_response(ApplicationApi.ApiKey.Operate.get_response_body_api()))
@has_permissions(ViewPermission(
[RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND), PermissionConstants.APPLICATION_EDIT,
compare=CompareConstants.AND)
@log(menu='Application', operate="Modify application API_KEY",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str, api_key_id: str):
return result.success(
ApplicationSerializer.ApplicationKeySerializer.Operate(
@ -387,6 +400,8 @@ class Application(APIView):
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND), PermissionConstants.APPLICATION_DELETE,
compare=CompareConstants.AND)
@log(menu='Application', operate="Delete Application API_KEY",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def delete(self, request: Request, application_id: str, api_key_id: str):
return result.success(
ApplicationSerializer.ApplicationKeySerializer.Operate(
@ -401,12 +416,15 @@ class Application(APIView):
operation_id=_("Modify Application AccessToken"),
tags=[_('Application/Public Access')],
manual_parameters=ApplicationApi.AccessToken.get_request_params_api(),
request_body=ApplicationApi.AccessToken.get_request_body_api())
request_body=ApplicationApi.AccessToken.get_request_body_api(),
responses=result.get_api_response(ApplicationApi.AccessToken.get_response_body_api()))
@has_permissions(ViewPermission(
[RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
@log(menu='Application', operate="Modify Application AccessToken",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str):
return result.success(
ApplicationSerializer.AccessTokenSerializer(data={'application_id': application_id}).edit(
@ -439,6 +457,7 @@ class Application(APIView):
@swagger_auto_schema(operation_summary=_("Application Certification"),
operation_id=_("Application Certification"),
request_body=ApplicationApi.Authentication.get_request_body_api(),
responses=result.get_api_response(ApplicationApi.Authentication.get_response_body_api()),
tags=[_("Application/Certification")],
security=[])
def post(self, request: Request):
@ -456,8 +475,11 @@ class Application(APIView):
@swagger_auto_schema(operation_summary=_("Create an application"),
operation_id=_("Create an application"),
request_body=ApplicationApi.Create.get_request_body_api(),
responses=result.get_api_response(ApplicationApi.Create.get_response_body_api()),
tags=[_('Application')])
@has_permissions(PermissionConstants.APPLICATION_CREATE, compare=CompareConstants.AND)
@log(menu='Application', operate="Create an application",
get_operation_object=lambda r, k: {'name': r.data.get('name')})
def post(self, request: Request):
return result.success(ApplicationSerializer.Create(data={'user_id': request.user.id}).insert(request.data))
@ -512,6 +534,8 @@ class Application(APIView):
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
@log(menu='Application', operate="Publishing an application",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str):
return result.success(
ApplicationSerializer.Operate(
@ -533,6 +557,8 @@ class Application(APIView):
compare=CompareConstants.AND),
lambda r, k: Permission(group=Group.APPLICATION, operate=Operate.DELETE,
dynamic_tag=k.get('application_id')), compare=CompareConstants.AND)
@log(menu='Application', operate="Deleting application",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def delete(self, request: Request, application_id: str):
return result.success(ApplicationSerializer.Operate(
data={'application_id': application_id, 'user_id': request.user.id}).delete(
@ -550,6 +576,8 @@ class Application(APIView):
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.MANAGE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND))
@log(menu='Application', operate="Modify the application",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str):
return result.success(
ApplicationSerializer.Operate(
@ -660,8 +688,19 @@ class Application(APIView):
dynamic_tag=keywords.get(
'application_id'))],
compare=CompareConstants.AND))
@log(menu='Application', operate="trial listening",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def post(self, request: Request, application_id: str):
byte_data = ApplicationSerializer.Operate(
data={'application_id': application_id, 'user_id': request.user.id}).play_demo_text(request.data)
return HttpResponse(byte_data, status=200, headers={'Content-Type': 'audio/mp3',
'Content-Disposition': 'attachment; filename="abc.mp3"'})
class McpServers(APIView):
authentication_classes = [TokenAuth]
@action(methods=['GET'], detail=False)
@has_permissions(PermissionConstants.APPLICATION_READ, compare=CompareConstants.AND)
def get(self, request: Request):
return result.success(ApplicationSerializer.McpServers(
data={'mcp_servers': request.query_params.get('mcp_servers')}).get_mcp_servers())

View File

@ -19,10 +19,12 @@ from application.serializers.chat_message_serializers import ChatMessageSerializ
from application.serializers.chat_serializers import ChatSerializers, ChatRecordSerializer
from application.swagger_api.chat_api import ChatApi, VoteApi, ChatRecordApi, ImproveApi, ChatRecordImproveApi, \
ChatClientHistoryApi, OpenAIChatApi
from application.views import get_application_operation_object
from common.auth import TokenAuth, has_permissions, OpenAIKeyAuth
from common.constants.authentication_type import AuthenticationType
from common.constants.permission_constants import Permission, Group, Operate, \
RoleConstants, ViewPermission, CompareConstants
from common.log.log import log
from common.response import result
from common.util.common import query_params_to_single_dict
from dataset.serializers.file_serializers import FileSerializer
@ -35,6 +37,7 @@ class Openai(APIView):
@swagger_auto_schema(operation_summary=_("OpenAI Interface Dialogue"),
operation_id=_("OpenAI Interface Dialogue"),
request_body=OpenAIChatApi.get_request_body_api(),
responses=OpenAIChatApi.get_response_body_api(),
tags=[_("OpenAI Dialogue")])
def post(self, request: Request, application_id: str):
return OpenAIChatSerializer(data={'application_id': application_id, 'client_id': request.auth.client_id,
@ -58,6 +61,8 @@ class ChatView(APIView):
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))])
)
@log(menu='Conversation Log', operate="Export conversation",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def post(self, request: Request, application_id: str):
return ChatSerializers.Query(
data={**query_params_to_single_dict(request.query_params), 'application_id': application_id,
@ -89,10 +94,11 @@ class ChatView(APIView):
@swagger_auto_schema(operation_summary=_("Get the workflow temporary session id"),
operation_id=_("Get the workflow temporary session id"),
request_body=ChatApi.OpenWorkFlowTemp.get_request_body_api(),
responses=result.get_api_response(ChatApi.OpenTempChat.get_response_body_api()),
tags=[_("Application/Chat")])
def post(self, request: Request):
return result.success(ChatSerializers.OpenWorkFlowChat(
data={**request.data, 'user_id': request.user.id}).open())
data={'user_id': request.user.id, **request.data}).open())
class OpenTemp(APIView):
authentication_classes = [TokenAuth]
@ -101,6 +107,7 @@ class ChatView(APIView):
@swagger_auto_schema(operation_summary=_("Get a temporary session id"),
operation_id=_("Get a temporary session id"),
request_body=ChatApi.OpenTempChat.get_request_body_api(),
responses=result.get_api_response(ChatApi.OpenTempChat.get_response_body_api()),
tags=[_("Application/Chat")])
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
def post(self, request: Request):
@ -139,6 +146,8 @@ class ChatView(APIView):
'document_list') if 'document_list' in request.data else [],
'audio_list': request.data.get(
'audio_list') if 'audio_list' in request.data else [],
'other_list': request.data.get(
'other_list') if 'other_list' in request.data else [],
'client_type': request.auth.client_type,
'node_id': request.data.get('node_id', None),
'runtime_node_id': request.data.get('runtime_node_id', None),
@ -177,6 +186,8 @@ class ChatView(APIView):
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND),
compare=CompareConstants.AND)
@log(menu='Conversation Log', operate="Delete a conversation",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def delete(self, request: Request, application_id: str, chat_id: str):
return result.success(
ChatSerializers.Operate(
@ -218,6 +229,8 @@ class ChatView(APIView):
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND),
compare=CompareConstants.AND)
@log(menu='Conversation Log', operate="Client deletes conversation",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def delete(self, request: Request, application_id: str, chat_id: str):
return result.success(
ChatSerializers.Operate(
@ -228,13 +241,16 @@ class ChatView(APIView):
@swagger_auto_schema(operation_summary=_("Client modifies dialogue summary"),
operation_id=_("Client modifies dialogue summary"),
request_body=ChatClientHistoryApi.Operate.ReAbstract.get_request_body_api(),
responses=result.get_default_response(),
tags=[_("Application/Conversation Log")])
@has_permissions(ViewPermission(
[RoleConstants.APPLICATION_ACCESS_TOKEN],
[RoleConstants.APPLICATION_ACCESS_TOKEN, RoleConstants.ADMIN, RoleConstants.USER],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))],
compare=CompareConstants.AND),
compare=CompareConstants.AND)
@log(menu='Conversation Log', operate="Client modifies dialogue summary",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str, chat_id: str):
return result.success(
ChatSerializers.Operate(
@ -343,6 +359,8 @@ class ChatView(APIView):
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))])
)
@log(menu='Conversation Log', operate="Like, Dislike",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str, chat_id: str, chat_record_id: str):
return result.success(ChatRecordSerializer.Vote(
data={'vote_status': request.data.get('vote_status'), 'chat_id': chat_id,
@ -390,6 +408,8 @@ class ChatView(APIView):
'dataset_id'))],
compare=CompareConstants.AND
), compare=CompareConstants.AND)
@log(menu='Conversation Log', operate="Annotation",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def put(self, request: Request, application_id: str, chat_id: str, chat_record_id: str, dataset_id: str,
document_id: str):
return result.success(ChatRecordSerializer.Improve(
@ -401,6 +421,7 @@ class ChatView(APIView):
operation_id=_("Add to Knowledge Base"),
manual_parameters=ImproveApi.get_request_params_api_post(),
request_body=ImproveApi.get_request_body_api_post(),
responses=result.get_default_response(),
tags=[_("Application/Conversation Log/Add to Knowledge Base")]
)
@has_permissions(
@ -415,6 +436,8 @@ class ChatView(APIView):
'dataset_id'))],
compare=CompareConstants.AND
), compare=CompareConstants.AND)
@log(menu='Conversation Log', operate="Add to Knowledge Base",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def post(self, request: Request, application_id: str, dataset_id: str):
return result.success(ChatRecordSerializer.PostImprove().post_improve(request.data))
@ -440,6 +463,8 @@ class ChatView(APIView):
'dataset_id'))],
compare=CompareConstants.AND
), compare=CompareConstants.AND)
@log(menu='Conversation Log', operate="Delete a Annotation",
get_operation_object=lambda r, k: get_application_operation_object(k.get('application_id')))
def delete(self, request: Request, application_id: str, chat_id: str, chat_record_id: str,
dataset_id: str,
document_id: str, paragraph_id: str):

View File

@ -0,0 +1,21 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file common.py
@date2025/3/25 16:56
@desc:
"""
from django.db.models import QuerySet
from application.models import Application
def get_application_operation_object(application_id):
application_model = QuerySet(model=Application).filter(id=application_id).first()
if application_model is not None:
return {
"name": application_model.name
}
return {}

View File

@ -11,35 +11,50 @@ import time
from common.cache.mem_cache import MemCache
lock = threading.Lock()
_lock = threading.Lock()
locks = {}
class ModelManage:
cache = MemCache('model', {})
up_clear_time = time.time()
@staticmethod
def _get_lock(_id):
lock = locks.get(_id)
if lock is None:
with _lock:
lock = locks.get(_id)
if lock is None:
lock = threading.Lock()
locks[_id] = lock
return lock
@staticmethod
def get_model(_id, get_model):
# 获取锁
lock.acquire()
try:
model_instance = ModelManage.cache.get(_id)
if model_instance is None or not model_instance.is_cache_model():
model_instance = ModelManage.cache.get(_id)
if model_instance is None:
lock = ModelManage._get_lock(_id)
with lock:
model_instance = ModelManage.cache.get(_id)
if model_instance is None:
model_instance = get_model(_id)
ModelManage.cache.set(_id, model_instance, timeout=60 * 60 * 8)
else:
if model_instance.is_cache_model():
ModelManage.cache.touch(_id, timeout=60 * 60 * 8)
else:
model_instance = get_model(_id)
ModelManage.cache.set(_id, model_instance, timeout=60 * 30)
return model_instance
# 续期
ModelManage.cache.touch(_id, timeout=60 * 30)
ModelManage.clear_timeout_cache()
return model_instance
finally:
# 释放锁
lock.release()
ModelManage.cache.set(_id, model_instance, timeout=60 * 60 * 8)
ModelManage.clear_timeout_cache()
return model_instance
@staticmethod
def clear_timeout_cache():
if time.time() - ModelManage.up_clear_time > 60:
ModelManage.cache.clear_timeout_data()
if time.time() - ModelManage.up_clear_time > 60 * 60:
threading.Thread(target=lambda: ModelManage.cache.clear_timeout_data()).start()
ModelManage.up_clear_time = time.time()
@staticmethod
def delete_key(_id):

View File

@ -6,7 +6,7 @@
@date2023/9/5 14:01
@desc: 用于swagger 分组
"""
from drf_yasg.generators import OpenAPISchemaGenerator
from drf_yasg.inspectors import SwaggerAutoSchema
tags_dict = {
@ -20,10 +20,10 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema):
if "api" in tags and operation_keys:
return [tags_dict.get(operation_keys[1]) if operation_keys[1] in tags_dict else operation_keys[1]]
return tags
class CustomOpenAPISchemaGenerator(OpenAPISchemaGenerator):
def get_schema(self, request=None, public=False):
schema = super().get_schema(request, public)
if request.is_secure():
schema.schemes = ['https']
else:
schema.schemes = ['http']
return schema
schema.schemes = ['https', 'http']
return schema

View File

@ -0,0 +1,30 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file SystemEncoder.py
@date2025/3/17 16:38
@desc:
"""
import datetime
import decimal
import json
import uuid
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
class SystemEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, uuid.UUID):
return str(obj)
if isinstance(obj, datetime.datetime):
return obj.strftime("%Y-%m-%d %H:%M:%S")
if isinstance(obj, decimal.Decimal):
return float(obj)
if isinstance(obj, InMemoryUploadedFile):
return {'name': obj.name, 'size': obj.size}
if isinstance(obj, TemporaryUploadedFile):
return {'name': obj.name, 'size': obj.size}
else:
return json.JSONEncoder.default(self, obj)

View File

@ -12,15 +12,22 @@ from .listener_manage import *
from django.utils.translation import gettext as _
from ..db.sql_execute import update_execute
from common.lock.impl.file_lock import FileLock
lock = FileLock()
update_document_status_sql = """
UPDATE "public"."document"
SET status ="replace"("replace"("replace"(status, '1', '3'), '0', '3'), '4', '3')
WHERE status ~ '1|0|4'
"""
def run():
# QuerySet(Document).filter(status__in=[Status.embedding, Status.queue_up]).update(**{'status': Status.error})
QuerySet(Model).filter(status=setting.models.Status.DOWNLOAD).update(status=setting.models.Status.ERROR,
meta={'message': _('The download process was interrupted, please try again')})
update_execute(update_document_status_sql, [])
if lock.try_lock('event_init', 30 * 30):
try:
QuerySet(Model).filter(status=setting.models.Status.DOWNLOAD).update(status=setting.models.Status.ERROR,
meta={'message': _(
'The download process was interrupted, please try again')})
update_execute(update_document_status_sql, [])
finally:
lock.un_lock('event_init')

View File

@ -238,11 +238,8 @@ class ListenerManagement:
for key in params_dict:
_value_ = params_dict[key]
exec_sql = exec_sql.replace(key, str(_value_))
lock.acquire()
try:
with lock:
native_update(query_set, exec_sql)
finally:
lock.release()
@staticmethod
def embedding_by_document(document_id, embedding_model: Embeddings, state_list=None):
@ -272,9 +269,6 @@ class ListenerManagement:
ListenerManagement.update_status(QuerySet(Document).filter(id=document_id), TaskType.EMBEDDING,
State.STARTED)
# 删除文档向量数据
VectorStore.get_embedding_vector().delete_by_document_id(document_id)
# 根据段落进行向量化处理
page_desc(QuerySet(Paragraph)
.annotate(

View File

@ -22,3 +22,4 @@ from .table_checkbox import *
from .radio_card_field import *
from .label import *
from .slider_field import *
from .switch_field import *

View File

@ -28,6 +28,6 @@ class SwitchField(BaseField):
@param props_info:
"""
super().__init__('Switch', label, required, default_value, relation_show_field_dict,
super().__init__('SwitchInput', label, required, default_value, relation_show_field_dict,
{},
TriggerType.OPTION_LIST, attrs, props_info)

View File

@ -110,24 +110,47 @@ def get_image_id_func():
return get_image_id
title_font_list = [
[36, 100],
[30, 36]
]
def get_title_level(paragraph: Paragraph):
try:
if paragraph.style is not None:
psn = paragraph.style.name
if psn.startswith('Heading') or psn.startswith('TOC 标题') or psn.startswith('标题'):
return int(psn.replace("Heading ", '').replace('TOC 标题', '').replace('标题',
''))
if len(paragraph.runs) == 1:
font_size = paragraph.runs[0].font.size
pt = font_size.pt
if pt >= 30:
for _value, index in zip(title_font_list, range(len(title_font_list))):
if pt >= _value[0] and pt < _value[1]:
return index + 1
except Exception as e:
pass
return None
class DocSplitHandle(BaseSplitHandle):
@staticmethod
def paragraph_to_md(paragraph: Paragraph, doc: Document, images_list, get_image_id):
try:
psn = paragraph.style.name
if psn.startswith('Heading') or psn.startswith('TOC 标题') or psn.startswith('标题'):
title = "".join(["#" for i in range(
int(psn.replace("Heading ", '').replace('TOC 标题', '').replace('标题',
'')))]) + " " + paragraph.text
title_level = get_title_level(paragraph)
if title_level is not None:
title = "".join(["#" for i in range(title_level)]) + " " + paragraph.text
images = reduce(lambda x, y: [*x, *y],
[get_paragraph_element_images(e, doc, images_list, get_image_id) for e in
paragraph._element],
[])
if len(images) > 0:
return title + '\n' + images_to_string(images, doc, images_list, get_image_id) if len(
paragraph.text) > 0 else images_to_string(images, doc, images_list, get_image_id)
return title
except Exception as e:
traceback.print_exc()
return paragraph.text

View File

@ -173,14 +173,15 @@ class PdfSplitHandle(BaseSplitHandle):
# Null characters are not allowed.
chapter_text = chapter_text.replace('\0', '')
# 限制标题长度
real_chapter_title = chapter_title[:256]
# 限制章节内容长度
if 0 < limit < len(chapter_text):
split_text = PdfSplitHandle.split_text(chapter_text, limit)
for text in split_text:
chapters.append({"title": chapter_title, "content": text})
chapters.append({"title": real_chapter_title, "content": text})
else:
chapters.append({"title": chapter_title, "content": chapter_text if chapter_text else chapter_title})
chapters.append({"title": real_chapter_title, "content": chapter_text if chapter_text else real_chapter_title})
# 保存章节内容和章节标题
return chapters

View File

@ -132,7 +132,8 @@ class ZipParseQAHandle(BaseParseQAHandle):
files = zip_ref.namelist()
# 读取压缩包中的文件内容
for file in files:
if file.endswith('/'):
# 跳过 macOS 特有的元数据目录和文件
if file.endswith('/') or file.startswith('__MACOSX'):
continue
with zip_ref.open(file) as f:
# 对文件内容进行处理

View File

@ -82,7 +82,10 @@ class XlsSplitHandle(BaseParseTableHandle):
for row in data:
# 将每个单元格中的内容替换换行符为 <br> 以保留原始格式
md_table += '| ' + ' | '.join(
[str(cell).replace('\n', '<br>') if cell else '' for cell in row]) + ' |\n'
[str(cell)
.replace('\r\n', '<br>')
.replace('\n', '<br>')
if cell else '' for cell in row]) + ' |\n'
md_tables += md_table + '\n\n'
return md_tables

View File

@ -19,36 +19,24 @@ class XlsxSplitHandle(BaseParseTableHandle):
def fill_merged_cells(self, sheet, image_dict):
data = []
# 获取第一行作为标题行
headers = []
for idx, cell in enumerate(sheet[1]):
if cell.value is None:
headers.append(' ' * (idx + 1))
else:
headers.append(cell.value)
# 从第二行开始遍历每一行
for row in sheet.iter_rows(min_row=2, values_only=False):
row_data = {}
for row in sheet.iter_rows(values_only=False):
row_data = []
for col_idx, cell in enumerate(row):
cell_value = cell.value
# 如果单元格为空,并且该单元格在合并单元格内,获取合并单元格的值
if cell_value is None:
for merged_range in sheet.merged_cells.ranges:
if cell.coordinate in merged_range:
cell_value = sheet[merged_range.min_row][merged_range.min_col - 1].value
break
image = image_dict.get(cell_value, None)
if image is not None:
cell_value = f'![](/api/image/{image.id})'
# 使用标题作为键,单元格的值作为值存入字典
row_data[headers[col_idx]] = cell_value
row_data.insert(col_idx, cell_value)
data.append(row_data)
for merged_range in sheet.merged_cells.ranges:
cell_value = data[merged_range.min_row - 1][merged_range.min_col - 1]
for row_index in range(merged_range.min_row, merged_range.max_row + 1):
for col_index in range(merged_range.min_col, merged_range.max_col + 1):
data[row_index - 1][col_index - 1] = cell_value
return data
def handle(self, file, get_buffer, save_image):
@ -65,11 +53,13 @@ class XlsxSplitHandle(BaseParseTableHandle):
paragraphs = []
ws = wb[sheetname]
data = self.fill_merged_cells(ws, image_dict)
for row in data:
row_output = "; ".join([f"{key}: {value}" for key, value in row.items()])
# print(row_output)
paragraphs.append({'title': '', 'content': row_output})
if len(data) >= 2:
head_list = data[0]
for row_index in range(1, len(data)):
row_output = "; ".join(
[f"{head_list[col_index]}: {data[row_index][col_index]}" for col_index in
range(0, len(data[row_index]))])
paragraphs.append({'title': '', 'content': row_output})
result.append({'name': sheetname, 'paragraphs': paragraphs})
@ -78,7 +68,6 @@ class XlsxSplitHandle(BaseParseTableHandle):
return [{'name': file.name, 'paragraphs': []}]
return result
def get_content(self, file, save_image):
try:
# 加载 Excel 文件
@ -94,18 +83,18 @@ class XlsxSplitHandle(BaseParseTableHandle):
# 如果未指定 sheet_name则使用第一个工作表
for sheetname in workbook.sheetnames:
sheet = workbook[sheetname] if sheetname else workbook.active
rows = self.fill_merged_cells(sheet, image_dict)
if len(rows) == 0:
data = self.fill_merged_cells(sheet, image_dict)
if len(data) == 0:
continue
# 提取表头和内容
headers = [f"{key}" for key, value in rows[0].items()]
headers = [f"{value}" for value in data[0]]
# 构建 Markdown 表格
md_table = '| ' + ' | '.join(headers) + ' |\n'
md_table += '| ' + ' | '.join(['---'] * len(headers)) + ' |\n'
for row in rows:
r = [f'{value}' for key, value in row.items()]
for row_index in range(1, len(data)):
r = [f'{value}' for value in data[row_index]]
md_table += '| ' + ' | '.join(
[str(cell).replace('\n', '<br>') if cell is not None else '' for cell in r]) + ' |\n'

View File

@ -14,7 +14,7 @@ from common.handle.base_split_handle import BaseSplitHandle
def post_cell(cell_value):
return cell_value.replace('\n', '<br>').replace('|', '&#124;')
return cell_value.replace('\r\n', '<br>').replace('\n', '<br>').replace('|', '&#124;')
def row_to_md(row):

View File

@ -14,6 +14,7 @@ import zipfile
from typing import List
from urllib.parse import urljoin
from charset_normalizer import detect
from django.db.models import QuerySet
from common.handle.base_split_handle import BaseSplitHandle
@ -28,6 +29,7 @@ from common.util.common import parse_md_image
from dataset.models import Image
from django.utils.translation import gettext_lazy as _
class FileBufferHandle:
buffer = None
@ -75,6 +77,7 @@ def get_image_list(result_list: list, zip_files: List[str]):
if search:
new_image_id = str(uuid.uuid1())
source_image_path = search.group().replace('(', '').replace(')', '')
source_image_path = source_image_path.strip().split(" ")[0]
image_path = urljoin(result.get('name'), '.' + source_image_path if source_image_path.startswith(
'/') else source_image_path)
if not zip_files.__contains__(image_path):
@ -98,6 +101,15 @@ def get_image_list(result_list: list, zip_files: List[str]):
return image_file_list
def get_file_name(file_name):
try:
file_name_code = file_name.encode('cp437')
charset = detect(file_name_code)['encoding']
return file_name_code.decode(charset)
except Exception as e:
return file_name
def filter_image_file(result_list: list, image_list):
image_source_file_list = [image.get('source_file') for image in image_list]
return [r for r in result_list if not image_source_file_list.__contains__(r.get('name', ''))]
@ -114,11 +126,13 @@ class ZipSplitHandle(BaseSplitHandle):
files = zip_ref.namelist()
# 读取压缩包中的文件内容
for file in files:
if file.endswith('/'):
if file.endswith('/') or file.startswith('__MACOSX'):
continue
with zip_ref.open(file) as f:
# 对文件内容进行处理
try:
# 处理一下文件名
f.name = get_file_name(f.name)
value = file_to_paragraph(f, pattern_list, with_filter, limit)
if isinstance(value, list):
result = [*result, *value]

100
apps/common/log/log.py Normal file
View File

@ -0,0 +1,100 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file log.py
@date2025/3/14 16:09
@desc:
"""
from gettext import gettext
from setting.models.log_management import Log
def _get_ip_address(request):
"""
获取ip地址
@param request:
@return:
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def _get_user(request):
"""
获取用户
@param request:
@return:
"""
user = request.user
if user is None:
return {
}
return {
"id": str(user.id),
"email": user.email,
"phone": user.phone,
"nick_name": user.nick_name,
"username": user.username,
"role": user.role,
}
def _get_details(request):
path = request.path
body = request.data
query = request.query_params
return {
'path': path,
'body': body,
'query': query
}
def log(menu: str, operate, get_user=_get_user, get_ip_address=_get_ip_address, get_details=_get_details,
get_operation_object=None):
"""
记录审计日志
@param menu: 操作菜单 str
@param operate: 操作 str|func 如果是一个函数 入参将是一个request 响应为str def operate(request): return "操作菜单"
@param get_user: 获取用户
@param get_ip_address:获取IP地址
@param get_details: 获取执行详情
@param get_operation_object: 获取操作对象
@return:
"""
def inner(func):
def run(view, request, **kwargs):
status = 200
operation_object = {}
try:
if get_operation_object is not None:
operation_object = get_operation_object(request, kwargs)
except Exception as e:
pass
try:
return func(view, request, **kwargs)
except Exception as e:
status = 500
raise e
finally:
ip = get_ip_address(request)
user = get_user(request)
details = get_details(request)
_operate = operate
if callable(operate):
_operate = operate(request)
# 插入审计日志
Log(menu=menu, operate=_operate, user=user, status=status, ip_address=ip, details=details,
operation_object=operation_object).save()
return run
return inner

View File

@ -24,12 +24,13 @@ class GunicornLocalModelService(BaseService):
os.environ.setdefault('SERVER_NAME', 'local_model')
log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s '
bind = f'{CONFIG.get("LOCAL_MODEL_HOST")}:{CONFIG.get("LOCAL_MODEL_PORT")}'
worker = CONFIG.get("LOCAL_MODEL_HOST_WORKER", 1)
cmd = [
'gunicorn', 'smartdoc.wsgi:application',
'-b', bind,
'-k', 'gthread',
'--threads', '200',
'-w', "1",
'-w', str(worker),
'--max-requests', '10240',
'--max-requests-jitter', '2048',
'--access-logformat', log_format,

View File

@ -0,0 +1,62 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file static_headers_middleware.py
@date2024/3/13 18:26
@desc:
"""
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
content = """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script>
window.onload = () => {
var xhr = new XMLHttpRequest()
xhr.open('GET', '/api/user', true)
xhr.setRequestHeader('Content-Type', 'application/json')
const token = localStorage.getItem('token')
const pathname = window.location.pathname
if (token) {
xhr.setRequestHeader('Authorization', token)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
window.location.href = pathname
}
if (xhr.status === 401) {
window.location.href = '/ui/login'
}
}
}
xhr.send()
} else {
window.location.href = '/ui/login'
}
}
</script>
</head>
<body></body>
</html>
"""
class DocHeadersMiddleware(MiddlewareMixin):
def process_response(self, request, response):
if request.path.startswith('/doc/') or request.path.startswith('/doc/chat/'):
HTTP_REFERER = request.META.get('HTTP_REFERER')
if HTTP_REFERER is None:
return HttpResponse(content)
if HTTP_REFERER == request._current_scheme_host + request.path:
return response
return response

View File

@ -10,6 +10,8 @@ import hashlib
import importlib
import io
import mimetypes
import pickle
import random
import re
import shutil
from functools import reduce
@ -23,6 +25,51 @@ from pydub import AudioSegment
from ..exception.app_exception import AppApiException
from ..models.db_model_manage import DBModelManage
safe_builtins = {
'MKInstance'
}
ALLOWED_CLASSES = {
("builtins", "dict"),
('uuid', 'UUID'),
("application.serializers.application_serializers", "MKInstance"),
("function_lib.serializers.function_lib_serializer", "FlibInstance")
}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if (module, name) in ALLOWED_CLASSES:
return super().find_class(module, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
def encryption(message: str):
"""
加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890
:param message:
:return:
"""
max_pre_len = 8
max_post_len = 4
message_len = len(message)
pre_len = int(message_len / 5 * 2)
post_len = int(message_len / 5 * 1)
pre_str = "".join([message[index] for index in
range(0, max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int(pre_len))])
end_str = "".join(
[message[index] for index in
range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len), message_len)])
content = "***************"
return pre_str + content + end_str
def sub_array(array: List, item_num=10):
result = []
@ -251,3 +298,14 @@ def markdown_to_plain_text(md: str) -> str:
# 去除首尾空格
text = text.strip()
return text
SAFE_CHAR_SET = (
[chr(i) for i in range(65, 91) if chr(i) not in {'I', 'O'}] + # 大写字母 A-H, J-N, P-Z
[chr(i) for i in range(97, 123) if chr(i) not in {'i', 'l', 'o'}] + # 小写字母 a-h, j-n, p-z
[str(i) for i in range(10) if str(i) not in {'0', '1', '7'}] # 数字 2-6, 8-9
)
def get_random_chars(number=4):
return ''.join(random.choices(SAFE_CHAR_SET, k=number))

View File

@ -7,13 +7,12 @@
@desc:
"""
import os
import pickle
import subprocess
import sys
import uuid
from textwrap import dedent
from diskcache import Cache
from smartdoc.const import BASE_DIR
from smartdoc.const import PROJECT_DIR
@ -37,6 +36,8 @@ class FunctionExecutor:
old_mask = os.umask(0o077)
try:
os.makedirs(self.sandbox_path, 0o700, exist_ok=True)
os.makedirs(os.path.join(self.sandbox_path, 'execute'), 0o700, exist_ok=True)
os.makedirs(os.path.join(self.sandbox_path, 'result'), 0o700, exist_ok=True)
finally:
os.umask(old_mask)
@ -44,10 +45,11 @@ class FunctionExecutor:
_id = str(uuid.uuid1())
success = '{"code":200,"msg":"成功","data":exec_result}'
err = '{"code":500,"msg":str(e),"data":None}'
path = r'' + self.sandbox_path + ''
result_path = f'{self.sandbox_path}/result/{_id}.result'
_exec_code = f"""
try:
import os
import pickle
env = dict(os.environ)
for key in list(env.keys()):
if key in os.environ and (key.startswith('MAXKB') or key.startswith('POSTGRES') or key.startswith('PG')):
@ -60,13 +62,11 @@ try:
for local in locals_v:
globals_v[local] = locals_v[local]
exec_result=f(**keywords)
from diskcache import Cache
cache = Cache({path!a})
cache.set({_id!a},{success})
with open({result_path!a}, 'wb') as file:
file.write(pickle.dumps({success}))
except Exception as e:
from diskcache import Cache
cache = Cache({path!a})
cache.set({_id!a},{err})
with open({result_path!a}, 'wb') as file:
file.write(pickle.dumps({err}))
"""
if self.sandbox:
subprocess_result = self._exec_sandbox(_exec_code, _id)
@ -74,21 +74,21 @@ except Exception as e:
subprocess_result = self._exec(_exec_code)
if subprocess_result.returncode == 1:
raise Exception(subprocess_result.stderr)
cache = Cache(self.sandbox_path)
result = cache.get(_id)
cache.delete(_id)
with open(result_path, 'rb') as file:
result = pickle.loads(file.read())
os.remove(result_path)
if result.get('code') == 200:
return result.get('data')
raise Exception(result.get('msg'))
def _exec_sandbox(self, _code, _id):
exec_python_file = f'{self.sandbox_path}/{_id}.py'
exec_python_file = f'{self.sandbox_path}/execute/{_id}.py'
with open(exec_python_file, 'w') as file:
file.write(_code)
os.system(f"chown {self.user}:{self.user} {exec_python_file}")
os.system(f"chown {self.user}:root {exec_python_file}")
kwargs = {'cwd': BASE_DIR}
subprocess_result = subprocess.run(
['su', '-c', python_directory + ' ' + exec_python_file, self.user],
['su', '-s', python_directory, '-c', "exec(open('" + exec_python_file + "').read())", self.user],
text=True,
capture_output=True, **kwargs)
os.remove(exec_python_file)

View File

@ -40,15 +40,12 @@ def generate():
def get_key_pair():
rsa_value = rsa_cache.get(cache_key)
if rsa_value is None:
lock.acquire()
rsa_value = rsa_cache.get(cache_key)
if rsa_value is not None:
return rsa_value
try:
with lock:
rsa_value = rsa_cache.get(cache_key)
if rsa_value is not None:
return rsa_value
rsa_value = get_key_pair_by_sql()
rsa_cache.set(cache_key, rsa_value)
finally:
lock.release()
return rsa_value

View File

@ -339,13 +339,14 @@ class SplitModel:
for e in result:
if len(e['content']) > 4096:
pass
return [item for item in [self.post_reset_paragraph(row) for row in result] if
title_list = list(set([row.get('title') for row in result]))
return [item for item in [self.post_reset_paragraph(row, title_list) for row in result] if
'content' in item and len(item.get('content').strip()) > 0]
def post_reset_paragraph(self, paragraph: Dict):
result = self.filter_title_special_characters(paragraph)
def post_reset_paragraph(self, paragraph: Dict, title_list: List[str]):
result = self.content_is_null(paragraph, title_list)
result = self.filter_title_special_characters(result)
result = self.sub_title(result)
result = self.content_is_null(result)
return result
@staticmethod
@ -357,11 +358,14 @@ class SplitModel:
return paragraph
@staticmethod
def content_is_null(paragraph: Dict):
def content_is_null(paragraph: Dict, title_list: List[str]):
if 'title' in paragraph:
title = paragraph.get('title')
content = paragraph.get('content')
if (content is None or len(content.strip()) == 0) and (title is not None and len(title) > 0):
find = [t for t in title_list if t.__contains__(title) and t != title]
if find:
return {'title': '', 'content': ''}
return {'title': '', 'content': title}
return paragraph

View File

@ -12,9 +12,6 @@ from typing import List
import jieba
import jieba.posseg
from jieba import analyse
from common.util.split_model import group_by
jieba_word_list_cache = [chr(item) for item in range(38, 84)]
@ -80,37 +77,12 @@ def get_key_by_word_dict(key, word_dict):
def to_ts_vector(text: str):
# 获取不分词的数据
word_list = get_word_list(text)
# 获取关键词关系
word_dict = to_word_dict(word_list, text)
# 替换字符串
text = replace_word(word_dict, text)
# 分词
filter_word = jieba.analyse.extract_tags(text, topK=100)
result = jieba.lcut(text, HMM=True, use_paddle=True)
# 过滤标点符号
result = [item for item in result if filter_word.__contains__(item) and len(item) < 10]
result_ = [{'word': get_key_by_word_dict(result[index], word_dict), 'index': index} for index in
range(len(result))]
result_group = group_by(result_, lambda r: r['word'])
return " ".join(
[f"{key.lower()}:{','.join([str(item['index'] + 1) for item in result_group[key]][:20])}" for key in
result_group if
not remove_chars.__contains__(key) and len(key.strip()) >= 0])
result = jieba.lcut(text, cut_all=True)
return " ".join(result)
def to_query(text: str):
# 获取不分词的数据
word_list = get_word_list(text)
# 获取关键词关系
word_dict = to_word_dict(word_list, text)
# 替换字符串
text = replace_word(word_dict, text)
extract_tags = analyse.extract_tags(text, topK=5, withWeight=True, allowPOS=('ns', 'n', 'vn', 'v', 'eng'))
result = " ".join([get_key_by_word_dict(word, word_dict) for word, score in extract_tags if
not remove_chars.__contains__(word)])
# 删除词库
for word in word_list:
jieba.del_word(word)
extract_tags = jieba.lcut(text, cut_all=True)
result = " ".join(extract_tags)
return result

View File

@ -82,6 +82,9 @@ class Type(models.TextChoices):
web = 1, 'web站点类型'
lark = 2, '飞书类型'
yuque = 3, '语雀类型'
class HitHandlingMethod(models.TextChoices):
optimization = 'optimization', '模型优化'

View File

@ -40,6 +40,14 @@ def zip_dir(zip_path, output=None):
zip.close()
def is_valid_uuid(s):
try:
uuid.UUID(s)
return True
except ValueError:
return False
def write_image(zip_path: str, image_list: List[str]):
for image in image_list:
search = re.search("\(.*\)", image)
@ -47,6 +55,9 @@ def write_image(zip_path: str, image_list: List[str]):
text = search.group()
if text.startswith('(/api/file/'):
r = text.replace('(/api/file/', '').replace(')', '')
r = r.strip().split(" ")[0]
if not is_valid_uuid(r):
break
file = QuerySet(File).filter(id=r).first()
if file is None:
break
@ -58,6 +69,9 @@ def write_image(zip_path: str, image_list: List[str]):
f.write(file.get_byte())
else:
r = text.replace('(/api/image/', '').replace(')', '')
r = r.strip().split(" ")[0]
if not is_valid_uuid(r):
break
image_model = QuerySet(Image).filter(id=r).first()
if image_model is None:
break
@ -208,3 +222,26 @@ def get_embedding_model_id_by_dataset_id_list(dataset_id_list: List):
if len(dataset_list) == 0:
raise Exception(_('Knowledge base setting error, please reset the knowledge base'))
return str(dataset_list[0].embedding_mode_id)
class GenerateRelatedSerializer(ApiMixin, serializers.Serializer):
model_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('Model id')))
prompt = serializers.CharField(required=True, error_messages=ErrMessage.uuid(_('Prompt word')))
state_list = serializers.ListField(required=False, child=serializers.CharField(required=True),
error_messages=ErrMessage.list("state list"))
@staticmethod
def get_request_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'model_id': openapi.Schema(type=openapi.TYPE_STRING,
title=_('Model id'),
description=_('Model id')),
'prompt': openapi.Schema(type=openapi.TYPE_STRING, title=_('Prompt word'),
description=_("Prompt word")),
'state_list': openapi.Schema(type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
title=_('state list'))
}
)

View File

@ -23,6 +23,7 @@ from django.contrib.postgres.fields import ArrayField
from django.core import validators
from django.db import transaction, models
from django.db.models import QuerySet
from django.db.models.functions import Reverse, Substr
from django.http import HttpResponse
from drf_yasg import openapi
from rest_framework import serializers
@ -42,9 +43,10 @@ from common.util.split_model import get_split_model
from dataset.models.data_set import DataSet, Document, Paragraph, Problem, Type, ProblemParagraphMapping, TaskType, \
State, File, Image
from dataset.serializers.common_serializers import list_paragraph, MetaSerializer, ProblemParagraphManage, \
get_embedding_model_by_dataset_id, get_embedding_model_id_by_dataset_id, write_image, zip_dir
get_embedding_model_by_dataset_id, get_embedding_model_id_by_dataset_id, write_image, zip_dir, \
GenerateRelatedSerializer
from dataset.serializers.document_serializers import DocumentSerializers, DocumentInstanceSerializer
from dataset.task import sync_web_dataset, sync_replace_web_dataset
from dataset.task import sync_web_dataset, sync_replace_web_dataset, generate_related_by_dataset_id
from embedding.models import SearchMode
from embedding.task import embedding_by_dataset, delete_embedding_by_dataset
from setting.models import AuthOperate, Model
@ -453,11 +455,13 @@ class DataSetSerializers(serializers.ModelSerializer):
# 批量插入关联问题
QuerySet(ProblemParagraphMapping).bulk_create(problem_paragraph_mapping_list) if len(
problem_paragraph_mapping_list) > 0 else None
# 响应数据
return {**DataSetSerializers(dataset).data,
'document_list': DocumentSerializers.Query(data={'dataset_id': dataset_id}).list(
with_valid=True)}, dataset_id
'user_id': user_id,
'document_list': document_model_list,
"document_count": len(document_model_list),
"char_length": reduce(lambda x, y: x + y, [d.char_length for d in document_model_list],
0)}, dataset_id
@staticmethod
def get_last_url_path(url):
@ -814,6 +818,31 @@ class DataSetSerializers(serializers.ModelSerializer):
except AlreadyQueued as e:
raise AppApiException(500, _('Failed to send the vectorization task, please try again later!'))
def generate_related(self, instance: Dict, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
GenerateRelatedSerializer(data=instance).is_valid(raise_exception=True)
dataset_id = self.data.get('id')
model_id = instance.get("model_id")
prompt = instance.get("prompt")
state_list = instance.get('state_list')
ListenerManagement.update_status(QuerySet(Document).filter(dataset_id=dataset_id),
TaskType.GENERATE_PROBLEM,
State.PENDING)
ListenerManagement.update_status(QuerySet(Paragraph).annotate(
reversed_status=Reverse('status'),
task_type_status=Substr('reversed_status', TaskType.GENERATE_PROBLEM.value,
1),
).filter(task_type_status__in=state_list, dataset_id=dataset_id)
.values('id'),
TaskType.GENERATE_PROBLEM,
State.PENDING)
ListenerManagement.get_aggregation_document_status_by_dataset_id(dataset_id)()
try:
generate_related_by_dataset_id.delay(dataset_id, model_id, prompt, state_list)
except AlreadyQueued as e:
raise AppApiException(500, _('Failed to send the vectorization task, please try again later!'))
def list_application(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)

View File

@ -23,6 +23,8 @@ from django.db import transaction, models
from django.db.models import QuerySet, Count
from django.db.models.functions import Substr, Reverse
from django.http import HttpResponse
from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as _, gettext, to_locale
from drf_yasg import openapi
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
from rest_framework import serializers
@ -64,8 +66,6 @@ from embedding.task.embedding import embedding_by_document, delete_embedding_by_
embedding_by_document_list
from setting.models import Model
from smartdoc.conf import PROJECT_DIR
from django.utils.translation import gettext_lazy as _, gettext, to_locale
from django.utils.translation import get_language
parse_qa_handle_list = [XlsParseQAHandle(), CsvParseQAHandle(), XlsxParseQAHandle(), ZipParseQAHandle()]
parse_table_handle_list = [CsvSplitTableHandle(), XlsSplitTableHandle(), XlsxSplitTableHandle()]
@ -661,6 +661,8 @@ class DocumentSerializers(ApiMixin, serializers.Serializer):
cell = worksheet.cell(row=row_idx + 1, column=col_idx + 1)
if isinstance(col, str):
col = re.sub(ILLEGAL_CHARACTERS_RE, '', col)
if col.startswith(('=', '+', '-', '@')):
col = '\ufeff' + col
cell.value = col
# 创建HttpResponse对象返回Excel文件
return workbook
@ -703,6 +705,8 @@ class DocumentSerializers(ApiMixin, serializers.Serializer):
@staticmethod
def reset_document_name(document_name):
if document_name is not None:
document_name = document_name.strip()[0:29]
if document_name is None or not Utils.valid_sheet_name(document_name):
return "Sheet"
return document_name.strip()

View File

@ -28,6 +28,9 @@ mime_types = {"html": "text/html", "htm": "text/html", "shtml": "text/html", "cs
"woff2": "font/woff2", "jar": "application/java-archive", "war": "application/java-archive",
"ear": "application/java-archive", "json": "application/json", "hqx": "application/mac-binhex40",
"doc": "application/msword", "pdf": "application/pdf", "ps": "application/postscript",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"eps": "application/postscript", "ai": "application/postscript", "rtf": "application/rtf",
"m3u8": "application/vnd.apple.mpegurl", "kml": "application/vnd.google-earth.kml+xml",
"kmz": "application/vnd.google-earth.kmz", "xls": "application/vnd.ms-excel",
@ -87,4 +90,4 @@ class FileSerializer(serializers.Serializer):
'Content-Disposition': 'attachment; filename="{}"'.format(
file.file_name)})
return HttpResponse(file.get_byte(), status=200,
headers={'Content-Type': mime_types.get(file.file_name.split(".")[-1], 'text/plain')})
headers={'Content-Type': mime_types.get(file_type, 'text/plain')})

View File

@ -2,6 +2,7 @@ UPDATE "document"
SET "char_length" = ( SELECT CASE WHEN
"sum" ( "char_length" ( "content" ) ) IS NULL THEN
0 ELSE "sum" ( "char_length" ( "content" ) )
END FROM paragraph WHERE "document_id" = %s )
END FROM paragraph WHERE "document_id" = %s ),
"update_time" = CURRENT_TIMESTAMP
WHERE
"id" = %s

View File

@ -64,6 +64,17 @@ def get_is_the_task_interrupted(document_id):
return is_the_task_interrupted
@celery_app.task(base=QueueOnce, once={'keys': ['dataset_id']},
name='celery:generate_related_by_dataset')
def generate_related_by_dataset_id(dataset_id, model_id, prompt, state_list=None):
document_list = QuerySet(Document).filter(dataset_id=dataset_id)
for document in document_list:
try:
generate_related_by_document_id.delay(document.id, model_id, prompt, state_list)
except Exception as e:
pass
@celery_app.task(base=QueueOnce, once={'keys': ['document_id']},
name='celery:generate_related_by_document')
def generate_related_by_document_id(document_id, model_id, prompt, state_list=None):

View File

@ -54,5 +54,8 @@ def sync_replace_web_dataset(dataset_id: str, url: str, selector: str):
def sync_web_document(dataset_id, source_url_list: List[str], selector: str):
handler = get_sync_web_document_handler(dataset_id)
for source_url in source_url_list:
result = Fork(base_fork_url=source_url, selector_list=selector.split(' ')).fork()
handler(source_url, selector, result)
try:
result = Fork(base_fork_url=source_url, selector_list=selector.split(' ')).fork()
handler(source_url, selector, result)
except Exception as e:
pass

View File

@ -1,4 +1,4 @@
Section title (optional), Section content (requiredquestion answerno more than 4096 characters), Question (optionalone per line in the cell)
Section title (optional), Section content (requiredquestion answer), Question (optionalone per line in the cell)
MaxKB product introduction,"MaxKB is a knowledge base question-answering system based on the LLM large language model. MaxKB = Max Knowledge Baseaims to become the most powerful brain of the enterprise。Out-of-the-box: supports direct document upload、automatic crawling of online documents、automatic text splitting and vectorization、and good intelligent question-answering interactive experienceSeamless embedding: supports zero-coding and rapid embedding into third-party business systemsMulti-model support: supports docking with mainstream large modelsincluding Ollama local private large models (such as Llama 2、Llama 3、qwen)、Tongyi Qianwen、OpenAI、Azure OpenAI、Kimi、Zhipu AI、iFlytek Spark and Baidu Qianfan large models、etc.","What is MaxKB?
MaxKB product introduction
Large language model supported by MaxKB

1 Section title (optional) Section content (required,question answer,no more than 4096 characters) Section content (required,question answer) Question (optional,one per line in the cell)
2 MaxKB product introduction MaxKB is a knowledge base question-answering system based on the LLM large language model. MaxKB = Max Knowledge Base,aims to become the most powerful brain of the enterprise。Out-of-the-box: supports direct document upload、automatic crawling of online documents、automatic text splitting and vectorization、and good intelligent question-answering interactive experience;Seamless embedding: supports zero-coding and rapid embedding into third-party business systems;Multi-model support: supports docking with mainstream large models,including Ollama local private large models (such as Llama 2、Llama 3、qwen)、Tongyi Qianwen、OpenAI、Azure OpenAI、Kimi、Zhipu AI、iFlytek Spark and Baidu Qianfan large models、etc. MaxKB is a knowledge base question-answering system based on the LLM large language model. MaxKB = Max Knowledge Base,aims to become the most powerful brain of the enterprise。Out-of-the-box: supports direct document upload、automatic crawling of online documents、automatic text splitting and vectorization、and good intelligent question-answering interactive experience;Seamless embedding: supports zero-coding and rapid embedding into third-party business systems;Multi-model support: supports docking with mainstream large models,including Ollama local private large models (such as Llama 2、Llama 3、qwen)、Tongyi Qianwen、OpenAI、Azure OpenAI、Kimi、Zhipu AI、iFlytek Spark and Baidu Qianfan large models、etc. What is MaxKB? MaxKB product introduction Large language model supported by MaxKB MaxKB advantages
3
4

View File

@ -1,4 +1,4 @@
分段标题(选填),分段内容(必填,问题答案最长不超过4096个字符,问题(选填,单元格内一行一个)
分段标题(选填),分段内容(必填,问题答案)),问题(选填,单元格内一行一个)
MaxKB产品介绍,"MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB = Max Knowledge Base旨在成为企业的最强大脑。
开箱即用:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化,智能问答交互体验好;
无缝嵌入:支持零编码快速嵌入到第三方业务系统;

1 分段标题(选填) 分段内容(必填,问题答案,最长不超过4096个字符)) 分段内容(必填,问题答案)) 问题(选填,单元格内一行一个)
2 MaxKB产品介绍 MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB = Max Knowledge Base,旨在成为企业的最强大脑。 开箱即用:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化,智能问答交互体验好; 无缝嵌入:支持零编码快速嵌入到第三方业务系统; 多模型支持:支持对接主流的大模型,包括 Ollama 本地私有大模型(如 Llama 2、Llama 3、qwen)、通义千问、OpenAI、Azure OpenAI、Kimi、智谱 AI、讯飞星火和百度千帆大模型等。 MaxKB 是一款基于 LLM 大语言模型的知识库问答系统。MaxKB = Max Knowledge Base,旨在成为企业的最强大脑。 开箱即用:支持直接上传文档、自动爬取在线文档,支持文本自动拆分、向量化,智能问答交互体验好; 无缝嵌入:支持零编码快速嵌入到第三方业务系统; 多模型支持:支持对接主流的大模型,包括 Ollama 本地私有大模型(如 Llama 2、Llama 3、qwen)、通义千问、OpenAI、Azure OpenAI、Kimi、智谱 AI、讯飞星火和百度千帆大模型等。 MaxKB是什么? MaxKB产品介绍 MaxKB支持的大语言模型 MaxKB优势
3
4

View File

@ -1,4 +1,4 @@
分段標題(選填),分段內容(必填,問題答案最長不超過4096個字元,問題(選填,單元格內一行一個)
分段標題(選填),分段內容(必填,問題答案)),問題(選填,單元格內一行一個)
MaxKB產品介紹,"MaxKB 是一款基於 LLM 大語言模型的知識庫問答系統。MaxKB = Max Knowledge Base旨在成為企業的最強大大腦。
開箱即用:支援直接上傳文檔、自動爬取線上文檔,支援文字自動分割、向量化,智慧問答互動體驗好;
無縫嵌入:支援零編碼快速嵌入到第三方業務系統;

1 分段標題(選填) 分段內容(必填,問題答案,最長不超過4096個字元)) 分段內容(必填,問題答案)) 問題(選填,單元格內一行一個)
2 MaxKB產品介紹 MaxKB 是一款基於 LLM 大語言模型的知識庫問答系統。MaxKB = Max Knowledge Base,旨在成為企業的最強大大腦。 開箱即用:支援直接上傳文檔、自動爬取線上文檔,支援文字自動分割、向量化,智慧問答互動體驗好; 無縫嵌入:支援零編碼快速嵌入到第三方業務系統; 多模型支援:支持對接主流的大模型,包括Ollama 本地私有大模型(如Llama 2、Llama 3、qwen)、通義千問、OpenAI、Azure OpenAI、Kimi、智譜AI、訊飛星火和百度千帆大模型等。 MaxKB 是一款基於 LLM 大語言模型的知識庫問答系統。MaxKB = Max Knowledge Base,旨在成為企業的最強大大腦。 開箱即用:支援直接上傳文檔、自動爬取線上文檔,支援文字自動分割、向量化,智慧問答互動體驗好; 無縫嵌入:支援零編碼快速嵌入到第三方業務系統; 多模型支援:支持對接主流的大模型,包括Ollama 本地私有大模型(如Llama 2、Llama 3、qwen)、通義千問、OpenAI、Azure OpenAI、Kimi、智譜AI、訊飛星火和百度千帆大模型等。 MaxKB是什麼? MaxKB產品介紹 MaxKB支援的大語言模型 MaxKB優勢
3
4

View File

@ -11,6 +11,8 @@ urlpatterns = [
path('dataset/<str:dataset_id>/export', views.Dataset.Export.as_view(), name="export"),
path('dataset/<str:dataset_id>/export_zip', views.Dataset.ExportZip.as_view(), name="export_zip"),
path('dataset/<str:dataset_id>/re_embedding', views.Dataset.Embedding.as_view(), name="dataset_key"),
path('dataset/<str:dataset_id>/generate_related', views.Dataset.GenerateRelated.as_view(),
name="dataset_generate_related"),
path('dataset/<str:dataset_id>/application', views.Dataset.Application.as_view()),
path('dataset/<int:current_page>/<int:page_size>', views.Dataset.Page.as_view(), name="dataset"),
path('dataset/<str:dataset_id>/sync_web', views.Dataset.SyncWeb.as_view()),

View File

@ -0,0 +1,56 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file common.py.py
@date2025/3/25 15:43
@desc:
"""
from django.db.models import QuerySet
from dataset.models import DataSet, Document
def get_dataset_operation_object(dataset_id: str):
dataset_model = QuerySet(model=DataSet).filter(id=dataset_id).first()
if dataset_model is not None:
return {
"name": dataset_model.name,
"desc": dataset_model.desc,
"type": dataset_model.type,
"create_time": dataset_model.create_time,
"update_time": dataset_model.update_time
}
return {}
def get_document_operation_object(document_id: str):
document_model = QuerySet(model=Document).filter(id=document_id).first()
if document_model is not None:
return {
"name": document_model.name,
"type": document_model.type,
}
return {}
def get_document_operation_object_batch(document_id_list: str):
document_model_list = QuerySet(model=Document).filter(id__in=document_id_list)
if document_model_list is not None:
return {
"name": f'[{",".join([document_model.name for document_model in document_model_list])}]',
'document_list': [{'name': document_model.name, 'type': document_model.type} for document_model in
document_model_list]
}
return {}
def get_dataset_document_operation_object(dataset_dict: dict, document_dict: dict):
return {
'name': f'{dataset_dict.get("name", "")}/{document_dict.get("name", "")}',
'dataset_name': dataset_dict.get("name", ""),
'dataset_desc': dataset_dict.get("desc", ""),
'dataset_type': dataset_dict.get("type", ""),
'document_name': document_dict.get("name", ""),
'document_type': document_dict.get("type", ""),
}

View File

@ -13,13 +13,17 @@ from rest_framework.parsers import MultiPartParser
from rest_framework.views import APIView
from rest_framework.views import Request
import dataset.models
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import PermissionConstants, CompareConstants, Permission, Group, Operate, \
ViewPermission, RoleConstants
from common.log.log import log
from common.response import result
from common.response.result import get_page_request_params, get_page_api_response, get_api_response
from common.swagger_api.common_api import CommonApi
from dataset.serializers.common_serializers import GenerateRelatedSerializer
from dataset.serializers.dataset_serializers import DataSetSerializers
from dataset.views.common import get_dataset_operation_object
from setting.serializers.provider_serializers import ModelSerializer
from django.utils.translation import gettext_lazy as _
@ -31,8 +35,8 @@ class Dataset(APIView):
authentication_classes = [TokenAuth]
@action(methods=['PUT'], detail=False)
@swagger_auto_schema(operation_summary="同步Web站点知识库",
operation_id="同步Web站点知识库",
@swagger_auto_schema(operation_summary=_("Synchronize the knowledge base of the website"),
operation_id=_("Synchronize the knowledge base of the website"),
manual_parameters=DataSetSerializers.SyncWeb.get_request_params_api(),
responses=result.get_default_response(),
tags=[_('Knowledge Base')])
@ -42,6 +46,8 @@ class Dataset(APIView):
dynamic_tag=keywords.get('dataset_id'))],
compare=CompareConstants.AND), PermissionConstants.DATASET_EDIT,
compare=CompareConstants.AND)
@log(menu='Knowledge Base', operate="Synchronize the knowledge base of the website",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def put(self, request: Request, dataset_id: str):
return result.success(DataSetSerializers.SyncWeb(
data={'sync_type': request.query_params.get('sync_type'), 'id': dataset_id,
@ -52,14 +58,17 @@ class Dataset(APIView):
parser_classes = [MultiPartParser]
@action(methods=['POST'], detail=False)
@swagger_auto_schema(operation_summary="创建QA知识库",
operation_id="创建QA知识库",
@swagger_auto_schema(operation_summary=_("Create QA knowledge base"),
operation_id=_("Create QA knowledge base"),
manual_parameters=DataSetSerializers.Create.CreateQASerializers.get_request_params_api(),
responses=get_api_response(
DataSetSerializers.Create.CreateQASerializers.get_response_body_api()),
tags=[_('Knowledge Base')]
)
@has_permissions(PermissionConstants.DATASET_CREATE, compare=CompareConstants.AND)
@log(menu='Knowledge Base', operate="Create QA knowledge base",
get_operation_object=lambda r, keywords: {'name': r.data.get('name'), 'desc': r.data.get('desc'),
'file_list': r.FILES.getlist('file')})
def post(self, request: Request):
return result.success(DataSetSerializers.Create(data={'user_id': request.user.id}).save_qa({
'file_list': request.FILES.getlist('file'),
@ -79,6 +88,13 @@ class Dataset(APIView):
tags=[_('Knowledge Base')]
)
@has_permissions(PermissionConstants.DATASET_CREATE, compare=CompareConstants.AND)
@log(menu='Knowledge Base', operate="Create a web site knowledge base",
get_operation_object=lambda r, keywords: {'name': r.data.get('name'), 'desc': r.data.get('desc'),
'file_list': r.FILES.getlist('file'),
'meta': {'source_url': r.data.get('source_url'),
'selector': r.data.get('selector'),
'embedding_mode_id': r.data.get('embedding_mode_id')}}
)
def post(self, request: Request):
return result.success(DataSetSerializers.Create(data={'user_id': request.user.id}).save_web(request.data))
@ -117,6 +133,8 @@ class Dataset(APIView):
tags=[_('Knowledge Base')]
)
@has_permissions(PermissionConstants.DATASET_CREATE, compare=CompareConstants.AND)
@log(menu='Knowledge Base', operate="Create a knowledge base",
get_operation_object=lambda r, keywords: {'name': r.data.get('name'), 'desc': r.data.get('desc')})
def post(self, request: Request):
return result.success(DataSetSerializers.Create(data={'user_id': request.user.id}).save(request.data))
@ -150,10 +168,30 @@ class Dataset(APIView):
)
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=keywords.get('dataset_id')))
@log(menu='Knowledge Base', operate="Re-vectorize",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def put(self, request: Request, dataset_id: str):
return result.success(
DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).re_embedding())
class GenerateRelated(APIView):
authentication_classes = [TokenAuth]
@action(methods=['PUT'], detail=False)
@swagger_auto_schema(operation_summary=_('Generate related'), operation_id=_('Generate related'),
manual_parameters=DataSetSerializers.Operate.get_request_params_api(),
request_body=GenerateRelatedSerializer.get_request_body_api(),
responses=result.get_default_response(),
tags=[_('Knowledge Base')]
)
@log(menu='document', operate="Generate related documents",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id'))
)
def put(self, request: Request, dataset_id: str):
return result.success(
DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).generate_related(
request.data))
class Export(APIView):
authentication_classes = [TokenAuth]
@ -164,6 +202,8 @@ class Dataset(APIView):
)
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=keywords.get('dataset_id')))
@log(menu='Knowledge Base', operate="Export knowledge base",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def get(self, request: Request, dataset_id: str):
return DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).export_excel()
@ -178,6 +218,8 @@ class Dataset(APIView):
)
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=keywords.get('dataset_id')))
@log(menu='Knowledge Base', operate="Export knowledge base containing images",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def get(self, request: Request, dataset_id: str):
return DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).export_zip()
@ -193,6 +235,8 @@ class Dataset(APIView):
dynamic_tag=keywords.get('dataset_id')),
lambda r, k: Permission(group=Group.DATASET, operate=Operate.DELETE,
dynamic_tag=k.get('dataset_id')), compare=CompareConstants.AND)
@log(menu='Knowledge Base', operate="Delete knowledge base",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def delete(self, request: Request, dataset_id: str):
operate = DataSetSerializers.Operate(data={'id': dataset_id})
return result.success(operate.delete())
@ -219,6 +263,8 @@ class Dataset(APIView):
)
@has_permissions(lambda r, keywords: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=keywords.get('dataset_id')))
@log(menu='Knowledge Base', operate="Modify knowledge base information",
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def put(self, request: Request, dataset_id: str):
return result.success(
DataSetSerializers.Operate(data={'id': dataset_id, 'user_id': request.user.id}).edit(request.data,

View File

@ -7,6 +7,7 @@
@desc:
"""
from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser
@ -15,12 +16,14 @@ from rest_framework.views import Request
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import Permission, Group, Operate, CompareConstants
from common.log.log import log
from common.response import result
from common.util.common import query_params_to_single_dict
from dataset.serializers.common_serializers import BatchSerializer
from dataset.serializers.document_serializers import DocumentSerializers, DocumentWebInstanceSerializer
from dataset.swagger_api.document_api import DocumentApi
from django.utils.translation import gettext_lazy as _
from dataset.views.common import get_dataset_document_operation_object, get_dataset_operation_object, \
get_document_operation_object_batch, get_document_operation_object
class Template(APIView):
@ -60,6 +63,11 @@ class WebDocument(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Create Web site documents",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
{'name': f'[{",".join([url for url in r.data.get("source_url_list", [])])}]',
'document_list': [{'name': url} for url in r.data.get("source_url_list", [])]}))
def post(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save_web(request.data, with_valid=True))
@ -78,6 +86,11 @@ class QaDocument(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Import QA and create documentation",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
{'name': f'[{",".join([file.name for file in r.FILES.getlist("file")])}]',
'document_list': [{'name': file.name} for file in r.FILES.getlist("file")]}))
def post(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save_qa(
@ -98,6 +111,11 @@ class TableDocument(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Import tables and create documents",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
{'name': f'[{",".join([file.name for file in r.FILES.getlist("file")])}]',
'document_list': [{'name': file.name} for file in r.FILES.getlist("file")]}))
def post(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save_table(
@ -118,6 +136,10 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Create document",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
{'name': r.data.get('name')}))
def post(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Create(data={'dataset_id': dataset_id}).save(request.data, with_valid=True))
@ -151,6 +173,10 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Modify document hit processing methods in batches",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data.get('id_list'))))
def put(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_edit_hit_handling(request.data))
@ -170,6 +196,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Create documents in batches",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
{'name': f'[{",".join([document.get("name") for document in r.data])}]',
'document_list': r.data})
)
def post(self, request: Request, dataset_id: str):
return result.success(DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_save(request.data))
@ -184,6 +216,11 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Batch sync documents",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data.get('id_list')))
)
def put(self, request: Request, dataset_id: str):
return result.success(DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_sync(request.data))
@ -198,6 +235,10 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Delete documents in batches",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data.get('id_list'))))
def delete(self, request: Request, dataset_id: str):
return result.success(DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_delete(request.data))
@ -214,6 +255,11 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Synchronize web site types",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
))
def put(self, request: Request, dataset_id: str, document_id: str):
return result.success(
DocumentSerializers.Sync(data={'document_id': document_id, 'dataset_id': dataset_id}).sync(
@ -233,6 +279,11 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Cancel task",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
))
def put(self, request: Request, dataset_id: str, document_id: str):
return result.success(
DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).cancel(
@ -253,6 +304,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Cancel tasks in batches",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data.get('id_list'))
)
)
def put(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_cancel(request.data))
@ -271,6 +328,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Refresh document vector library",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, document_id: str):
return result.success(
DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).refresh(
@ -291,6 +354,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Batch refresh document vector library",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data.get('id_list'))
)
)
def put(self, request: Request, dataset_id: str):
return result.success(
DocumentSerializers.Batch(data={'dataset_id': dataset_id}).batch_refresh(request.data))
@ -313,6 +382,12 @@ class Document(APIView):
dynamic_tag=k.get('target_dataset_id')),
compare=CompareConstants.AND
)
@log(menu='document', operate="Migrate documents in batches",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data)
)
)
def put(self, request: Request, dataset_id: str, target_dataset_id: str):
return result.success(
DocumentSerializers.Migrate(
@ -332,6 +407,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Export document",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def get(self, request: Request, dataset_id: str, document_id: str):
return DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).export()
@ -346,6 +427,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Export Zip document",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def get(self, request: Request, dataset_id: str, document_id: str):
return DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).export_zip()
@ -377,6 +464,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Modify document",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, document_id: str):
return result.success(
DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id}).edit(
@ -392,6 +485,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Delete document",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def delete(self, request: Request, dataset_id: str, document_id: str):
operate = DocumentSerializers.Operate(data={'document_id': document_id, 'dataset_id': dataset_id})
operate.is_valid(raise_exception=True)
@ -456,6 +555,12 @@ class Document(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='document', operate="Batch generate related documents",
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object_batch(r.data.get('document_id_list'))
)
)
def put(self, request: Request, dataset_id: str):
return result.success(DocumentSerializers.BatchGenerateRelated(data={'dataset_id': dataset_id})
.batch_generate_related(request.data))

View File

@ -14,6 +14,7 @@ from rest_framework.views import APIView
from rest_framework.views import Request
from common.auth import TokenAuth
from common.log.log import log
from common.response import result
from dataset.serializers.file_serializers import FileSerializer
from django.utils.translation import gettext_lazy as _
@ -32,6 +33,7 @@ class FileView(APIView):
required=True,
description=_('Upload file'))],
tags=[_('file')])
@log(menu='file', operate='Upload file')
def post(self, request: Request):
return result.success(FileSerializer(data={'file': request.FILES.get('file')}).upload())

View File

@ -14,6 +14,7 @@ from rest_framework.views import APIView
from rest_framework.views import Request
from common.auth import TokenAuth
from common.log.log import log
from common.response import result
from dataset.serializers.image_serializers import ImageSerializer
from django.utils.translation import gettext_lazy as _

View File

@ -13,12 +13,16 @@ from rest_framework.views import Request
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import Permission, Group, Operate, CompareConstants
from common.log.log import log
from common.response import result
from common.util.common import query_params_to_single_dict
from dataset.serializers.common_serializers import BatchSerializer
from dataset.serializers.paragraph_serializers import ParagraphSerializers
from django.utils.translation import gettext_lazy as _
from dataset.views import get_dataset_document_operation_object, get_dataset_operation_object, \
get_document_operation_object
class Paragraph(APIView):
authentication_classes = [TokenAuth]
@ -50,6 +54,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Create Paragraph',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def post(self, request: Request, dataset_id: str, document_id: str):
return result.success(
ParagraphSerializers.Create(data={'dataset_id': dataset_id, 'document_id': document_id}).save(request.data))
@ -67,6 +77,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Add associated questions',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def post(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str):
return result.success(ParagraphSerializers.Problem(
data={"dataset_id": dataset_id, 'document_id': document_id, 'paragraph_id': paragraph_id}).save(
@ -99,6 +115,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Disassociation issue',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str, problem_id: str):
return result.success(ParagraphSerializers.Association(
data={'dataset_id': dataset_id, 'document_id': document_id, 'paragraph_id': paragraph_id,
@ -116,6 +138,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Related questions',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str, problem_id: str):
return result.success(ParagraphSerializers.Association(
data={'dataset_id': dataset_id, 'document_id': document_id, 'paragraph_id': paragraph_id,
@ -134,6 +162,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Modify paragraph data',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str):
o = ParagraphSerializers.Operate(
data={"paragraph_id": paragraph_id, 'dataset_id': dataset_id, 'document_id': document_id})
@ -164,6 +198,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Delete paragraph',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def delete(self, request: Request, dataset_id: str, document_id: str, paragraph_id: str):
o = ParagraphSerializers.Operate(
data={"dataset_id": dataset_id, 'document_id': document_id, "paragraph_id": paragraph_id})
@ -184,6 +224,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Delete paragraphs in batches',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def delete(self, request: Request, dataset_id: str, document_id: str):
return result.success(ParagraphSerializers.Batch(
data={"dataset_id": dataset_id, 'document_id': document_id}).batch_delete(request.data))
@ -206,6 +252,12 @@ class Paragraph(APIView):
dynamic_tag=k.get('target_dataset_id')),
compare=CompareConstants.AND
)
@log(menu='Paragraph', operate='Migrate paragraphs in batches',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, target_dataset_id: str, document_id: str, target_document_id):
return result.success(
ParagraphSerializers.Migrate(
@ -241,6 +293,12 @@ class Paragraph(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='Paragraph', operate='Batch generate related',
get_operation_object=lambda r, keywords: get_dataset_document_operation_object(
get_dataset_operation_object(keywords.get('dataset_id')),
get_document_operation_object(keywords.get('document_id'))
)
)
def put(self, request: Request, dataset_id: str, document_id: str):
return result.success(
ParagraphSerializers.BatchGenerateRelated(data={'dataset_id': dataset_id, 'document_id': document_id})

View File

@ -13,12 +13,15 @@ from rest_framework.views import Request
from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import Permission, Group, Operate
from common.log.log import log
from common.response import result
from common.util.common import query_params_to_single_dict
from dataset.serializers.problem_serializers import ProblemSerializers
from dataset.swagger_api.problem_api import ProblemApi
from django.utils.translation import gettext_lazy as _
from dataset.views import get_dataset_operation_object
class Problem(APIView):
authentication_classes = [TokenAuth]
@ -49,6 +52,9 @@ class Problem(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='problem', operate='Create question',
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id'))
)
def post(self, request: Request, dataset_id: str):
return result.success(
ProblemSerializers.Create(
@ -85,6 +91,8 @@ class Problem(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='problem', operate='Batch deletion issues',
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def delete(self, request: Request, dataset_id: str):
return result.success(
ProblemSerializers.BatchOperate(data={'dataset_id': dataset_id}).delete(request.data))
@ -99,6 +107,8 @@ class Problem(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='problem', operate='Batch associated paragraphs',
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def post(self, request: Request, dataset_id: str):
return result.success(
ProblemSerializers.BatchOperate(data={'dataset_id': dataset_id}).association(request.data))
@ -115,6 +125,8 @@ class Problem(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='problem', operate='Delete question',
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def delete(self, request: Request, dataset_id: str, problem_id: str):
return result.success(ProblemSerializers.Operate(
data={**query_params_to_single_dict(request.query_params), 'dataset_id': dataset_id,
@ -130,6 +142,8 @@ class Problem(APIView):
@has_permissions(
lambda r, k: Permission(group=Group.DATASET, operate=Operate.MANAGE,
dynamic_tag=k.get('dataset_id')))
@log(menu='problem', operate='Modify question',
get_operation_object=lambda r, keywords: get_dataset_operation_object(keywords.get('dataset_id')))
def put(self, request: Request, dataset_id: str, problem_id: str):
return result.success(ProblemSerializers.Operate(
data={**query_params_to_single_dict(request.query_params), 'dataset_id': dataset_id,

View File

@ -12,7 +12,9 @@ import uuid
from abc import ABC, abstractmethod
from typing import Dict, List
from django.db.models import QuerySet
import jieba
from django.contrib.postgres.search import SearchVector
from django.db.models import QuerySet, Value
from langchain_core.embeddings import Embeddings
from common.db.search import generate_sql_by_query_dict
@ -68,7 +70,8 @@ class PGVector(BaseVectorStore):
source_id=text_list[index].get('source_id'),
source_type=text_list[index].get('source_type'),
embedding=embeddings[index],
search_vector=to_ts_vector(text_list[index]['text'])) for index in
search_vector=SearchVector(Value(to_ts_vector(text_list[index]['text'])))) for
index in
range(0, len(texts))]
if not is_the_task_interrupted():
QuerySet(Embedding).bulk_create(embedding_list) if len(embedding_list) > 0 else None

View File

@ -0,0 +1,194 @@
# Generated by Django 4.2.15 on 2025-03-13 07:21
from django.db import migrations, models
function_template = '''
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-10 06:20:35.945414 +00:00', '2025-03-10 09:19:23.608026 +00:00', 'c75cb48e-fd77-11ef-84d2-5618c4394482', '博查搜索', '从博查搜索任何信息和网页URL', e'def bocha_search(query, apikey):
import requests
import json
url = "https://api.bochaai.com/v1/web-search"
payload = json.dumps({
"query": query,
"Boolean": "true",
"count": 8
})
headers = {
"Authorization": "Bearer " + apikey, #鉴权参数示例Bearer xxxxxxAPI KEY请先前往博查AI开放平台https://open.bochaai.com> API KEY 管理中获取。
"Content-Type": "application/json"
}
response = requests.request("POST", url, headers=headers, data=payload)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/ui/fx/bochaai/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "API Key", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "API Key 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "API Key 长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-02-26 03:36:48.187286 +00:00', '2025-03-11 07:23:46.123972 +00:00', 'e89ad2ae-f3f2-11ef-ad09-0242ac110002', 'Google Search', 'Google Web Search', e'def google_search(query, apikey, cx):
import requests
import json
url = "https://customsearch.googleapis.com/customsearch/v1"
params = {
"q": query,
"key": apikey,
"cx": cx,
"num": 10, # 每次最多返回10条
}
response = requests.get(url, params=params)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/ui/fx/google_search/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "API Key", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "API Key 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "API Key 长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "cx", "label": "cx", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "cx 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "cx长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-02-25 07:44:40.141515 +00:00', '2025-03-11 06:33:53.248495 +00:00', '5e912f00-f34c-11ef-8a9c-5618c4394482', 'LangSearch', e'A Web Search tool supporting natural language search
', e'
def langsearch(query, apikey):
import json
import requests
url = "https://api.langsearch.com/v1/web-search"
payload = json.dumps({
"query": query,
"summary": True,
"freshness": "noLimit",
"livecrawl": True,
"count": 20
})
headers = {
"Authorization": apikey,
"Content-Type": "application/json"
}
# key从官网申请 https://langsearch.com/
response = requests.request("POST", url, headers=headers, data=payload)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"API请求失败: {response.status_code}, 错误信息: {response.text}")
return (response.text)', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', TRUE, 'PUBLIC', 'INTERNAL', '/ui/fx/langsearch/icon.png', '[{"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "apikey", "label": "API Key", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "API Key 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "API Key 长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', '', NULL);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 08:16:32.626245 +00:00', '2025-03-17 08:16:32.626308 +00:00', '22c21b76-0308-11f0-9694-5618c4394482', 'MySQL 查询', '一个连接MySQL数据库执行SQL查询的工具', e'
def query_mysql(host,port, user, password, database, sql):
import pymysql
import json
from pymysql.cursors import DictCursor
try:
# 创建连接
db = pymysql.connect(
host=host,
port=int(port),
user=user,
password=password,
database=database,
cursorclass=DictCursor # 使用字典游标
)
# 使用 cursor() 方法创建一个游标对象 cursor
cursor = db.cursor()
# 使用 execute() 方法执行 SQL 查询
cursor.execute(sql)
# 使用 fetchall() 方法获取所有数据
data = cursor.fetchall()
# 处理 bytes 类型的数据
for row in data:
for key, value in row.items():
if isinstance(value, bytes):
row[key] = value.decode("utf-8") # 转换为字符串
# 将数据序列化为 JSON
json_data = json.dumps(data, ensure_ascii=False)
return json_data
# 关闭数据库连接
db.close()
except Exception as e:
print(f"Error while connecting to MySQL: {e}")
raise e', '{"{\\"name\\": \\"sql\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/ui/fx/mysql/icon.png', '[{"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "host", "label": "host", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "host 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "host长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 20, "minlength": 1, "show-word-limit": true}, "field": "port", "label": "port", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "port 为必填属性", "required": true}, {"max": 20, "min": 1, "message": "port长度在 1 到 20 个字符", "trigger": "blur"}]}, "default_value": "3306", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "user", "label": "user", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "user 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "user长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "root", "show_default_value": false}, {"attrs": {"type": "password", "maxlength": 200, "minlength": 1, "show-password": true, "show-word-limit": true}, "field": "password", "label": "password", "required": true, "input_type": "PasswordInput", "props_info": {"rules": [{"message": "password 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "password长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}, {"attrs": {"maxlength": 200, "minlength": 1, "show-word-limit": true}, "field": "database", "label": "database", "required": true, "input_type": "TextInput", "props_info": {"rules": [{"message": "database 为必填属性", "required": true}, {"max": 200, "min": 1, "message": "database长度在 1 到 200 个字符", "trigger": "blur"}]}, "default_value": "x", "show_default_value": false}]', null, null);
INSERT INTO function_lib (create_time, update_time, id, name, "desc", code, input_field_list, user_id, is_active, permission_type, function_type, icon, init_field_list, init_params, template_id) VALUES ('2025-03-17 07:37:54.620836 +00:00', '2025-03-17 07:37:54.620887 +00:00', 'bd1e8b88-0302-11f0-87bb-5618c4394482', 'PostgreSQL 查询', '一个连接PostgreSQL数据库执行SQL查询的工具', e'
def queryPgSQL(database, user, password, host, port, query):
import psycopg2
import json
from datetime import datetime
# 自定义 JSON 序列化函数
def default_serializer(obj):
if isinstance(obj, datetime):
return obj.isoformat() # 将 datetime 转换为 ISO 格式字符串
raise TypeError(f"Type {type(obj)} not serializable")
# 数据库连接信息
conn_params = {
"dbname": database,
"user": user,
"password": password,
"host": host,
"port": port
}
try:
# 建立连接
conn = psycopg2.connect(**conn_params)
print("连接成功!")
# 创建游标对象
cursor = conn.cursor()
# 执行查询语句
cursor.execute(query)
# 获取查询结果
rows = cursor.fetchall()
# 处理 bytes 类型的数据
columns = [desc[0] for desc in cursor.description]
result = [dict(zip(columns, row)) for row in rows]
# 转换为 JSON 格式
json_result = json.dumps(result, default=default_serializer, ensure_ascii=False)
return json_result
except Exception as e:
print(f"发生错误:{e}")
raise e
finally:
# 关闭游标和连接
if cursor:
cursor.close()
if conn:
conn.close()', '{"{\\"name\\": \\"query\\", \\"type\\": \\"string\\", \\"source\\": \\"reference\\", \\"is_required\\": true}"}', 'f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', true, 'PUBLIC', 'INTERNAL', '/ui/fx/postgresql/icon.png', '[{"attrs":{"maxlength":200,"minlength":1,"show-word-limit":true},"field":"host","label":"host","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"host 为必填属性","required":true},{"max":200,"min":1,"message":"host长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"x","show_default_value":false},{"attrs":{"maxlength":20,"minlength":1,"show-word-limit":true},"field":"port","label":"port","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"port 为必填属性","required":true},{"max":20,"min":1,"message":"port长度在 1 到 20 个字符","trigger":"blur"}]},"default_value":"5432","show_default_value":false},{"attrs":{"maxlength":200,"minlength":1,"show-word-limit":true},"field":"user","label":"user","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"user 为必填属性","required":true},{"max":200,"min":1,"message":"user长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"root","show_default_value":false},{"attrs":{"type":"password","maxlength":200,"minlength":1,"show-password":true,"show-word-limit":true},"field":"password","label":"password","required":true,"input_type":"PasswordInput","props_info":{"rules":[{"message":"password 为必填属性","required":true},{"max":200,"min":1,"message":"password长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"x","show_default_value":false},{"attrs":{"maxlength":200,"minlength":1,"show-word-limit":true},"field":"database","label":"database","required":true,"input_type":"TextInput","props_info":{"rules":[{"message":"database 为必填属性","required":true},{"max":200,"min":1,"message":"database长度在 1 到 200 个字符","trigger":"blur"}]},"default_value":"x","show_default_value":false}]', null, null);
'''
class Migration(migrations.Migration):
dependencies = [
('function_lib', '0002_functionlib_is_active_functionlib_permission_type'),
]
operations = [
migrations.AddField(
model_name='functionlib',
name='function_type',
field=models.CharField(choices=[('INTERNAL', '内置'), ('PUBLIC', '公开')],
default='PUBLIC', max_length=20, verbose_name='函数类型'),
),
migrations.AddField(
model_name='functionlib',
name='icon',
field=models.CharField(default='/ui/favicon.ico', max_length=256,
verbose_name='函数库icon'),
),
migrations.AddField(
model_name='functionlib',
name='init_field_list',
field=models.JSONField(default=list, verbose_name='启动字段列表'),
),
migrations.AddField(
model_name='functionlib',
name='init_params',
field=models.CharField(max_length=102400, null=True, verbose_name='初始化参数'),
),
migrations.AddField(
model_name='functionlib',
name='template_id',
field=models.UUIDField(default=None, null=True, verbose_name='模版id'),
),
migrations.RunSQL(function_template)
]

View File

@ -19,6 +19,10 @@ class PermissionType(models.TextChoices):
PUBLIC = "PUBLIC", '公开'
PRIVATE = "PRIVATE", "私有"
class FunctionType(models.TextChoices):
INTERNAL = "INTERNAL", '内置'
PUBLIC = "PUBLIC", "公开"
class FunctionLib(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id")
@ -29,9 +33,15 @@ class FunctionLib(AppModelMixin):
input_field_list = ArrayField(verbose_name="输入字段列表",
base_field=models.JSONField(verbose_name="输入字段", default=dict)
, default=list)
init_field_list = models.JSONField(verbose_name="启动字段列表", default=list)
icon = models.CharField(max_length=256, verbose_name="函数库icon", default="/ui/favicon.ico")
is_active = models.BooleanField(default=True)
permission_type = models.CharField(max_length=20, verbose_name='权限类型', choices=PermissionType.choices,
default=PermissionType.PRIVATE)
function_type = models.CharField(max_length=20, verbose_name='函数类型', choices=FunctionType.choices,
default=FunctionType.PUBLIC)
template_id = models.UUIDField(max_length=128, verbose_name="模版id", null=True, default=None)
init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True)
class Meta:
db_table = "function_lib"

View File

@ -0,0 +1,8 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py.py
@date2024/8/2 14:55
@desc:
"""

View File

@ -6,27 +6,30 @@
@date2024/8/2 17:35
@desc:
"""
import io
import json
import pickle
import re
import uuid
from typing import List
from django.core import validators
from django.db import transaction
from django.db.models import QuerySet, Q
from django.db.models import QuerySet, Q, OuterRef, Exists
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers, status
from common.db.search import page_search
from common.exception.app_exception import AppApiException
from common.field.common import UploadedFileField
from common.field.common import UploadedFileField, UploadedImageField
from common.response import result
from common.util.common import restricted_loads
from common.util.field_message import ErrMessage
from common.util.function_code import FunctionExecutor
from function_lib.models.function import FunctionLib
from common.util.rsa_util import rsa_long_decrypt, rsa_long_encrypt
from dataset.models import File
from function_lib.models.function import FunctionLib, PermissionType, FunctionType
from smartdoc.const import CONFIG
from django.utils.translation import gettext_lazy as _
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
@ -35,11 +38,37 @@ class FlibInstance:
self.function_lib = function_lib
self.version = version
def encryption(message: str):
"""
加密敏感字段数据 加密方式是 如果密码是 1234567890 那么给前端则是 123******890
:param message:
:return:
"""
if type(message) != str:
return message
if message == "":
return ""
max_pre_len = 8
max_post_len = 4
message_len = len(message)
pre_len = int(message_len / 5 * 2)
post_len = int(message_len / 5 * 1)
pre_str = "".join([message[index] for index in
range(0,
max_pre_len if pre_len > max_pre_len else 1 if pre_len <= 0 else int(
pre_len))])
end_str = "".join(
[message[index] for index in
range(message_len - (int(post_len) if pre_len < max_post_len else max_post_len),
message_len)])
content = "***************"
return pre_str + content + end_str
class FunctionLibModelSerializer(serializers.ModelSerializer):
class Meta:
model = FunctionLib
fields = ['id', 'name', 'desc', 'code', 'input_field_list', 'permission_type', 'is_active', 'user_id',
fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list','init_field_list', 'init_params', 'permission_type', 'is_active', 'user_id', 'template_id',
'create_time', 'update_time']
@ -65,6 +94,8 @@ class DebugField(serializers.Serializer):
class DebugInstance(serializers.Serializer):
debug_field_list = DebugField(required=True, many=True)
input_field_list = FunctionLibInputField(required=True, many=True)
init_field_list = serializers.ListField(required=False, default=list)
init_params = serializers.JSONField(required=False, default=dict)
code = serializers.CharField(required=True, error_messages=ErrMessage.char(_('function content')))
@ -80,6 +111,8 @@ class EditFunctionLib(serializers.Serializer):
input_field_list = FunctionLibInputField(required=False, many=True)
init_field_list = serializers.ListField(required=False, default=list)
is_active = serializers.BooleanField(required=False, error_messages=ErrMessage.char(_('Is active')))
@ -93,6 +126,8 @@ class CreateFunctionLib(serializers.Serializer):
input_field_list = FunctionLibInputField(required=True, many=True)
init_field_list = serializers.ListField(required=False, default=list)
permission_type = serializers.CharField(required=True, error_messages=ErrMessage.char(_('permission')), validators=[
validators.RegexValidator(regex=re.compile("^PUBLIC|PRIVATE$"),
message="权限只支持PUBLIC|PRIVATE", code=500)
@ -111,6 +146,8 @@ class FunctionLibSerializer(serializers.Serializer):
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id')))
select_user_id = serializers.CharField(required=False, allow_null=True, allow_blank=True)
function_type = serializers.CharField(required=False, allow_null=True, allow_blank=True)
def get_query_set(self):
query_set = QuerySet(FunctionLib).filter(
@ -123,19 +160,33 @@ class FunctionLibSerializer(serializers.Serializer):
query_set = query_set.filter(is_active=self.data.get('is_active'))
if self.data.get('select_user_id') is not None:
query_set = query_set.filter(user_id=self.data.get('select_user_id'))
if self.data.get('function_type') is not None:
query_set = query_set.filter(function_type=self.data.get('function_type'))
query_set = query_set.order_by("-create_time")
return query_set
def list(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
return [FunctionLibModelSerializer(item).data for item in self.get_query_set()]
rs = []
for item in self.get_query_set():
data = {**FunctionLibModelSerializer(item).data, 'init_params': None}
rs.append(data)
return rs
def page(self, current_page: int, page_size: int, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
def post_records_handler(row):
return {
**FunctionLibModelSerializer(row).data,
'init_params': None
}
return page_search(current_page, page_size, self.get_query_set(),
post_records_handler=lambda row: FunctionLibModelSerializer(row).data)
post_records_handler=post_records_handler)
class Create(serializers.Serializer):
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id')))
@ -148,8 +199,9 @@ class FunctionLibSerializer(serializers.Serializer):
code=instance.get('code'),
user_id=self.data.get('user_id'),
input_field_list=instance.get('input_field_list'),
init_field_list=instance.get('init_field_list'),
permission_type=instance.get('permission_type'),
is_active=instance.get('is_active', True))
is_active=False)
function_lib.save()
return FunctionLibModelSerializer(function_lib).data
@ -163,13 +215,19 @@ class FunctionLibSerializer(serializers.Serializer):
input_field_list = debug_instance.get('input_field_list')
code = debug_instance.get('code')
debug_field_list = debug_instance.get('debug_field_list')
init_params = debug_instance.get('init_params')
params = {field.get('name'): self.convert_value(field.get('name'), field.get('value'), field.get('type'),
field.get('is_required'))
for field in
[{'value': self.get_field_value(debug_field_list, field.get('name'), field.get('is_required')),
**field} for field in
input_field_list]}
return function_executor.exec_code(code, params)
# 合并初始化参数
if init_params is not None:
all_params = init_params | params
else:
all_params = params
return function_executor.exec_code(code, all_params)
@staticmethod
def get_field_value(debug_field_list, name, is_required):
@ -217,6 +275,9 @@ class FunctionLibSerializer(serializers.Serializer):
def delete(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
fun = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
if fun.template_id is None and fun.icon != '/ui/favicon.ico':
QuerySet(File).filter(id=fun.icon.split('/')[-1]).delete()
QuerySet(FunctionLib).filter(id=self.data.get('id')).delete()
return True
@ -224,9 +285,25 @@ class FunctionLibSerializer(serializers.Serializer):
if with_valid:
self.is_valid(raise_exception=True)
EditFunctionLib(data=instance).is_valid(raise_exception=True)
edit_field_list = ['name', 'desc', 'code', 'input_field_list', 'permission_type', 'is_active']
edit_field_list = ['name', 'desc', 'code', 'icon', 'input_field_list', 'init_field_list', 'init_params', 'permission_type', 'is_active']
edit_dict = {field: instance.get(field) for field in edit_field_list if (
field in instance and instance.get(field) is not None)}
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
if 'init_params' in edit_dict:
if edit_dict['init_field_list'] is not None:
rm_key = []
for key in edit_dict['init_params']:
if key not in [field['field'] for field in edit_dict['init_field_list']]:
rm_key.append(key)
for key in rm_key:
edit_dict['init_params'].pop(key)
if function_lib.init_params:
old_init_params = json.loads(rsa_long_decrypt(function_lib.init_params))
for key in edit_dict['init_params']:
if key in old_init_params and edit_dict['init_params'][key] == encryption(old_init_params[key]):
edit_dict['init_params'][key] = old_init_params[key]
edit_dict['init_params'] = rsa_long_encrypt(json.dumps(edit_dict['init_params']))
QuerySet(FunctionLib).filter(id=self.data.get('id')).update(**edit_dict)
return self.one(False)
@ -237,7 +314,15 @@ class FunctionLibSerializer(serializers.Serializer):
Q(user_id=self.data.get('user_id')) | Q(permission_type='PUBLIC')).exists():
raise AppApiException(500, _('Function does not exist'))
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
return FunctionLibModelSerializer(function_lib).data
if function_lib.init_params:
function_lib.init_params = json.loads(rsa_long_decrypt(function_lib.init_params))
if function_lib.init_field_list:
password_fields = [i["field"] for i in function_lib.init_field_list if i.get("input_type") == "PasswordInput"]
if function_lib.init_params:
for k in function_lib.init_params:
if k in password_fields and function_lib.init_params[k]:
function_lib.init_params[k] = encryption(function_lib.init_params[k])
return {**FunctionLibModelSerializer(function_lib).data, 'init_params': function_lib.init_params}
def export(self, with_valid=True):
try:
@ -265,7 +350,7 @@ class FunctionLibSerializer(serializers.Serializer):
user_id = self.data.get('user_id')
flib_instance_bytes = self.data.get('file').read()
try:
flib_instance = pickle.loads(flib_instance_bytes)
flib_instance = restricted_loads(flib_instance_bytes)
except Exception as e:
raise AppApiException(1001, _("Unsupported file format"))
function_lib = flib_instance.function_lib
@ -274,7 +359,68 @@ class FunctionLibSerializer(serializers.Serializer):
code=function_lib.get('code'),
user_id=user_id,
input_field_list=function_lib.get('input_field_list'),
init_field_list=function_lib.get('init_field_list', []),
permission_type='PRIVATE',
is_active=function_lib.get('is_active'))
is_active=False)
function_lib_model.save()
return True
return True
class IconOperate(serializers.Serializer):
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID")))
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
image = UploadedImageField(required=True, error_messages=ErrMessage.image(_("picture")))
def edit(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
functionLib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
if functionLib is None:
raise AppApiException(500, _('Function does not exist'))
# 删除旧的图片
if functionLib.icon != '/ui/favicon.ico':
QuerySet(File).filter(id=functionLib.icon.split('/')[-1]).delete()
if self.data.get('image') is None:
functionLib.icon = '/ui/favicon.ico'
else:
meta = {
'debug': False
}
file_id = uuid.uuid1()
file = File(id=file_id, file_name=self.data.get('image').name, meta=meta)
file.save(self.data.get('image').read())
functionLib.icon = f'/api/file/{file_id}'
functionLib.save()
return functionLib.icon
class InternalFunction(serializers.Serializer):
id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("function ID")))
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_("function name")))
def add(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
internal_function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
if internal_function_lib is None:
raise AppApiException(500, _('Function does not exist'))
function_lib = FunctionLib(
id=uuid.uuid1(),
name=self.data.get('name'),
desc=internal_function_lib.desc,
code=internal_function_lib.code,
user_id=self.data.get('user_id'),
input_field_list=internal_function_lib.input_field_list,
init_field_list=internal_function_lib.init_field_list,
permission_type=PermissionType.PRIVATE,
template_id=internal_function_lib.id,
function_type=FunctionType.PUBLIC,
icon=internal_function_lib.icon,
is_active=False
)
function_lib.save()
return FunctionLibModelSerializer(function_lib).data

View File

@ -0,0 +1,8 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file __init__.py.py
@date2024/8/2 14:55
@desc:
"""

View File

@ -195,6 +195,53 @@ class FunctionLibApi(ApiMixin):
}
)
@staticmethod
def get_response_body_api():
return openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['id', 'name', 'code', 'input_field_list', 'permission_type'],
properties={
'id': openapi.Schema(type=openapi.TYPE_STRING, title="", description=_('ID')),
'name': openapi.Schema(type=openapi.TYPE_STRING, title=_('function name'),
description=_('function name')),
'desc': openapi.Schema(type=openapi.TYPE_STRING, title=_('function description'),
description=_('function description')),
'code': openapi.Schema(type=openapi.TYPE_STRING, title=_('function content'),
description=_('function content')),
'permission_type': openapi.Schema(type=openapi.TYPE_STRING, title=_('permission'),
description=_('permission')),
'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN, title=_('Is active'),
description=_('Is active')),
'input_field_list': openapi.Schema(type=openapi.TYPE_ARRAY,
description=_('Input variable list'),
items=openapi.Schema(type=openapi.TYPE_OBJECT,
required=['name', 'is_required', 'source'],
properties={
'name': openapi.Schema(
type=openapi.TYPE_STRING,
title=_('variable name'),
description=_('variable name')),
'is_required': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
title=_('required'),
description=_('required')),
'type': openapi.Schema(
type=openapi.TYPE_STRING,
title=_('type'),
description=_(
'Field type string|int|dict|array|float')
),
'source': openapi.Schema(
type=openapi.TYPE_STRING,
title=_('source'),
description=_(
'The source only supports custom|reference')),
}))
}
)
class Export(ApiMixin):
@staticmethod
def get_request_params_api():
@ -214,4 +261,4 @@ class FunctionLibApi(ApiMixin):
type=openapi.TYPE_FILE,
required=True,
description=_('Upload image files'))
]
]

View File

@ -8,6 +8,8 @@ urlpatterns = [
path('function_lib/debug', views.FunctionLibView.Debug.as_view()),
path('function_lib/<str:id>/export', views.FunctionLibView.Export.as_view()),
path('function_lib/import', views.FunctionLibView.Import.as_view()),
path('function_lib/<str:id>/edit_icon', views.FunctionLibView.EditIcon.as_view()),
path('function_lib/<str:id>/add_internal_fun', views.FunctionLibView.AddInternalFun.as_view()),
path('function_lib/pylint', views.PyLintView.as_view()),
path('function_lib/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()),
path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(),

View File

@ -0,0 +1,20 @@
# coding=utf-8
"""
@project: MaxKB
@Author
@file common.py
@date2025/3/25 17:27
@desc:
"""
from django.db.models import QuerySet
from function_lib.models.function import FunctionLib
def get_function_lib_operation_object(function_lib_id):
function_lib_model = QuerySet(model=FunctionLib).filter(id=function_lib_id).first()
if function_lib_model is not None:
return {
"name": function_lib_model.name
}
return {}

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