@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.
Files changed (96) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/src/main/services/__tests__/fileSrv.test.ts +603 -0
  3. package/changelog/v1.json +9 -0
  4. package/codecov.yml +1 -0
  5. package/locales/ar/plugin.json +34 -22
  6. package/locales/ar/tool.json +8 -0
  7. package/locales/bg-BG/plugin.json +34 -22
  8. package/locales/bg-BG/tool.json +8 -0
  9. package/locales/de-DE/plugin.json +34 -22
  10. package/locales/de-DE/tool.json +8 -0
  11. package/locales/en-US/plugin.json +34 -22
  12. package/locales/en-US/tool.json +8 -0
  13. package/locales/es-ES/plugin.json +34 -22
  14. package/locales/es-ES/tool.json +8 -0
  15. package/locales/fa-IR/plugin.json +34 -22
  16. package/locales/fa-IR/tool.json +8 -0
  17. package/locales/fr-FR/plugin.json +34 -22
  18. package/locales/fr-FR/tool.json +8 -0
  19. package/locales/it-IT/plugin.json +34 -22
  20. package/locales/it-IT/tool.json +8 -0
  21. package/locales/ja-JP/plugin.json +34 -22
  22. package/locales/ja-JP/tool.json +8 -0
  23. package/locales/ko-KR/plugin.json +34 -22
  24. package/locales/ko-KR/tool.json +8 -0
  25. package/locales/nl-NL/plugin.json +34 -22
  26. package/locales/nl-NL/tool.json +8 -0
  27. package/locales/pl-PL/plugin.json +34 -22
  28. package/locales/pl-PL/tool.json +8 -0
  29. package/locales/pt-BR/plugin.json +34 -22
  30. package/locales/pt-BR/tool.json +8 -0
  31. package/locales/ru-RU/plugin.json +34 -22
  32. package/locales/ru-RU/tool.json +8 -0
  33. package/locales/tr-TR/plugin.json +34 -22
  34. package/locales/tr-TR/tool.json +8 -0
  35. package/locales/vi-VN/plugin.json +34 -22
  36. package/locales/vi-VN/tool.json +8 -0
  37. package/locales/zh-CN/plugin.json +34 -22
  38. package/locales/zh-CN/tool.json +8 -0
  39. package/locales/zh-TW/plugin.json +34 -22
  40. package/locales/zh-TW/tool.json +8 -0
  41. package/package.json +1 -1
  42. package/packages/database/src/models/__tests__/document.test.ts +149 -0
  43. package/packages/database/src/models/chunk.ts +3 -1
  44. package/packages/database/src/models/document.ts +8 -2
  45. package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatFileContents.test.ts.snap +75 -0
  46. package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatNoSearchResults.test.ts.snap +45 -0
  47. package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatSearchResults.test.ts.snap +82 -0
  48. package/packages/prompts/src/prompts/knowledgeBaseQA/formatFileContents.test.ts +118 -0
  49. package/packages/prompts/src/prompts/knowledgeBaseQA/formatFileContents.ts +31 -0
  50. package/packages/prompts/src/prompts/knowledgeBaseQA/formatNoSearchResults.test.ts +25 -0
  51. package/packages/prompts/src/prompts/knowledgeBaseQA/formatNoSearchResults.ts +13 -0
  52. package/packages/prompts/src/prompts/knowledgeBaseQA/formatSearchResults.test.ts +191 -0
  53. package/packages/prompts/src/prompts/knowledgeBaseQA/formatSearchResults.ts +50 -0
  54. package/packages/prompts/src/prompts/knowledgeBaseQA/index.ts +6 -0
  55. package/packages/types/src/rag.ts +13 -4
  56. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
  57. package/src/features/ChatInput/ActionBar/Token/TokenTagForGroupChat.tsx +2 -2
  58. package/src/features/ChatList/Messages/Group/Tool/Inspector/ToolTitle.tsx +5 -23
  59. package/src/features/ChatList/Messages/Tool/Inspector/ToolTitle.tsx +5 -25
  60. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  61. package/src/helpers/toolEngineering/index.test.ts +3 -3
  62. package/src/helpers/toolEngineering/index.ts +17 -4
  63. package/src/libs/trpc/client/lambda.ts +0 -6
  64. package/src/locales/default/plugin.ts +34 -22
  65. package/src/locales/default/tool.ts +13 -5
  66. package/src/server/routers/lambda/chunk.ts +168 -41
  67. package/src/services/chat/chat.test.ts +3 -3
  68. package/src/services/chat/index.ts +2 -2
  69. package/src/services/rag.ts +6 -2
  70. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +11 -0
  71. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +0 -87
  72. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +2 -69
  73. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +0 -2
  74. package/src/store/chat/slices/aiChat/actions/rag.ts +0 -47
  75. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +8 -69
  76. package/src/store/chat/slices/builtinTool/actions/index.ts +4 -1
  77. package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +174 -0
  78. package/src/store/chat/slices/operation/types.ts +1 -0
  79. package/src/store/chat/slices/thread/action.test.ts +0 -1
  80. package/src/store/chat/slices/thread/action.ts +0 -1
  81. package/src/tools/executionRuntimes.ts +3 -0
  82. package/src/tools/identifiers.ts +13 -0
  83. package/src/tools/index.ts +7 -0
  84. package/src/tools/knowledge-base/ExecutionRuntime/index.ts +96 -0
  85. package/src/tools/knowledge-base/Render/ReadKnowledge/FileCard.tsx +135 -0
  86. package/src/tools/knowledge-base/Render/ReadKnowledge/index.tsx +27 -0
  87. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/Item/index.tsx +54 -0
  88. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/Item/style.ts +51 -0
  89. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/index.tsx +23 -0
  90. package/src/tools/knowledge-base/Render/index.ts +7 -0
  91. package/src/tools/knowledge-base/index.ts +64 -0
  92. package/src/tools/knowledge-base/systemRole.ts +102 -0
  93. package/src/tools/knowledge-base/type.ts +25 -0
  94. package/src/tools/local-system/Intervention/WriteFile/index.tsx +1 -1
  95. package/src/tools/renders.ts +4 -0
  96. 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, useMemo } from 'react';
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 { LocalSystemManifest } from '@/tools/local-system';
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
- const plugins = useMemo(
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
- {...builtinPluginTitle}
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, useMemo } from 'react';
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 { LocalSystemManifest } from '@/tools/local-system';
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
- const plugins = useMemo(
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
- {...builtinPluginTitle}
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
  );
