@lobehub/chat 1.56.1 → 1.56.3
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 +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/CategoryContent/useCategory.tsx +2 -2
- package/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx +1 -1
- package/src/database/server/models/__tests__/session.test.ts +3 -3
- package/src/database/server/models/session.ts +5 -6
- package/src/database/server/models/user.ts +1 -5
- package/src/features/AgentSetting/AgentMeta/index.tsx +4 -1
- package/src/features/DevPanel/features/Table/TableCell.tsx +34 -33
- package/src/libs/next-auth/adapter/index.ts +7 -0
- package/src/server/routers/lambda/agent.test.ts +306 -0
- package/src/server/routers/lambda/agent.ts +3 -1
- package/src/server/services/agent/index.test.ts +65 -0
- package/src/server/services/agent/index.ts +22 -0
- package/src/server/services/user/index.test.ts +17 -1
- package/src/server/services/user/index.ts +5 -0
- package/src/services/session/client.ts +12 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,56 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.56.3](https://github.com/lobehub/lobe-chat/compare/v1.56.2...v1.56.3)
|
6
|
+
|
7
|
+
<sup>Released on **2025-02-16**</sup>
|
8
|
+
|
9
|
+
#### 💄 Styles
|
10
|
+
|
11
|
+
- **misc**: Improve inbox agent settings.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### Styles
|
19
|
+
|
20
|
+
- **misc**: Improve inbox agent settings, closes [#6197](https://github.com/lobehub/lobe-chat/issues/6197) ([37b70f0](https://github.com/lobehub/lobe-chat/commit/37b70f0))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
### [Version 1.56.2](https://github.com/lobehub/lobe-chat/compare/v1.56.1...v1.56.2)
|
31
|
+
|
32
|
+
<sup>Released on **2025-02-16**</sup>
|
33
|
+
|
34
|
+
#### 🐛 Bug Fixes
|
35
|
+
|
36
|
+
- **misc**: Fix inbox agent can not save config.
|
37
|
+
|
38
|
+
<br/>
|
39
|
+
|
40
|
+
<details>
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
42
|
+
|
43
|
+
#### What's fixed
|
44
|
+
|
45
|
+
- **misc**: Fix inbox agent can not save config, closes [#6186](https://github.com/lobehub/lobe-chat/issues/6186) ([588cba7](https://github.com/lobehub/lobe-chat/commit/588cba7))
|
46
|
+
|
47
|
+
</details>
|
48
|
+
|
49
|
+
<div align="right">
|
50
|
+
|
51
|
+
[](#readme-top)
|
52
|
+
|
53
|
+
</div>
|
54
|
+
|
5
55
|
### [Version 1.56.1](https://github.com/lobehub/lobe-chat/compare/v1.56.0...v1.56.1)
|
6
56
|
|
7
57
|
<sup>Released on **2025-02-16**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Improve inbox agent settings."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-02-16",
|
9
|
+
"version": "1.56.3"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"fixes": [
|
14
|
+
"Fix inbox agent can not save config."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-02-16",
|
18
|
+
"version": "1.56.2"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"fixes": [
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.56.
|
3
|
+
"version": "1.56.3",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -27,11 +27,11 @@ export const useCategory = ({ mobile }: UseCategoryOptions = {}) => {
|
|
27
27
|
key: ChatSettingsTabs.Meta,
|
28
28
|
label: t('agentTab.meta'),
|
29
29
|
}) as MenuItemType,
|
30
|
-
|
30
|
+
{
|
31
31
|
icon: <Icon icon={Bot} size={iconSize} />,
|
32
32
|
key: ChatSettingsTabs.Prompt,
|
33
33
|
label: t('agentTab.prompt'),
|
34
|
-
}
|
34
|
+
},
|
35
35
|
{
|
36
36
|
icon: <Icon icon={MessagesSquare} size={iconSize} />,
|
37
37
|
key: ChatSettingsTabs.Chat,
|
@@ -41,7 +41,7 @@ const AgentSettings = memo(() => {
|
|
41
41
|
]);
|
42
42
|
const isInbox = id === INBOX_SESSION_ID;
|
43
43
|
|
44
|
-
const [tab, setTab] = useState(isInbox ? ChatSettingsTabs.
|
44
|
+
const [tab, setTab] = useState(isInbox ? ChatSettingsTabs.Prompt : ChatSettingsTabs.Meta);
|
45
45
|
|
46
46
|
const ref = useRef<any>(null);
|
47
47
|
const theme = useTheme();
|
@@ -628,7 +628,7 @@ describe('SessionModel', () => {
|
|
628
628
|
|
629
629
|
describe('createInbox', () => {
|
630
630
|
it('should create inbox session if not exists', async () => {
|
631
|
-
const inbox = await sessionModel.createInbox();
|
631
|
+
const inbox = await sessionModel.createInbox({});
|
632
632
|
|
633
633
|
expect(inbox).toBeDefined();
|
634
634
|
expect(inbox?.slug).toBe('inbox');
|
@@ -641,10 +641,10 @@ describe('SessionModel', () => {
|
|
641
641
|
|
642
642
|
it('should not create duplicate inbox session', async () => {
|
643
643
|
// Create first inbox
|
644
|
-
await sessionModel.createInbox();
|
644
|
+
await sessionModel.createInbox({});
|
645
645
|
|
646
646
|
// Try to create another inbox
|
647
|
-
const duplicateInbox = await sessionModel.createInbox();
|
647
|
+
const duplicateInbox = await sessionModel.createInbox({});
|
648
648
|
|
649
649
|
// Should return undefined as inbox already exists
|
650
650
|
expect(duplicateInbox).toBeUndefined();
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { Column, count, sql } from 'drizzle-orm';
|
2
2
|
import { and, asc, desc, eq, gt, inArray, isNull, like, not, or } from 'drizzle-orm/expressions';
|
3
|
+
import { DeepPartial } from 'utility-types';
|
3
4
|
|
4
|
-
import { appEnv } from '@/config/app';
|
5
5
|
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
|
6
6
|
import { INBOX_SESSION_ID } from '@/const/session';
|
7
7
|
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
@@ -13,7 +13,7 @@ import {
|
|
13
13
|
genWhere,
|
14
14
|
} from '@/database/utils/genWhere';
|
15
15
|
import { idGenerator } from '@/database/utils/idGenerator';
|
16
|
-
import {
|
16
|
+
import { LobeAgentConfig } from '@/types/agent';
|
17
17
|
import { ChatSessionList, LobeAgentSession, SessionRankItem } from '@/types/session';
|
18
18
|
import { merge } from '@/utils/merge';
|
19
19
|
|
@@ -226,16 +226,15 @@ export class SessionModel {
|
|
226
226
|
});
|
227
227
|
};
|
228
228
|
|
229
|
-
createInbox = async () => {
|
229
|
+
createInbox = async (defaultAgentConfig: DeepPartial<LobeAgentConfig>) => {
|
230
230
|
const item = await this.db.query.sessions.findFirst({
|
231
231
|
where: and(eq(sessions.userId, this.userId), eq(sessions.slug, INBOX_SESSION_ID)),
|
232
232
|
});
|
233
|
-
if (item) return;
|
234
233
|
|
235
|
-
|
234
|
+
if (item) return;
|
236
235
|
|
237
236
|
return await this.create({
|
238
|
-
config: merge(DEFAULT_AGENT_CONFIG,
|
237
|
+
config: merge(DEFAULT_AGENT_CONFIG, defaultAgentConfig),
|
239
238
|
slug: INBOX_SESSION_ID,
|
240
239
|
type: 'agent',
|
241
240
|
});
|
@@ -10,7 +10,6 @@ import { merge } from '@/utils/merge';
|
|
10
10
|
import { today } from '@/utils/time';
|
11
11
|
|
12
12
|
import { NewUser, UserItem, UserSettingsItem, userSettings, users } from '../../schemas';
|
13
|
-
import { SessionModel } from './session';
|
14
13
|
|
15
14
|
type DecryptUserKeyVaults = (
|
16
15
|
encryptKeyVaultsStr: string | null,
|
@@ -160,10 +159,7 @@ export class UserModel {
|
|
160
159
|
.values({ ...params })
|
161
160
|
.returning();
|
162
161
|
|
163
|
-
|
164
|
-
const model = new SessionModel(db, user.id);
|
165
|
-
|
166
|
-
await model.createInbox();
|
162
|
+
return user;
|
167
163
|
};
|
168
164
|
|
169
165
|
static deleteUser = async (db: LobeChatDatabase, id: string) => {
|
@@ -9,6 +9,7 @@ import { memo } from 'react';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
10
10
|
|
11
11
|
import { FORM_STYLE } from '@/const/layoutTokens';
|
12
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
12
13
|
|
13
14
|
import { useStore } from '../store';
|
14
15
|
import { SessionLoadingState } from '../store/initialState';
|
@@ -26,9 +27,11 @@ const AgentMeta = memo(() => {
|
|
26
27
|
s.autocompleteMeta,
|
27
28
|
s.autocompleteAllMeta,
|
28
29
|
]);
|
29
|
-
const loading = useStore((s) => s.autocompleteLoading);
|
30
|
+
const [isInbox, loading] = useStore((s) => [s.id === INBOX_SESSION_ID, s.autocompleteLoading]);
|
30
31
|
const meta = useStore((s) => s.meta, isEqual);
|
31
32
|
|
33
|
+
if (isInbox) return;
|
34
|
+
|
32
35
|
const basic = [
|
33
36
|
{
|
34
37
|
Render: AutoGenerateInput,
|
@@ -1,29 +1,27 @@
|
|
1
|
-
import { Typography } from 'antd';
|
2
|
-
import { createStyles } from 'antd-style';
|
3
1
|
import dayjs from 'dayjs';
|
4
2
|
import { get, isDate } from 'lodash-es';
|
5
3
|
import React, { useMemo } from 'react';
|
6
4
|
|
7
|
-
import TooltipContent from './TooltipContent';
|
5
|
+
// import TooltipContent from './TooltipContent';
|
8
6
|
|
9
|
-
const { Text } = Typography;
|
7
|
+
// const { Text } = Typography;
|
10
8
|
|
11
|
-
const useStyles = createStyles(({ token, css }) => ({
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
}));
|
9
|
+
// const useStyles = createStyles(({ token, css }) => ({
|
10
|
+
// cell: css`
|
11
|
+
// font-family: ${token.fontFamilyCode};
|
12
|
+
// font-size: ${token.fontSizeSM}px;
|
13
|
+
// `,
|
14
|
+
// tooltip: css`
|
15
|
+
// border: 1px solid ${token.colorBorder};
|
16
|
+
//
|
17
|
+
// font-family: ${token.fontFamilyCode};
|
18
|
+
// font-size: ${token.fontSizeSM}px;
|
19
|
+
// color: ${token.colorText} !important;
|
20
|
+
// word-break: break-all;
|
21
|
+
//
|
22
|
+
// background: ${token.colorBgElevated} !important;
|
23
|
+
// `,
|
24
|
+
// }));
|
27
25
|
|
28
26
|
interface TableCellProps {
|
29
27
|
column: string;
|
@@ -32,7 +30,7 @@ interface TableCellProps {
|
|
32
30
|
}
|
33
31
|
|
34
32
|
const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
|
35
|
-
const { styles } = useStyles();
|
33
|
+
// const { styles } = useStyles();
|
36
34
|
const data = get(dataItem, column);
|
37
35
|
const content = useMemo(() => {
|
38
36
|
if (isDate(data)) return dayjs(data).format('YYYY-MM-DD HH:mm:ss');
|
@@ -54,18 +52,21 @@ const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
|
|
54
52
|
|
55
53
|
return (
|
56
54
|
<td key={column} onDoubleClick={() => console.log('Edit cell:', rowIndex, column)}>
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
>
|
67
|
-
|
68
|
-
|
55
|
+
{content}
|
56
|
+
|
57
|
+
{/* 不能使用 antd 的 Text, 会有大量的重渲染导致滚动极其卡顿 */}
|
58
|
+
{/*<Text*/}
|
59
|
+
{/* className={styles.cell}*/}
|
60
|
+
{/* ellipsis={{*/}
|
61
|
+
{/* tooltip: {*/}
|
62
|
+
{/* arrow: false,*/}
|
63
|
+
{/* classNames: { body: styles.tooltip },*/}
|
64
|
+
{/* title: <TooltipContent>{content}</TooltipContent>,*/}
|
65
|
+
{/* },*/}
|
66
|
+
{/* }}*/}
|
67
|
+
{/*>*/}
|
68
|
+
{/* {content}*/}
|
69
|
+
{/*</Text>*/}
|
69
70
|
</td>
|
70
71
|
);
|
71
72
|
};
|
@@ -10,6 +10,7 @@ import { Adapter, AdapterAccount } from 'next-auth/adapters';
|
|
10
10
|
|
11
11
|
import * as schema from '@/database/schemas';
|
12
12
|
import { UserModel } from '@/database/server/models/user';
|
13
|
+
import { AgentService } from '@/server/services/agent';
|
13
14
|
import { merge } from '@/utils/merge';
|
14
15
|
|
15
16
|
import {
|
@@ -65,6 +66,7 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
|
|
65
66
|
const adapterUser = mapLobeUserToAdapterUser(existingUser);
|
66
67
|
return adapterUser;
|
67
68
|
}
|
69
|
+
|
68
70
|
// create a new user if it does not exist
|
69
71
|
await UserModel.createUser(
|
70
72
|
serverDB,
|
@@ -77,6 +79,11 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
|
|
77
79
|
name,
|
78
80
|
}),
|
79
81
|
);
|
82
|
+
|
83
|
+
// 3. Create an inbox session for the user
|
84
|
+
const agentService = new AgentService(serverDB, id);
|
85
|
+
await agentService.createInbox();
|
86
|
+
|
80
87
|
return { ...user, id: providerAccountId ?? id };
|
81
88
|
},
|
82
89
|
async createVerificationToken(data): Promise<VerificationToken | null | undefined> {
|
@@ -0,0 +1,306 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
5
|
+
import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
|
6
|
+
import { serverDB } from '@/database/server';
|
7
|
+
import { AgentModel } from '@/database/server/models/agent';
|
8
|
+
import { FileModel } from '@/database/server/models/file';
|
9
|
+
import { KnowledgeBaseModel } from '@/database/server/models/knowledgeBase';
|
10
|
+
import { SessionModel } from '@/database/server/models/session';
|
11
|
+
import { UserModel } from '@/database/server/models/user';
|
12
|
+
import { AgentService } from '@/server/services/agent';
|
13
|
+
import { KnowledgeType } from '@/types/knowledgeBase';
|
14
|
+
|
15
|
+
import { agentRouter } from './agent';
|
16
|
+
|
17
|
+
vi.mock('@/database/server/models/user', () => ({
|
18
|
+
UserModel: {
|
19
|
+
findById: vi.fn(),
|
20
|
+
},
|
21
|
+
}));
|
22
|
+
|
23
|
+
vi.mock('@/database/server/models/agent', () => ({
|
24
|
+
AgentModel: vi.fn(),
|
25
|
+
}));
|
26
|
+
|
27
|
+
vi.mock('@/database/server/models/session', () => ({
|
28
|
+
SessionModel: vi.fn(),
|
29
|
+
}));
|
30
|
+
|
31
|
+
vi.mock('@/database/server/models/file', () => ({
|
32
|
+
FileModel: vi.fn(),
|
33
|
+
}));
|
34
|
+
|
35
|
+
vi.mock('@/database/server/models/knowledgeBase', () => ({
|
36
|
+
KnowledgeBaseModel: vi.fn(),
|
37
|
+
}));
|
38
|
+
|
39
|
+
vi.mock('@/server/services/agent', () => ({
|
40
|
+
AgentService: vi.fn(),
|
41
|
+
}));
|
42
|
+
|
43
|
+
describe('agentRouter', () => {
|
44
|
+
const userId = 'testUserId';
|
45
|
+
let mockCtx: any;
|
46
|
+
let agentModelMock: any;
|
47
|
+
let sessionModelMock: any;
|
48
|
+
let fileModelMock: any;
|
49
|
+
let knowledgeBaseModelMock: any;
|
50
|
+
let agentServiceMock: any;
|
51
|
+
|
52
|
+
beforeEach(() => {
|
53
|
+
vi.clearAllMocks();
|
54
|
+
|
55
|
+
agentModelMock = {
|
56
|
+
createAgentFiles: vi.fn(),
|
57
|
+
createAgentKnowledgeBase: vi.fn(),
|
58
|
+
deleteAgentFile: vi.fn(),
|
59
|
+
deleteAgentKnowledgeBase: vi.fn(),
|
60
|
+
findBySessionId: vi.fn(),
|
61
|
+
getAgentAssignedKnowledge: vi.fn(),
|
62
|
+
toggleFile: vi.fn(),
|
63
|
+
toggleKnowledgeBase: vi.fn(),
|
64
|
+
};
|
65
|
+
vi.mocked(AgentModel).mockImplementation(() => agentModelMock);
|
66
|
+
|
67
|
+
sessionModelMock = {
|
68
|
+
findByIdOrSlug: vi.fn(),
|
69
|
+
};
|
70
|
+
vi.mocked(SessionModel).mockImplementation(() => sessionModelMock);
|
71
|
+
|
72
|
+
fileModelMock = {
|
73
|
+
query: vi.fn(),
|
74
|
+
};
|
75
|
+
vi.mocked(FileModel).mockImplementation(() => fileModelMock);
|
76
|
+
|
77
|
+
knowledgeBaseModelMock = {
|
78
|
+
query: vi.fn(),
|
79
|
+
};
|
80
|
+
vi.mocked(KnowledgeBaseModel).mockImplementation(() => knowledgeBaseModelMock);
|
81
|
+
|
82
|
+
agentServiceMock = {
|
83
|
+
createInbox: vi.fn(),
|
84
|
+
};
|
85
|
+
vi.mocked(AgentService).mockImplementation(() => agentServiceMock);
|
86
|
+
|
87
|
+
mockCtx = {
|
88
|
+
userId,
|
89
|
+
agentModel: agentModelMock,
|
90
|
+
agentService: agentServiceMock,
|
91
|
+
fileModel: fileModelMock,
|
92
|
+
knowledgeBaseModel: knowledgeBaseModelMock,
|
93
|
+
sessionModel: sessionModelMock,
|
94
|
+
};
|
95
|
+
});
|
96
|
+
|
97
|
+
describe('getAgentConfig', () => {
|
98
|
+
it('should return default config if user not found when getting inbox config', async () => {
|
99
|
+
vi.mocked(UserModel.findById).mockResolvedValue(undefined);
|
100
|
+
sessionModelMock.findByIdOrSlug.mockResolvedValue(undefined);
|
101
|
+
|
102
|
+
const caller = agentRouter.createCaller(mockCtx);
|
103
|
+
const result = await caller.getAgentConfig({ sessionId: INBOX_SESSION_ID });
|
104
|
+
|
105
|
+
expect(result).toEqual(DEFAULT_AGENT_CONFIG);
|
106
|
+
});
|
107
|
+
|
108
|
+
it('should create inbox session if user exists but no inbox session', async () => {
|
109
|
+
const mockUser = { id: userId };
|
110
|
+
const mockSession = { id: 'inboxSessionId' };
|
111
|
+
|
112
|
+
vi.mocked(UserModel.findById).mockResolvedValue(mockUser as any);
|
113
|
+
sessionModelMock.findByIdOrSlug
|
114
|
+
.mockResolvedValueOnce(undefined)
|
115
|
+
.mockResolvedValueOnce(mockSession);
|
116
|
+
agentModelMock.findBySessionId.mockResolvedValue(DEFAULT_AGENT_CONFIG);
|
117
|
+
|
118
|
+
const caller = agentRouter.createCaller(mockCtx);
|
119
|
+
const result = await caller.getAgentConfig({ sessionId: INBOX_SESSION_ID });
|
120
|
+
|
121
|
+
expect(agentServiceMock.createInbox).toHaveBeenCalled();
|
122
|
+
expect(result).toEqual(DEFAULT_AGENT_CONFIG);
|
123
|
+
});
|
124
|
+
|
125
|
+
it('should find agent by session id if session exists', async () => {
|
126
|
+
const mockSession = { id: 'session1' };
|
127
|
+
sessionModelMock.findByIdOrSlug.mockResolvedValue(mockSession);
|
128
|
+
agentModelMock.findBySessionId.mockResolvedValue(DEFAULT_AGENT_CONFIG);
|
129
|
+
|
130
|
+
const caller = agentRouter.createCaller(mockCtx);
|
131
|
+
const result = await caller.getAgentConfig({ sessionId: 'session1' });
|
132
|
+
|
133
|
+
expect(agentModelMock.findBySessionId).toHaveBeenCalledWith('session1');
|
134
|
+
expect(result).toEqual(DEFAULT_AGENT_CONFIG);
|
135
|
+
});
|
136
|
+
});
|
137
|
+
|
138
|
+
describe('getKnowledgeBasesAndFiles', () => {
|
139
|
+
it('should return combined knowledge bases and files', async () => {
|
140
|
+
const mockFiles = [
|
141
|
+
{ id: 'file1', name: 'File 1', fileType: 'text' },
|
142
|
+
{ id: 'file2', name: 'File 2', fileType: 'pdf' },
|
143
|
+
];
|
144
|
+
|
145
|
+
const mockKnowledgeBases = [
|
146
|
+
{ id: 'kb1', name: 'KB 1', description: 'desc 1', avatar: 'avatar1' },
|
147
|
+
{ id: 'kb2', name: 'KB 2', description: 'desc 2', avatar: 'avatar2' },
|
148
|
+
];
|
149
|
+
|
150
|
+
const mockKnowledge = {
|
151
|
+
files: [{ id: 'file1', enabled: true }],
|
152
|
+
knowledgeBases: [{ id: 'kb1', enabled: true }],
|
153
|
+
};
|
154
|
+
|
155
|
+
fileModelMock.query.mockResolvedValue(mockFiles);
|
156
|
+
knowledgeBaseModelMock.query.mockResolvedValue(mockKnowledgeBases);
|
157
|
+
agentModelMock.getAgentAssignedKnowledge.mockResolvedValue(mockKnowledge);
|
158
|
+
|
159
|
+
const caller = agentRouter.createCaller(mockCtx);
|
160
|
+
const result = await caller.getKnowledgeBasesAndFiles({ agentId: 'agent1' });
|
161
|
+
|
162
|
+
expect(result).toEqual([
|
163
|
+
{
|
164
|
+
enabled: true,
|
165
|
+
fileType: 'text',
|
166
|
+
id: 'file1',
|
167
|
+
name: 'File 1',
|
168
|
+
type: KnowledgeType.File,
|
169
|
+
},
|
170
|
+
{
|
171
|
+
enabled: false,
|
172
|
+
fileType: 'pdf',
|
173
|
+
id: 'file2',
|
174
|
+
name: 'File 2',
|
175
|
+
type: KnowledgeType.File,
|
176
|
+
},
|
177
|
+
{
|
178
|
+
avatar: 'avatar1',
|
179
|
+
description: 'desc 1',
|
180
|
+
enabled: true,
|
181
|
+
id: 'kb1',
|
182
|
+
name: 'KB 1',
|
183
|
+
type: KnowledgeType.KnowledgeBase,
|
184
|
+
},
|
185
|
+
{
|
186
|
+
avatar: 'avatar2',
|
187
|
+
description: 'desc 2',
|
188
|
+
enabled: false,
|
189
|
+
id: 'kb2',
|
190
|
+
name: 'KB 2',
|
191
|
+
type: KnowledgeType.KnowledgeBase,
|
192
|
+
},
|
193
|
+
]);
|
194
|
+
});
|
195
|
+
});
|
196
|
+
|
197
|
+
describe('createAgentFiles', () => {
|
198
|
+
it('should create agent files', async () => {
|
199
|
+
const mockInput = {
|
200
|
+
agentId: 'agent1',
|
201
|
+
fileIds: ['file1', 'file2'],
|
202
|
+
enabled: true,
|
203
|
+
};
|
204
|
+
|
205
|
+
const caller = agentRouter.createCaller(mockCtx);
|
206
|
+
await caller.createAgentFiles(mockInput);
|
207
|
+
|
208
|
+
expect(agentModelMock.createAgentFiles).toHaveBeenCalledWith(
|
209
|
+
mockInput.agentId,
|
210
|
+
mockInput.fileIds,
|
211
|
+
mockInput.enabled,
|
212
|
+
);
|
213
|
+
});
|
214
|
+
});
|
215
|
+
|
216
|
+
describe('deleteAgentFile', () => {
|
217
|
+
it('should delete agent file', async () => {
|
218
|
+
const mockInput = {
|
219
|
+
agentId: 'agent1',
|
220
|
+
fileId: 'file1',
|
221
|
+
};
|
222
|
+
|
223
|
+
const caller = agentRouter.createCaller(mockCtx);
|
224
|
+
await caller.deleteAgentFile(mockInput);
|
225
|
+
|
226
|
+
expect(agentModelMock.deleteAgentFile).toHaveBeenCalledWith(
|
227
|
+
mockInput.agentId,
|
228
|
+
mockInput.fileId,
|
229
|
+
);
|
230
|
+
});
|
231
|
+
});
|
232
|
+
|
233
|
+
describe('toggleFile', () => {
|
234
|
+
it('should toggle file', async () => {
|
235
|
+
const mockInput = {
|
236
|
+
agentId: 'agent1',
|
237
|
+
fileId: 'file1',
|
238
|
+
enabled: true,
|
239
|
+
};
|
240
|
+
|
241
|
+
const caller = agentRouter.createCaller(mockCtx);
|
242
|
+
await caller.toggleFile(mockInput);
|
243
|
+
|
244
|
+
expect(agentModelMock.toggleFile).toHaveBeenCalledWith(
|
245
|
+
mockInput.agentId,
|
246
|
+
mockInput.fileId,
|
247
|
+
mockInput.enabled,
|
248
|
+
);
|
249
|
+
});
|
250
|
+
});
|
251
|
+
|
252
|
+
describe('createAgentKnowledgeBase', () => {
|
253
|
+
it('should create agent knowledge base', async () => {
|
254
|
+
const mockInput = {
|
255
|
+
agentId: 'agent1',
|
256
|
+
knowledgeBaseId: 'kb1',
|
257
|
+
enabled: true,
|
258
|
+
};
|
259
|
+
|
260
|
+
const caller = agentRouter.createCaller(mockCtx);
|
261
|
+
await caller.createAgentKnowledgeBase(mockInput);
|
262
|
+
|
263
|
+
expect(agentModelMock.createAgentKnowledgeBase).toHaveBeenCalledWith(
|
264
|
+
mockInput.agentId,
|
265
|
+
mockInput.knowledgeBaseId,
|
266
|
+
mockInput.enabled,
|
267
|
+
);
|
268
|
+
});
|
269
|
+
});
|
270
|
+
|
271
|
+
describe('deleteAgentKnowledgeBase', () => {
|
272
|
+
it('should delete agent knowledge base', async () => {
|
273
|
+
const mockInput = {
|
274
|
+
agentId: 'agent1',
|
275
|
+
knowledgeBaseId: 'kb1',
|
276
|
+
};
|
277
|
+
|
278
|
+
const caller = agentRouter.createCaller(mockCtx);
|
279
|
+
await caller.deleteAgentKnowledgeBase(mockInput);
|
280
|
+
|
281
|
+
expect(agentModelMock.deleteAgentKnowledgeBase).toHaveBeenCalledWith(
|
282
|
+
mockInput.agentId,
|
283
|
+
mockInput.knowledgeBaseId,
|
284
|
+
);
|
285
|
+
});
|
286
|
+
});
|
287
|
+
|
288
|
+
describe('toggleKnowledgeBase', () => {
|
289
|
+
it('should toggle knowledge base', async () => {
|
290
|
+
const mockInput = {
|
291
|
+
agentId: 'agent1',
|
292
|
+
knowledgeBaseId: 'kb1',
|
293
|
+
enabled: true,
|
294
|
+
};
|
295
|
+
|
296
|
+
const caller = agentRouter.createCaller(mockCtx);
|
297
|
+
await caller.toggleKnowledgeBase(mockInput);
|
298
|
+
|
299
|
+
expect(agentModelMock.toggleKnowledgeBase).toHaveBeenCalledWith(
|
300
|
+
mockInput.agentId,
|
301
|
+
mockInput.knowledgeBaseId,
|
302
|
+
mockInput.enabled,
|
303
|
+
);
|
304
|
+
});
|
305
|
+
});
|
306
|
+
});
|
@@ -10,6 +10,7 @@ import { SessionModel } from '@/database/server/models/session';
|
|
10
10
|
import { UserModel } from '@/database/server/models/user';
|
11
11
|
import { pino } from '@/libs/logger';
|
12
12
|
import { authedProcedure, router } from '@/libs/trpc';
|
13
|
+
import { AgentService } from '@/server/services/agent';
|
13
14
|
import { KnowledgeItem, KnowledgeType } from '@/types/knowledgeBase';
|
14
15
|
|
15
16
|
const agentProcedure = authedProcedure.use(async (opts) => {
|
@@ -18,6 +19,7 @@ const agentProcedure = authedProcedure.use(async (opts) => {
|
|
18
19
|
return opts.next({
|
19
20
|
ctx: {
|
20
21
|
agentModel: new AgentModel(serverDB, ctx.userId),
|
22
|
+
agentService: new AgentService(serverDB, ctx.userId),
|
21
23
|
fileModel: new FileModel(serverDB, ctx.userId),
|
22
24
|
knowledgeBaseModel: new KnowledgeBaseModel(serverDB, ctx.userId),
|
23
25
|
sessionModel: new SessionModel(serverDB, ctx.userId),
|
@@ -91,7 +93,7 @@ export const agentRouter = router({
|
|
91
93
|
const user = await UserModel.findById(serverDB, ctx.userId);
|
92
94
|
if (!user) return DEFAULT_AGENT_CONFIG;
|
93
95
|
|
94
|
-
const res = await ctx.
|
96
|
+
const res = await ctx.agentService.createInbox();
|
95
97
|
pino.info('create inbox session', res);
|
96
98
|
}
|
97
99
|
}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { SessionModel } from '@/database/server/models/session';
|
5
|
+
import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
|
6
|
+
|
7
|
+
import { AgentService } from './index';
|
8
|
+
|
9
|
+
vi.mock('@/config/app', () => ({
|
10
|
+
appEnv: {
|
11
|
+
DEFAULT_AGENT_CONFIG: 'model=gpt-4;temperature=0.7',
|
12
|
+
},
|
13
|
+
}));
|
14
|
+
|
15
|
+
vi.mock('@/server/globalConfig/parseDefaultAgent', () => ({
|
16
|
+
parseAgentConfig: vi.fn(),
|
17
|
+
}));
|
18
|
+
|
19
|
+
vi.mock('@/database/server/models/session', () => ({
|
20
|
+
SessionModel: vi.fn(),
|
21
|
+
}));
|
22
|
+
|
23
|
+
describe('AgentService', () => {
|
24
|
+
let service: AgentService;
|
25
|
+
const mockDb = {} as any;
|
26
|
+
const mockUserId = 'test-user-id';
|
27
|
+
|
28
|
+
beforeEach(() => {
|
29
|
+
vi.clearAllMocks();
|
30
|
+
service = new AgentService(mockDb, mockUserId);
|
31
|
+
});
|
32
|
+
|
33
|
+
describe('createInbox', () => {
|
34
|
+
it('should create inbox with default agent config', async () => {
|
35
|
+
const mockConfig = { model: 'gpt-4', temperature: 0.7 };
|
36
|
+
const mockSessionModel = {
|
37
|
+
createInbox: vi.fn(),
|
38
|
+
};
|
39
|
+
|
40
|
+
(SessionModel as any).mockImplementation(() => mockSessionModel);
|
41
|
+
(parseAgentConfig as any).mockReturnValue(mockConfig);
|
42
|
+
|
43
|
+
await service.createInbox();
|
44
|
+
|
45
|
+
expect(SessionModel).toHaveBeenCalledWith(mockDb, mockUserId);
|
46
|
+
expect(parseAgentConfig).toHaveBeenCalledWith('model=gpt-4;temperature=0.7');
|
47
|
+
expect(mockSessionModel.createInbox).toHaveBeenCalledWith(mockConfig);
|
48
|
+
});
|
49
|
+
|
50
|
+
it('should create inbox with empty config if parseAgentConfig returns undefined', async () => {
|
51
|
+
const mockSessionModel = {
|
52
|
+
createInbox: vi.fn(),
|
53
|
+
};
|
54
|
+
|
55
|
+
(SessionModel as any).mockImplementation(() => mockSessionModel);
|
56
|
+
(parseAgentConfig as any).mockReturnValue(undefined);
|
57
|
+
|
58
|
+
await service.createInbox();
|
59
|
+
|
60
|
+
expect(SessionModel).toHaveBeenCalledWith(mockDb, mockUserId);
|
61
|
+
expect(parseAgentConfig).toHaveBeenCalledWith('model=gpt-4;temperature=0.7');
|
62
|
+
expect(mockSessionModel.createInbox).toHaveBeenCalledWith({});
|
63
|
+
});
|
64
|
+
});
|
65
|
+
});
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { appEnv } from '@/config/app';
|
2
|
+
import { SessionModel } from '@/database/server/models/session';
|
3
|
+
import { LobeChatDatabase } from '@/database/type';
|
4
|
+
import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
|
5
|
+
|
6
|
+
export class AgentService {
|
7
|
+
private readonly userId: string;
|
8
|
+
private readonly db: LobeChatDatabase;
|
9
|
+
|
10
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
11
|
+
this.userId = userId;
|
12
|
+
this.db = db;
|
13
|
+
}
|
14
|
+
|
15
|
+
async createInbox() {
|
16
|
+
const sessionModel = new SessionModel(this.db, this.userId);
|
17
|
+
|
18
|
+
const defaultAgentConfig = parseAgentConfig(appEnv.DEFAULT_AGENT_CONFIG) || {};
|
19
|
+
|
20
|
+
await sessionModel.createInbox(defaultAgentConfig);
|
21
|
+
}
|
22
|
+
}
|
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { UserItem } from '@/database/schemas';
|
5
5
|
import { UserModel } from '@/database/server/models/user';
|
6
6
|
import { pino } from '@/libs/logger';
|
7
|
+
import { AgentService } from '@/server/services/agent';
|
7
8
|
|
8
9
|
import { UserService } from './index';
|
9
10
|
|
@@ -29,6 +30,12 @@ vi.mock('@/libs/logger', () => ({
|
|
29
30
|
},
|
30
31
|
}));
|
31
32
|
|
33
|
+
vi.mock('@/server/services/agent', () => ({
|
34
|
+
AgentService: vi.fn().mockImplementation(() => ({
|
35
|
+
createInbox: vi.fn().mockResolvedValue(undefined),
|
36
|
+
})),
|
37
|
+
}));
|
38
|
+
|
32
39
|
let service: UserService;
|
33
40
|
const mockUserId = 'test-user-id';
|
34
41
|
|
@@ -57,7 +64,7 @@ describe('UserService', () => {
|
|
57
64
|
// Mock user not found
|
58
65
|
vi.mocked(UserModel.findById).mockResolvedValue(null as any);
|
59
66
|
|
60
|
-
await service.createUser(mockUserId, mockUserJSON);
|
67
|
+
const result = await service.createUser(mockUserId, mockUserJSON);
|
61
68
|
|
62
69
|
expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
63
70
|
expect(UserModel.createUser).toHaveBeenCalledWith(
|
@@ -73,6 +80,12 @@ describe('UserService', () => {
|
|
73
80
|
clerkCreatedAt: new Date('2023-01-01T00:00:00Z'),
|
74
81
|
}),
|
75
82
|
);
|
83
|
+
expect(AgentService).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
84
|
+
expect(vi.mocked(AgentService).mock.results[0].value.createInbox).toHaveBeenCalled();
|
85
|
+
expect(result).toEqual({
|
86
|
+
message: 'user created',
|
87
|
+
success: true,
|
88
|
+
});
|
76
89
|
});
|
77
90
|
|
78
91
|
it('should not create user if already exists', async () => {
|
@@ -83,6 +96,7 @@ describe('UserService', () => {
|
|
83
96
|
|
84
97
|
expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
85
98
|
expect(UserModel.createUser).not.toHaveBeenCalled();
|
99
|
+
expect(AgentService).not.toHaveBeenCalled();
|
86
100
|
expect(result).toEqual({
|
87
101
|
message: 'user not created due to user already existing in the database',
|
88
102
|
success: false,
|
@@ -106,6 +120,8 @@ describe('UserService', () => {
|
|
106
120
|
phone: '+1234567890', // Should use first phone number
|
107
121
|
}),
|
108
122
|
);
|
123
|
+
expect(AgentService).toHaveBeenCalledWith(expect.anything(), mockUserId);
|
124
|
+
expect(vi.mocked(AgentService).mock.results[0].value.createInbox).toHaveBeenCalled();
|
109
125
|
});
|
110
126
|
});
|
111
127
|
|
@@ -4,6 +4,7 @@ import { serverDB } from '@/database/server';
|
|
4
4
|
import { UserModel } from '@/database/server/models/user';
|
5
5
|
import { pino } from '@/libs/logger';
|
6
6
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
7
|
+
import { AgentService } from '@/server/services/agent';
|
7
8
|
|
8
9
|
export class UserService {
|
9
10
|
createUser = async (id: string, params: UserJSON) => {
|
@@ -41,6 +42,10 @@ export class UserService {
|
|
41
42
|
username: params.username,
|
42
43
|
});
|
43
44
|
|
45
|
+
// 3. Create an inbox session for the user
|
46
|
+
const agentService = new AgentService(serverDB, id);
|
47
|
+
await agentService.createInbox();
|
48
|
+
|
44
49
|
/* ↓ cloud slot ↓ */
|
45
50
|
|
46
51
|
/* ↑ cloud slot ↑ */
|
@@ -52,6 +52,18 @@ export class ClientService extends BaseClientService implements ISessionService
|
|
52
52
|
};
|
53
53
|
|
54
54
|
getSessionConfig: ISessionService['getSessionConfig'] = async (id) => {
|
55
|
+
if (id === INBOX_SESSION_ID) {
|
56
|
+
const item = await this.sessionModel.findByIdOrSlug(INBOX_SESSION_ID);
|
57
|
+
|
58
|
+
// if there is no session for user, create one
|
59
|
+
if (!item) {
|
60
|
+
const defaultAgentConfig =
|
61
|
+
window.global_serverConfigStore.getState().serverConfig.defaultAgent?.config || {};
|
62
|
+
|
63
|
+
await this.sessionModel.createInbox(defaultAgentConfig);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
55
67
|
const res = await this.sessionModel.findByIdOrSlug(id);
|
56
68
|
|
57
69
|
if (!res) throw new Error('Session not found');
|