@lobehub/lobehub 2.0.0-next.98 → 2.0.0-next.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/apps/desktop/src/main/services/__tests__/fileSrv.test.ts +603 -0
- package/changelog/v1.json +9 -0
- package/codecov.yml +1 -0
- package/locales/ar/plugin.json +34 -22
- package/locales/ar/tool.json +8 -0
- package/locales/bg-BG/plugin.json +34 -22
- package/locales/bg-BG/tool.json +8 -0
- package/locales/de-DE/plugin.json +34 -22
- package/locales/de-DE/tool.json +8 -0
- package/locales/en-US/plugin.json +34 -22
- package/locales/en-US/tool.json +8 -0
- package/locales/es-ES/plugin.json +34 -22
- package/locales/es-ES/tool.json +8 -0
- package/locales/fa-IR/plugin.json +34 -22
- package/locales/fa-IR/tool.json +8 -0
- package/locales/fr-FR/plugin.json +34 -22
- package/locales/fr-FR/tool.json +8 -0
- package/locales/it-IT/plugin.json +34 -22
- package/locales/it-IT/tool.json +8 -0
- package/locales/ja-JP/plugin.json +34 -22
- package/locales/ja-JP/tool.json +8 -0
- package/locales/ko-KR/plugin.json +34 -22
- package/locales/ko-KR/tool.json +8 -0
- package/locales/nl-NL/plugin.json +34 -22
- package/locales/nl-NL/tool.json +8 -0
- package/locales/pl-PL/plugin.json +34 -22
- package/locales/pl-PL/tool.json +8 -0
- package/locales/pt-BR/plugin.json +34 -22
- package/locales/pt-BR/tool.json +8 -0
- package/locales/ru-RU/plugin.json +34 -22
- package/locales/ru-RU/tool.json +8 -0
- package/locales/tr-TR/plugin.json +34 -22
- package/locales/tr-TR/tool.json +8 -0
- package/locales/vi-VN/plugin.json +34 -22
- package/locales/vi-VN/tool.json +8 -0
- package/locales/zh-CN/plugin.json +34 -22
- package/locales/zh-CN/tool.json +8 -0
- package/locales/zh-TW/plugin.json +34 -22
- package/locales/zh-TW/tool.json +8 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/document.test.ts +149 -0
- package/packages/database/src/models/chunk.ts +3 -1
- package/packages/database/src/models/document.ts +8 -2
- package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatFileContents.test.ts.snap +75 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatNoSearchResults.test.ts.snap +45 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatSearchResults.test.ts.snap +82 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/formatFileContents.test.ts +118 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/formatFileContents.ts +31 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/formatNoSearchResults.test.ts +25 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/formatNoSearchResults.ts +13 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/formatSearchResults.test.ts +191 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/formatSearchResults.ts +50 -0
- package/packages/prompts/src/prompts/knowledgeBaseQA/index.ts +6 -0
- package/packages/types/src/rag.ts +13 -4
- package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
- package/src/features/ChatInput/ActionBar/Token/TokenTagForGroupChat.tsx +2 -2
- package/src/features/ChatList/Messages/Group/Tool/Inspector/ToolTitle.tsx +5 -23
- package/src/features/ChatList/Messages/Tool/Inspector/ToolTitle.tsx +5 -25
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
- package/src/helpers/toolEngineering/index.test.ts +3 -3
- package/src/helpers/toolEngineering/index.ts +17 -4
- package/src/libs/trpc/client/lambda.ts +0 -6
- package/src/locales/default/plugin.ts +34 -22
- package/src/locales/default/tool.ts +13 -5
- package/src/server/routers/lambda/chunk.ts +168 -41
- package/src/services/chat/chat.test.ts +3 -3
- package/src/services/chat/index.ts +2 -2
- package/src/services/rag.ts +6 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +11 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +0 -87
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +2 -69
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/rag.ts +0 -47
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +8 -69
- package/src/store/chat/slices/builtinTool/actions/index.ts +4 -1
- package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +174 -0
- package/src/store/chat/slices/operation/types.ts +1 -0
- package/src/store/chat/slices/thread/action.test.ts +0 -1
- package/src/store/chat/slices/thread/action.ts +0 -1
- package/src/tools/executionRuntimes.ts +3 -0
- package/src/tools/identifiers.ts +13 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/knowledge-base/ExecutionRuntime/index.ts +96 -0
- package/src/tools/knowledge-base/Render/ReadKnowledge/FileCard.tsx +135 -0
- package/src/tools/knowledge-base/Render/ReadKnowledge/index.tsx +27 -0
- package/src/tools/knowledge-base/Render/SearchKnowledgeBase/Item/index.tsx +54 -0
- package/src/tools/knowledge-base/Render/SearchKnowledgeBase/Item/style.ts +51 -0
- package/src/tools/knowledge-base/Render/SearchKnowledgeBase/index.tsx +23 -0
- package/src/tools/knowledge-base/Render/index.ts +7 -0
- package/src/tools/knowledge-base/index.ts +64 -0
- package/src/tools/knowledge-base/systemRole.ts +102 -0
- package/src/tools/knowledge-base/type.ts +25 -0
- package/src/tools/local-system/Intervention/WriteFile/index.tsx +1 -1
- package/src/tools/renders.ts +4 -0
- package/src/store/chat/agents/createToolEngine.ts +0 -22
|
@@ -2,15 +2,14 @@ import { Icon } from '@lobehub/ui';
|
|
|
2
2
|
import { createStyles } from 'antd-style';
|
|
3
3
|
import isEqual from 'fast-deep-equal';
|
|
4
4
|
import { ChevronRight } from 'lucide-react';
|
|
5
|
-
import { memo
|
|
5
|
+
import { memo } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
|
8
8
|
|
|
9
9
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
10
10
|
import { toolSelectors } from '@/store/tool/selectors';
|
|
11
11
|
import { shinyTextStylish } from '@/styles/loading';
|
|
12
|
-
import {
|
|
13
|
-
import { WebBrowsingManifest } from '@/tools/web-browsing';
|
|
12
|
+
import { builtinToolIdentifiers } from '@/tools/identifiers';
|
|
14
13
|
|
|
15
14
|
import BuiltinPluginTitle from './BuiltinPluginTitle';
|
|
16
15
|
|
|
@@ -49,33 +48,16 @@ const ToolTitle = memo<ToolTitleProps>(
|
|
|
49
48
|
|
|
50
49
|
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
() => [
|
|
54
|
-
{
|
|
55
|
-
apiName: t(`search.apiName.${apiName}`, apiName),
|
|
56
|
-
id: WebBrowsingManifest.identifier,
|
|
57
|
-
title: t('search.title'),
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
apiName: t(`localSystem.apiName.${apiName}`, apiName),
|
|
61
|
-
id: LocalSystemManifest.identifier,
|
|
62
|
-
title: t('localSystem.title'),
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
[],
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const builtinPluginTitle = plugins.find((item) => item.id === identifier);
|
|
69
|
-
|
|
70
|
-
if (!!builtinPluginTitle) {
|
|
51
|
+
if (builtinToolIdentifiers.includes(identifier)) {
|
|
71
52
|
return (
|
|
72
53
|
<BuiltinPluginTitle
|
|
73
|
-
{
|
|
54
|
+
apiName={t(`builtins.${identifier}.apiName.${apiName}`, apiName)}
|
|
74
55
|
identifier={identifier}
|
|
75
56
|
index={index}
|
|
76
57
|
isExpanded={isExpanded}
|
|
77
58
|
isLoading={isLoading}
|
|
78
59
|
messageId={messageId}
|
|
60
|
+
title={t(`builtins.${identifier}.title`, identifier)}
|
|
79
61
|
toolCallId={toolCallId}
|
|
80
62
|
/>
|
|
81
63
|
);
|
|
@@ -2,15 +2,14 @@ import { Icon } from '@lobehub/ui';
|
|
|
2
2
|
import { createStyles } from 'antd-style';
|
|
3
3
|
import isEqual from 'fast-deep-equal';
|
|
4
4
|
import { ChevronRight } from 'lucide-react';
|
|
5
|
-
import { memo
|
|
5
|
+
import { memo } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
|
8
8
|
|
|
9
9
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
10
10
|
import { toolSelectors } from '@/store/tool/selectors';
|
|
11
11
|
import { shinyTextStylish } from '@/styles/loading';
|
|
12
|
-
import {
|
|
13
|
-
import { WebBrowsingManifest } from '@/tools/web-browsing';
|
|
12
|
+
import { builtinToolIdentifiers } from '@/tools/identifiers';
|
|
14
13
|
|
|
15
14
|
import BuiltinPluginTitle from './BuiltinPluginTitle';
|
|
16
15
|
|
|
@@ -43,33 +42,14 @@ const ToolTitle = memo<ToolTitleProps>(({ identifier, messageId, index, apiName,
|
|
|
43
42
|
|
|
44
43
|
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
() => [
|
|
48
|
-
{
|
|
49
|
-
apiName: t(`search.apiName.${apiName}`, apiName),
|
|
50
|
-
// icon: <Icon icon={Globe} size={13} />,
|
|
51
|
-
id: WebBrowsingManifest.identifier,
|
|
52
|
-
title: t('search.title'),
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
apiName: t(`localSystem.apiName.${apiName}`, apiName),
|
|
56
|
-
// icon: <Icon icon={Laptop} size={13} />,
|
|
57
|
-
id: LocalSystemManifest.identifier,
|
|
58
|
-
title: t('localSystem.title'),
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
[],
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const builtinPluginTitle = plugins.find((item) => item.id === identifier);
|
|
65
|
-
|
|
66
|
-
if (!!builtinPluginTitle) {
|
|
45
|
+
if (builtinToolIdentifiers.includes(identifier)) {
|
|
67
46
|
return (
|
|
68
47
|
<BuiltinPluginTitle
|
|
69
|
-
{
|
|
48
|
+
apiName={t(`builtins.${identifier}.apiName.${apiName}`, apiName)}
|
|
70
49
|
identifier={identifier}
|
|
71
50
|
index={index}
|
|
72
51
|
messageId={messageId}
|
|
52
|
+
title={t(`builtins.${identifier}.title`, identifier)}
|
|
73
53
|
toolCallId={toolCallId}
|
|
74
54
|
/>
|
|
75
55
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { createAgentToolsEngine, createToolsEngine, getEnabledTools } from './index';
|
|
5
5
|
|
|
6
6
|
// Mock the store and helper dependencies
|
|
7
7
|
vi.mock('@/store/tool', () => ({
|
|
@@ -137,7 +137,7 @@ describe('toolEngineering', () => {
|
|
|
137
137
|
|
|
138
138
|
describe('createChatToolsEngine', () => {
|
|
139
139
|
it('should include web browsing tool as default when no tools are provided', () => {
|
|
140
|
-
const toolsEngine =
|
|
140
|
+
const toolsEngine = createAgentToolsEngine({
|
|
141
141
|
model: 'gpt-4',
|
|
142
142
|
provider: 'openai',
|
|
143
143
|
});
|
|
@@ -152,7 +152,7 @@ describe('toolEngineering', () => {
|
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
it('should include web browsing tool alongside user-provided tools', () => {
|
|
155
|
-
const toolsEngine =
|
|
155
|
+
const toolsEngine = createAgentToolsEngine({
|
|
156
156
|
model: 'gpt-4',
|
|
157
157
|
provider: 'openai',
|
|
158
158
|
});
|
|
@@ -13,6 +13,9 @@ import { WebBrowsingManifest } from '@/tools/web-browsing';
|
|
|
13
13
|
import { getSearchConfig } from '../getSearchConfig';
|
|
14
14
|
import { isCanUseFC } from '../isCanUseFC';
|
|
15
15
|
import { shouldEnableTool } from '../toolFilters';
|
|
16
|
+
import { KnowledgeBaseManifest } from '@/tools/knowledge-base';
|
|
17
|
+
import { getAgentStoreState } from '@/store/agent';
|
|
18
|
+
import { agentSelectors } from '@/store/agent/slices/chat';
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Tools engine configuration options
|
|
@@ -53,23 +56,33 @@ export const createToolsEngine = (config: ToolsEngineConfig = {}): ToolsEngine =
|
|
|
53
56
|
});
|
|
54
57
|
};
|
|
55
58
|
|
|
56
|
-
export const
|
|
59
|
+
export const createAgentToolsEngine = (workingModel: WorkingModel) =>
|
|
57
60
|
createToolsEngine({
|
|
58
|
-
// Add
|
|
59
|
-
defaultToolIds: [
|
|
61
|
+
// Add default tools based on configuration
|
|
62
|
+
defaultToolIds: [
|
|
63
|
+
WebBrowsingManifest.identifier,
|
|
64
|
+
// Only add KnowledgeBase tool if knowledge is enabled
|
|
65
|
+
KnowledgeBaseManifest.identifier,
|
|
66
|
+
],
|
|
60
67
|
// Create search-aware enableChecker for this request
|
|
61
68
|
enableChecker: ({ pluginId }) => {
|
|
62
69
|
// Check platform-specific constraints (e.g., LocalSystem desktop-only)
|
|
63
70
|
if (!shouldEnableTool(pluginId)) {
|
|
64
71
|
return false;
|
|
65
72
|
}
|
|
66
|
-
|
|
67
73
|
// For WebBrowsingManifest, apply search logic
|
|
68
74
|
if (pluginId === WebBrowsingManifest.identifier) {
|
|
69
75
|
const searchConfig = getSearchConfig(workingModel.model, workingModel.provider);
|
|
70
76
|
return searchConfig.useApplicationBuiltinSearchTool;
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
// For KnowledgeBaseManifest, only enable if knowledge is enabled
|
|
80
|
+
if (pluginId === KnowledgeBaseManifest.identifier) {
|
|
81
|
+
const agentState = getAgentStoreState();
|
|
82
|
+
|
|
83
|
+
return agentSelectors.hasEnabledKnowledge(agentState);
|
|
84
|
+
}
|
|
85
|
+
|
|
73
86
|
// For all other plugins, enable by default
|
|
74
87
|
return true;
|
|
75
88
|
},
|
|
@@ -34,9 +34,6 @@ const errorHandlingLink: TRPCLink<LambdaRouter> = () => {
|
|
|
34
34
|
// Don't show notifications for abort errors
|
|
35
35
|
if (showError && !isAbortError) {
|
|
36
36
|
const { loginRequired } = await import('@/components/Error/loginRequiredNotification');
|
|
37
|
-
const { fetchErrorNotification } = await import(
|
|
38
|
-
'@/components/Error/fetchErrorNotification'
|
|
39
|
-
);
|
|
40
37
|
|
|
41
38
|
switch (status) {
|
|
42
39
|
case 401: {
|
|
@@ -53,9 +50,6 @@ const errorHandlingLink: TRPCLink<LambdaRouter> = () => {
|
|
|
53
50
|
|
|
54
51
|
default: {
|
|
55
52
|
console.error(err);
|
|
56
|
-
if (fetchErrorNotification && status) {
|
|
57
|
-
fetchErrorNotification.error({ errorMessage: err.message, status });
|
|
58
|
-
}
|
|
59
53
|
}
|
|
60
54
|
}
|
|
61
55
|
}
|
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
export default {
|
|
2
|
+
builtins: {
|
|
3
|
+
'lobe-knowledge-base': {
|
|
4
|
+
apiName: {
|
|
5
|
+
readKnowledge: '读取知识库内容',
|
|
6
|
+
searchKnowledgeBase: '搜索知识库',
|
|
7
|
+
},
|
|
8
|
+
title: '知识库',
|
|
9
|
+
},
|
|
10
|
+
'lobe-local-system': {
|
|
11
|
+
apiName: {
|
|
12
|
+
editLocalFile: '编辑文件',
|
|
13
|
+
getCommandOutput: '获取代码输出',
|
|
14
|
+
globLocalFiles: '匹配搜索文件',
|
|
15
|
+
grepContent: '搜索内容',
|
|
16
|
+
killCommand: '终止代码执行',
|
|
17
|
+
listLocalFiles: '查看文件列表',
|
|
18
|
+
moveLocalFiles: '移动文件',
|
|
19
|
+
readLocalFile: '读取文件内容',
|
|
20
|
+
renameLocalFile: '重命名',
|
|
21
|
+
runCommand: '执行代码',
|
|
22
|
+
searchLocalFiles: '搜索文件',
|
|
23
|
+
writeLocalFile: '写入文件',
|
|
24
|
+
},
|
|
25
|
+
title: '本地系统',
|
|
26
|
+
},
|
|
27
|
+
'lobe-web-browsing': {
|
|
28
|
+
apiName: {
|
|
29
|
+
crawlMultiPages: '读取多个页面内容',
|
|
30
|
+
crawlSinglePage: '读取页面内容',
|
|
31
|
+
search: '搜索页面',
|
|
32
|
+
},
|
|
33
|
+
title: '联网搜索',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
2
36
|
confirm: '确定',
|
|
3
37
|
debug: {
|
|
4
38
|
arguments: '调用参数',
|
|
@@ -253,23 +287,6 @@ export default {
|
|
|
253
287
|
content: '调用插件中...',
|
|
254
288
|
plugin: '插件运行中...',
|
|
255
289
|
},
|
|
256
|
-
localSystem: {
|
|
257
|
-
apiName: {
|
|
258
|
-
editLocalFile: '编辑文件',
|
|
259
|
-
getCommandOutput: '获取代码输出',
|
|
260
|
-
globLocalFiles: '匹配搜索文件',
|
|
261
|
-
grepContent: '搜索内容',
|
|
262
|
-
killCommand: '终止代码执行',
|
|
263
|
-
listLocalFiles: '查看文件列表',
|
|
264
|
-
moveLocalFiles: '移动文件',
|
|
265
|
-
readLocalFile: '读取文件内容',
|
|
266
|
-
renameLocalFile: '重命名',
|
|
267
|
-
runCommand: '执行代码',
|
|
268
|
-
searchLocalFiles: '搜索文件',
|
|
269
|
-
writeLocalFile: '写入文件',
|
|
270
|
-
},
|
|
271
|
-
title: '本地系统',
|
|
272
|
-
},
|
|
273
290
|
mcpInstall: {
|
|
274
291
|
CHECKING_INSTALLATION: '检查安装环境...',
|
|
275
292
|
COMPLETED: '安装完成',
|
|
@@ -378,11 +395,6 @@ export default {
|
|
|
378
395
|
warning: '⚠️ 请确认您信任此插件的来源,恶意插件可能会危害您的系统安全。',
|
|
379
396
|
},
|
|
380
397
|
search: {
|
|
381
|
-
apiName: {
|
|
382
|
-
crawlMultiPages: '读取多个页面内容',
|
|
383
|
-
crawlSinglePage: '读取页面内容',
|
|
384
|
-
search: '搜索页面',
|
|
385
|
-
},
|
|
386
398
|
config: {
|
|
387
399
|
addKey: '添加秘钥',
|
|
388
400
|
close: '删除',
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
codeInterpreter: {
|
|
2
|
+
'codeInterpreter': {
|
|
3
3
|
error: '执行错误',
|
|
4
4
|
executing: '执行中...',
|
|
5
5
|
files: '文件:',
|
|
6
6
|
output: '输出:',
|
|
7
7
|
returnValue: '返回值:',
|
|
8
8
|
},
|
|
9
|
-
dalle: {
|
|
9
|
+
'dalle': {
|
|
10
10
|
autoGenerate: '自动生成',
|
|
11
11
|
downloading: 'DallE3 生成的图片链接有效期仅1小时,正在缓存图片到本地...',
|
|
12
12
|
generate: '生成',
|
|
@@ -14,7 +14,15 @@ export default {
|
|
|
14
14
|
images: '图片:',
|
|
15
15
|
prompt: '提示词',
|
|
16
16
|
},
|
|
17
|
-
|
|
17
|
+
'lobe-knowledge-base': {
|
|
18
|
+
readKnowledge: {
|
|
19
|
+
meta: {
|
|
20
|
+
chars: '字符数',
|
|
21
|
+
lines: '行数',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
'localFiles': {
|
|
18
26
|
editFile: {
|
|
19
27
|
newString: '替换为',
|
|
20
28
|
oldString: '查找内容',
|
|
@@ -47,7 +55,7 @@ export default {
|
|
|
47
55
|
truncated: '已截断',
|
|
48
56
|
},
|
|
49
57
|
},
|
|
50
|
-
search: {
|
|
58
|
+
'search': {
|
|
51
59
|
createNewSearch: '创建新的搜索记录',
|
|
52
60
|
emptyResult: '没有搜索到结果,请修改关键词后重试',
|
|
53
61
|
genAiMessage: '创建助手消息',
|
|
@@ -94,7 +102,7 @@ export default {
|
|
|
94
102
|
summaryTooltip: '总结当前内容',
|
|
95
103
|
viewMoreResults: '查看更多 {{results}} 个结果',
|
|
96
104
|
},
|
|
97
|
-
updateArgs: {
|
|
105
|
+
'updateArgs': {
|
|
98
106
|
duplicateKeyError: '字段键必须唯一',
|
|
99
107
|
form: {
|
|
100
108
|
add: '添加一项',
|
|
@@ -1,32 +1,50 @@
|
|
|
1
1
|
import { DEFAULT_FILE_EMBEDDING_MODEL_ITEM } from '@lobechat/const';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ChatSemanticSearchChunk,
|
|
4
|
+
FileSearchResult,
|
|
5
|
+
ProviderConfig,
|
|
6
|
+
SemanticSearchSchema,
|
|
7
|
+
} from '@lobechat/types';
|
|
3
8
|
import { TRPCError } from '@trpc/server';
|
|
4
9
|
import { inArray } from 'drizzle-orm';
|
|
10
|
+
import pMap from 'p-map';
|
|
5
11
|
import { z } from 'zod';
|
|
6
12
|
|
|
7
13
|
import { AsyncTaskModel } from '@/database/models/asyncTask';
|
|
8
14
|
import { ChunkModel } from '@/database/models/chunk';
|
|
15
|
+
import { DocumentModel } from '@/database/models/document';
|
|
9
16
|
import { EmbeddingModel } from '@/database/models/embedding';
|
|
10
17
|
import { FileModel } from '@/database/models/file';
|
|
11
18
|
import { MessageModel } from '@/database/models/message';
|
|
19
|
+
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
|
12
20
|
import { knowledgeBaseFiles } from '@/database/schemas';
|
|
13
21
|
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
|
14
22
|
import { keyVaults, serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
15
|
-
import { getServerDefaultFilesConfig } from '@/server/globalConfig';
|
|
23
|
+
import { getServerDefaultFilesConfig, getServerGlobalConfig } from '@/server/globalConfig';
|
|
24
|
+
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
16
25
|
import { initModelRuntimeWithUserPayload } from '@/server/modules/ModelRuntime';
|
|
17
26
|
import { ChunkService } from '@/server/services/chunk';
|
|
27
|
+
import { DocumentService } from '@/server/services/document';
|
|
18
28
|
|
|
19
29
|
const chunkProcedure = authedProcedure
|
|
20
30
|
.use(serverDatabase)
|
|
21
31
|
.use(keyVaults)
|
|
22
32
|
.use(async (opts) => {
|
|
23
33
|
const { ctx } = opts;
|
|
34
|
+
const { aiProvider } = await getServerGlobalConfig();
|
|
24
35
|
|
|
25
36
|
return opts.next({
|
|
26
37
|
ctx: {
|
|
38
|
+
aiInfraRepos: new AiInfraRepos(
|
|
39
|
+
ctx.serverDB,
|
|
40
|
+
ctx.userId,
|
|
41
|
+
aiProvider as Record<string, ProviderConfig>,
|
|
42
|
+
),
|
|
27
43
|
asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
|
|
28
44
|
chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
|
|
29
45
|
chunkService: new ChunkService(ctx.serverDB, ctx.userId),
|
|
46
|
+
documentModel: new DocumentModel(ctx.serverDB, ctx.userId),
|
|
47
|
+
documentService: new DocumentService(ctx.serverDB, ctx.userId),
|
|
30
48
|
embeddingModel: new EmbeddingModel(ctx.serverDB, ctx.userId),
|
|
31
49
|
fileModel: new FileModel(ctx.serverDB, ctx.userId),
|
|
32
50
|
messageModel: new MessageModel(ctx.serverDB, ctx.userId),
|
|
@@ -34,6 +52,50 @@ const chunkProcedure = authedProcedure
|
|
|
34
52
|
});
|
|
35
53
|
});
|
|
36
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Group chunks by file and calculate relevance scores
|
|
57
|
+
*/
|
|
58
|
+
const groupAndRankFiles = (chunks: ChatSemanticSearchChunk[], topK: number): FileSearchResult[] => {
|
|
59
|
+
const fileMap = new Map<string, FileSearchResult>();
|
|
60
|
+
|
|
61
|
+
// Group chunks by file
|
|
62
|
+
for (const chunk of chunks) {
|
|
63
|
+
const fileId = chunk.fileId || 'unknown';
|
|
64
|
+
const fileName = chunk.fileName || `File ${fileId}`;
|
|
65
|
+
|
|
66
|
+
if (!fileMap.has(fileId)) {
|
|
67
|
+
fileMap.set(fileId, {
|
|
68
|
+
fileId,
|
|
69
|
+
fileName,
|
|
70
|
+
relevanceScore: 0,
|
|
71
|
+
topChunks: [],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fileResult = fileMap.get(fileId)!;
|
|
76
|
+
fileResult.topChunks.push({
|
|
77
|
+
id: chunk.id,
|
|
78
|
+
similarity: chunk.similarity,
|
|
79
|
+
text: chunk.text || '',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Calculate relevance score for each file (average of top 3 chunks)
|
|
84
|
+
for (const fileResult of fileMap.values()) {
|
|
85
|
+
fileResult.topChunks.sort((a, b) => b.similarity - a.similarity);
|
|
86
|
+
const top3 = fileResult.topChunks.slice(0, 3);
|
|
87
|
+
fileResult.relevanceScore =
|
|
88
|
+
top3.reduce((sum, chunk) => sum + chunk.similarity, 0) / top3.length;
|
|
89
|
+
// Keep only top chunks per file
|
|
90
|
+
fileResult.topChunks = fileResult.topChunks.slice(0, 3);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Sort files by relevance score and return top K
|
|
94
|
+
return Array.from(fileMap.values())
|
|
95
|
+
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
96
|
+
.slice(0, topK);
|
|
97
|
+
};
|
|
98
|
+
|
|
37
99
|
export const chunkRouter = router({
|
|
38
100
|
createEmbeddingChunksTask: chunkProcedure
|
|
39
101
|
.input(
|
|
@@ -78,6 +140,66 @@ export const chunkRouter = router({
|
|
|
78
140
|
};
|
|
79
141
|
}),
|
|
80
142
|
|
|
143
|
+
getFileContents: chunkProcedure
|
|
144
|
+
.input(
|
|
145
|
+
z.object({
|
|
146
|
+
fileIds: z.array(z.string()),
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
.mutation(async ({ ctx, input }) => {
|
|
150
|
+
return await pMap(
|
|
151
|
+
input.fileIds,
|
|
152
|
+
async (fileId) => {
|
|
153
|
+
// 1. Find file information
|
|
154
|
+
const file = await ctx.fileModel.findById(fileId);
|
|
155
|
+
if (!file) {
|
|
156
|
+
return {
|
|
157
|
+
content: '',
|
|
158
|
+
error: 'File not found',
|
|
159
|
+
fileId,
|
|
160
|
+
filename: `Unknown file ${fileId}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2. Find existing parsed document
|
|
165
|
+
let document = await ctx.documentModel.findByFileId(fileId);
|
|
166
|
+
|
|
167
|
+
// 3. If not exists, parse the file
|
|
168
|
+
if (!document) {
|
|
169
|
+
try {
|
|
170
|
+
document = await ctx.documentService.parseFile(fileId);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
content: '',
|
|
174
|
+
error: `Failed to parse file: ${(error as Error).message}`,
|
|
175
|
+
fileId,
|
|
176
|
+
filename: file.name,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. Calculate file statistics
|
|
182
|
+
const content = document.content || '';
|
|
183
|
+
const lines = content.split('\n');
|
|
184
|
+
const totalLineCount = lines.length;
|
|
185
|
+
const totalCharCount = content.length;
|
|
186
|
+
const preview = lines.slice(0, 5).join('\n');
|
|
187
|
+
|
|
188
|
+
// 5. Return content with details
|
|
189
|
+
return {
|
|
190
|
+
content,
|
|
191
|
+
fileId,
|
|
192
|
+
filename: file.name,
|
|
193
|
+
metadata: document.metadata,
|
|
194
|
+
preview,
|
|
195
|
+
totalCharCount,
|
|
196
|
+
totalLineCount,
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
{ concurrency: 3 },
|
|
200
|
+
);
|
|
201
|
+
}),
|
|
202
|
+
|
|
81
203
|
retryParseFileTask: chunkProcedure
|
|
82
204
|
.input(
|
|
83
205
|
z.object({
|
|
@@ -117,7 +239,6 @@ export const chunkRouter = router({
|
|
|
117
239
|
input: input.query,
|
|
118
240
|
model,
|
|
119
241
|
});
|
|
120
|
-
console.timeEnd('embedding');
|
|
121
242
|
|
|
122
243
|
return ctx.chunkModel.semanticSearch({
|
|
123
244
|
embedding: embeddings![0],
|
|
@@ -130,47 +251,30 @@ export const chunkRouter = router({
|
|
|
130
251
|
.input(SemanticSearchSchema)
|
|
131
252
|
.mutation(async ({ ctx, input }) => {
|
|
132
253
|
try {
|
|
133
|
-
const item = await ctx.messageModel.findMessageQueriesById(input.messageId);
|
|
134
254
|
const { model, provider } =
|
|
135
255
|
getServerDefaultFilesConfig().embeddingModel || DEFAULT_FILE_EMBEDDING_MODEL_ITEM;
|
|
136
256
|
let embedding: number[];
|
|
137
|
-
let ragQueryId: string;
|
|
138
|
-
|
|
139
|
-
// if there is no message rag or it's embeddings, then we need to create one
|
|
140
|
-
if (!item || !item.embeddings) {
|
|
141
|
-
// TODO: need to support customize
|
|
142
|
-
const agentRuntime = await initModelRuntimeWithUserPayload(provider, ctx.jwtPayload);
|
|
143
|
-
|
|
144
|
-
// slice content to make sure in the context window limit
|
|
145
|
-
const query =
|
|
146
|
-
input.rewriteQuery.length > 8000
|
|
147
|
-
? input.rewriteQuery.slice(0, 8000)
|
|
148
|
-
: input.rewriteQuery;
|
|
149
|
-
|
|
150
|
-
const embeddings = await agentRuntime.embeddings({
|
|
151
|
-
dimensions: 1024,
|
|
152
|
-
input: query,
|
|
153
|
-
model,
|
|
154
|
-
});
|
|
155
257
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
258
|
+
const providerDetail = await ctx.aiInfraRepos.getAiProviderDetail(
|
|
259
|
+
provider,
|
|
260
|
+
KeyVaultsGateKeeper.getUserKeyVaults,
|
|
261
|
+
);
|
|
161
262
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
userQuery: input.userQuery,
|
|
167
|
-
});
|
|
263
|
+
const modelRuntime = initModelRuntimeWithUserPayload(
|
|
264
|
+
provider,
|
|
265
|
+
providerDetail.keyVaults || {},
|
|
266
|
+
);
|
|
168
267
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
268
|
+
// slice content to make sure in the context window limit
|
|
269
|
+
const query = input.query.length > 8000 ? input.query.slice(0, 8000) : input.query;
|
|
270
|
+
|
|
271
|
+
const embeddings = await modelRuntime.embeddings({
|
|
272
|
+
dimensions: 1024,
|
|
273
|
+
input: query,
|
|
274
|
+
model,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
embedding = embeddings![0];
|
|
174
278
|
|
|
175
279
|
let finalFileIds = input.fileIds ?? [];
|
|
176
280
|
|
|
@@ -185,18 +289,41 @@ export const chunkRouter = router({
|
|
|
185
289
|
const chunks = await ctx.chunkModel.semanticSearchForChat({
|
|
186
290
|
embedding,
|
|
187
291
|
fileIds: finalFileIds,
|
|
188
|
-
query: input.
|
|
292
|
+
query: input.query,
|
|
293
|
+
topK: input.topK,
|
|
189
294
|
});
|
|
190
295
|
|
|
296
|
+
// Group chunks by file and calculate relevance scores
|
|
297
|
+
const fileResults = groupAndRankFiles(chunks, input.topK || 15);
|
|
298
|
+
|
|
191
299
|
// TODO: need to rerank the chunks
|
|
192
300
|
|
|
193
|
-
return { chunks,
|
|
301
|
+
return { chunks, fileResults };
|
|
194
302
|
} catch (e) {
|
|
195
303
|
console.error(e);
|
|
196
304
|
|
|
305
|
+
const error = e as any;
|
|
306
|
+
const errorType = error.errorType;
|
|
307
|
+
|
|
308
|
+
// Map business error types to appropriate HTTP status codes
|
|
309
|
+
if (errorType === 'InvalidProviderAPIKey') {
|
|
310
|
+
throw new TRPCError({
|
|
311
|
+
code: 'METHOD_NOT_SUPPORTED',
|
|
312
|
+
message: error.message || 'Invalid API key for embedding provider',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (errorType === 'ProviderBizError') {
|
|
317
|
+
throw new TRPCError({
|
|
318
|
+
code: 'BAD_REQUEST',
|
|
319
|
+
message: error.message || 'Provider service error',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// For unknown errors, still return 500 but with proper message
|
|
197
324
|
throw new TRPCError({
|
|
198
325
|
code: 'INTERNAL_SERVER_ERROR',
|
|
199
|
-
message:
|
|
326
|
+
message: error.message || errorType || 'Failed to perform semantic search',
|
|
200
327
|
});
|
|
201
328
|
}
|
|
202
329
|
}),
|
|
@@ -913,7 +913,7 @@ describe('ChatService', () => {
|
|
|
913
913
|
enabledToolIds: [WebBrowsingManifest.identifier],
|
|
914
914
|
}),
|
|
915
915
|
};
|
|
916
|
-
vi.spyOn(toolEngineeringModule, '
|
|
916
|
+
vi.spyOn(toolEngineeringModule, 'createAgentToolsEngine').mockReturnValue(
|
|
917
917
|
mockToolsEngine as any,
|
|
918
918
|
);
|
|
919
919
|
|
|
@@ -964,7 +964,7 @@ describe('ChatService', () => {
|
|
|
964
964
|
enabledToolIds: [WebBrowsingManifest.identifier],
|
|
965
965
|
}),
|
|
966
966
|
};
|
|
967
|
-
vi.spyOn(toolEngineeringModule, '
|
|
967
|
+
vi.spyOn(toolEngineeringModule, 'createAgentToolsEngine').mockReturnValue(
|
|
968
968
|
mockToolsEngine as any,
|
|
969
969
|
);
|
|
970
970
|
|
|
@@ -1009,7 +1009,7 @@ describe('ChatService', () => {
|
|
|
1009
1009
|
enabledToolIds: [WebBrowsingManifest.identifier],
|
|
1010
1010
|
}),
|
|
1011
1011
|
};
|
|
1012
|
-
vi.spyOn(toolEngineeringModule, '
|
|
1012
|
+
vi.spyOn(toolEngineeringModule, 'createAgentToolsEngine').mockReturnValue(
|
|
1013
1013
|
mockToolsEngine as any,
|
|
1014
1014
|
);
|
|
1015
1015
|
|
|
@@ -14,7 +14,7 @@ import { enableAuth } from '@/const/auth';
|
|
|
14
14
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
|
15
15
|
import { isDesktop } from '@/const/version';
|
|
16
16
|
import { getSearchConfig } from '@/helpers/getSearchConfig';
|
|
17
|
-
import {
|
|
17
|
+
import { createAgentToolsEngine, createToolsEngine } from '@/helpers/toolEngineering';
|
|
18
18
|
import { getAgentStoreState } from '@/store/agent';
|
|
19
19
|
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
|
20
20
|
import { aiModelSelectors, aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
|
|
@@ -90,7 +90,7 @@ class ChatService {
|
|
|
90
90
|
|
|
91
91
|
const pluginIds = [...(enabledPlugins || [])];
|
|
92
92
|
|
|
93
|
-
const toolsEngine =
|
|
93
|
+
const toolsEngine = createAgentToolsEngine({
|
|
94
94
|
model: payload.model,
|
|
95
95
|
provider: payload.provider!,
|
|
96
96
|
});
|