@@ -37,7 +37,7 @@ const BuiltinType = memo<BuiltinTypeProps>(
37
37
  return (
38
38
  <Render
39
39
  apiName={apiName}
40
- args={args}
40
+ args={args || {}}
41
41
  content={data}
42
42
  identifier={identifier}
43
43
  messageId={id}
@@ -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 { createChatToolsEngine, createToolsEngine, getEnabledTools } from './index';
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 = createChatToolsEngine({
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 = createChatToolsEngine({
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 createChatToolsEngine = (workingModel: WorkingModel) =>
59
+ export const createAgentToolsEngine = (workingModel: WorkingModel) =>
57
60
  createToolsEngine({
58
- // Add WebBrowsingManifest as default tool
59
- defaultToolIds: [WebBrowsingManifest.identifier],
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
- localFiles: {
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 { SemanticSearchSchema } from '@lobechat/types';
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
- embedding = embeddings![0];
157
- const embeddingsId = await ctx.embeddingModel.create({
158
- embeddings: embedding,
159
- model,
160
- });
258
+ const providerDetail = await ctx.aiInfraRepos.getAiProviderDetail(
259
+ provider,
260
+ KeyVaultsGateKeeper.getUserKeyVaults,
261
+ );
161
262
 
162
- const result = await ctx.messageModel.createMessageQuery({
163
- embeddingsId,
164
- messageId: input.messageId,
165
- rewriteQuery: input.rewriteQuery,
166
- userQuery: input.userQuery,
167
- });
263
+ const modelRuntime = initModelRuntimeWithUserPayload(
264
+ provider,
265
+ providerDetail.keyVaults || {},
266
+ );
168
267
 
169
- ragQueryId = result.id;
170
- } else {
171
- embedding = item.embeddings;
172
- ragQueryId = item.id;
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.rewriteQuery,
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, queryId: ragQueryId };
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: (e as any).errorType || JSON.stringify(e),
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, 'createChatToolsEngine').mockReturnValue(
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, 'createChatToolsEngine').mockReturnValue(
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, 'createChatToolsEngine').mockReturnValue(
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 { createChatToolsEngine, createToolsEngine } from '@/helpers/toolEngineering';
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 = createChatToolsEngine({
93
+ const toolsEngine = createAgentToolsEngine({
94
94
  model: payload.model,
95
95
  provider: payload.provider!,
96
96
  });