@lobehub/chat 0.147.21 → 0.148.0
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 +42 -0
- package/locales/ar/setting.json +4 -0
- package/locales/bg-BG/setting.json +4 -0
- package/locales/de-DE/setting.json +4 -0
- package/locales/en-US/setting.json +4 -0
- package/locales/es-ES/setting.json +4 -0
- package/locales/fr-FR/setting.json +4 -0
- package/locales/it-IT/setting.json +4 -0
- package/locales/ja-JP/setting.json +4 -0
- package/locales/ko-KR/setting.json +4 -0
- package/locales/nl-NL/setting.json +4 -0
- package/locales/pl-PL/setting.json +4 -0
- package/locales/pt-BR/setting.json +4 -0
- package/locales/ru-RU/setting.json +4 -0
- package/locales/tr-TR/setting.json +4 -0
- package/locales/vi-VN/setting.json +4 -0
- package/locales/zh-CN/setting.json +4 -0
- package/locales/zh-TW/setting.json +4 -0
- package/package.json +3 -2
- package/public/favicon-32x32.ico +0 -0
- package/public/favicon.ico +0 -0
- package/public/icons/apple-touch-icon.png +0 -0
- package/src/app/api/chat/[provider]/route.test.ts +5 -7
- package/src/app/api/chat/[provider]/route.ts +13 -7
- package/src/app/api/chat/agentRuntime.test.ts +195 -451
- package/src/app/api/chat/agentRuntime.ts +197 -280
- package/src/app/api/chat/models/[provider]/route.ts +2 -2
- package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +2 -2
- package/src/app/metadata.ts +3 -5
- package/src/app/settings/llm/components/ProviderConfig/index.tsx +23 -1
- package/src/app/settings/llm/index.tsx +2 -2
- package/src/app/settings/llm/page.tsx +1 -5
- package/src/features/ChatInput/Topic/index.tsx +6 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +8 -3
- package/src/libs/agent-runtime/AgentRuntime.test.ts +400 -0
- package/src/libs/agent-runtime/AgentRuntime.ts +192 -0
- package/src/libs/agent-runtime/index.ts +1 -0
- package/src/libs/swr/index.ts +9 -0
- package/src/locales/default/setting.ts +4 -0
- package/src/services/__tests__/chat.test.ts +287 -1
- package/src/services/chat.ts +148 -2
- package/src/store/chat/slices/message/action.ts +80 -42
- package/src/store/chat/slices/message/initialState.ts +1 -1
- package/src/store/chat/slices/message/reducer.ts +32 -1
- package/src/store/chat/slices/topic/action.test.ts +25 -2
- package/src/store/chat/slices/topic/action.ts +24 -7
- package/src/store/chat/slices/topic/reducer.test.ts +141 -0
- package/src/store/chat/slices/topic/reducer.ts +67 -0
- package/src/store/global/slices/settings/selectors/modelConfig.ts +13 -0
- package/src/store/session/slices/session/action.ts +4 -5
- package/src/types/settings/modelProvider.ts +4 -0
- package/vercel.json +1 -1
|
@@ -19,6 +19,9 @@ import ActionsBar from './ActionsBar';
|
|
|
19
19
|
import HistoryDivider from './HistoryDivider';
|
|
20
20
|
|
|
21
21
|
const useStyles = createStyles(({ css, prefixCls }) => ({
|
|
22
|
+
loading: css`
|
|
23
|
+
opacity: 0.6;
|
|
24
|
+
`,
|
|
22
25
|
message: css`
|
|
23
26
|
// prevent the textarea too long
|
|
24
27
|
.${prefixCls}-input {
|
|
@@ -35,7 +38,7 @@ export interface ChatListItemProps {
|
|
|
35
38
|
const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
36
39
|
const fontSize = useGlobalStore((s) => settingsSelectors.currentSettings(s).fontSize);
|
|
37
40
|
const { t } = useTranslation('common');
|
|
38
|
-
const { styles } = useStyles();
|
|
41
|
+
const { styles, cx } = useStyles();
|
|
39
42
|
const [editing, setEditing] = useState(false);
|
|
40
43
|
const [type = 'chat'] = useSessionStore((s) => {
|
|
41
44
|
const config = agentSelectors.currentAgentConfig(s);
|
|
@@ -54,10 +57,12 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
|
54
57
|
const historyLength = useChatStore((s) => chatSelectors.currentChats(s).length);
|
|
55
58
|
|
|
56
59
|
const [loading, updateMessageContent] = useChatStore((s) => [
|
|
57
|
-
s.chatLoadingId === id,
|
|
60
|
+
s.chatLoadingId === id || s.messageLoadingIds.includes(id),
|
|
58
61
|
s.modifyMessageContent,
|
|
59
62
|
]);
|
|
60
63
|
|
|
64
|
+
const [isMessageLoading] = useChatStore((s) => [s.messageLoadingIds.includes(id)]);
|
|
65
|
+
|
|
61
66
|
const onAvatarsClick = useAvatarsClick();
|
|
62
67
|
|
|
63
68
|
const RenderMessage = useCallback(
|
|
@@ -110,7 +115,7 @@ const Item = memo<ChatListItemProps>(({ index, id }) => {
|
|
|
110
115
|
<ChatItem
|
|
111
116
|
actions={<ActionsBar index={index} setEditing={setEditing} />}
|
|
112
117
|
avatar={item.meta}
|
|
113
|
-
className={styles.message}
|
|
118
|
+
className={cx(styles.message, isMessageLoading && styles.loading)}
|
|
114
119
|
editing={editing}
|
|
115
120
|
error={error}
|
|
116
121
|
errorMessage={<ErrorMessageExtra data={item} />}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { Langfuse } from 'langfuse';
|
|
3
|
+
import { LangfuseGenerationClient, LangfuseTraceClient } from 'langfuse-core';
|
|
4
|
+
import { ClientOptions } from 'openai';
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createTraceOptions } from '@/app/api/chat/agentRuntime';
|
|
8
|
+
import { getServerConfig } from '@/config/server';
|
|
9
|
+
import { JWTPayload } from '@/const/auth';
|
|
10
|
+
import { TraceNameMap } from '@/const/trace';
|
|
11
|
+
import {
|
|
12
|
+
AgentRuntime,
|
|
13
|
+
ChatStreamPayload,
|
|
14
|
+
LobeAnthropicAI,
|
|
15
|
+
LobeAzureOpenAI,
|
|
16
|
+
LobeBedrockAI,
|
|
17
|
+
LobeGoogleAI,
|
|
18
|
+
LobeMistralAI,
|
|
19
|
+
LobeMoonshotAI,
|
|
20
|
+
LobeOllamaAI,
|
|
21
|
+
LobeOpenAI,
|
|
22
|
+
LobeOpenRouterAI,
|
|
23
|
+
LobePerplexityAI,
|
|
24
|
+
LobeRuntimeAI,
|
|
25
|
+
LobeTogetherAI,
|
|
26
|
+
LobeZhipuAI,
|
|
27
|
+
ModelProvider,
|
|
28
|
+
} from '@/libs/agent-runtime';
|
|
29
|
+
|
|
30
|
+
import { AgentChatOptions } from './AgentRuntime';
|
|
31
|
+
import { LobeBedrockAIParams } from './bedrock';
|
|
32
|
+
|
|
33
|
+
// 模拟依赖项
|
|
34
|
+
vi.mock('@/config/server', () => ({
|
|
35
|
+
getServerConfig: vi.fn(() => ({
|
|
36
|
+
// 确保为每个provider提供必要的配置信息
|
|
37
|
+
OPENAI_API_KEY: 'test-openai-key',
|
|
38
|
+
GOOGLE_API_KEY: 'test-google-key',
|
|
39
|
+
|
|
40
|
+
AZURE_API_KEY: 'test-azure-key',
|
|
41
|
+
AZURE_ENDPOINT: 'endpoint',
|
|
42
|
+
|
|
43
|
+
ZHIPU_API_KEY: 'test.zhipu-key',
|
|
44
|
+
MOONSHOT_API_KEY: 'test-moonshot-key',
|
|
45
|
+
AWS_SECRET_ACCESS_KEY: 'test-aws-secret',
|
|
46
|
+
AWS_ACCESS_KEY_ID: 'test-aws-id',
|
|
47
|
+
AWS_REGION: 'test-aws-region',
|
|
48
|
+
OLLAMA_PROXY_URL: 'test-ollama-url',
|
|
49
|
+
PERPLEXITY_API_KEY: 'test-perplexity-key',
|
|
50
|
+
ANTHROPIC_API_KEY: 'test-anthropic-key',
|
|
51
|
+
MISTRAL_API_KEY: 'test-mistral-key',
|
|
52
|
+
OPENROUTER_API_KEY: 'test-openrouter-key',
|
|
53
|
+
TOGETHERAI_API_KEY: 'test-togetherai-key',
|
|
54
|
+
})),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
describe('AgentRuntime', () => {
|
|
58
|
+
describe('should initialize with various providers', () => {
|
|
59
|
+
describe('OpenAI provider', () => {
|
|
60
|
+
it('should initialize correctly', async () => {
|
|
61
|
+
const jwtPayload: ClientOptions = { apiKey: 'user-openai-key', baseURL: 'user-endpoint' };
|
|
62
|
+
|
|
63
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.OpenAI, {
|
|
64
|
+
openai: jwtPayload,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(runtime).toBeInstanceOf(AgentRuntime);
|
|
68
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
|
|
69
|
+
expect(runtime['_runtime'].baseURL).toBe('user-endpoint');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('Azure OpenAI provider', () => {
|
|
74
|
+
it('should initialize correctly', async () => {
|
|
75
|
+
const jwtPayload = {
|
|
76
|
+
apikey: 'user-azure-key',
|
|
77
|
+
endpoint: 'user-azure-endpoint',
|
|
78
|
+
apiVersion: '2024-02-01',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Azure, {
|
|
82
|
+
azure: jwtPayload,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(runtime).toBeInstanceOf(AgentRuntime);
|
|
86
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeAzureOpenAI);
|
|
87
|
+
expect(runtime['_runtime'].baseURL).toBe('user-azure-endpoint');
|
|
88
|
+
});
|
|
89
|
+
it('should initialize with azureOpenAIParams correctly', async () => {
|
|
90
|
+
const jwtPayload = {
|
|
91
|
+
apikey: 'user-openai-key',
|
|
92
|
+
endpoint: 'user-endpoint',
|
|
93
|
+
apiVersion: 'custom-version',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Azure, {
|
|
97
|
+
azure: jwtPayload,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(runtime).toBeInstanceOf(AgentRuntime);
|
|
101
|
+
const openAIRuntime = runtime['_runtime'] as LobeRuntimeAI;
|
|
102
|
+
expect(openAIRuntime).toBeInstanceOf(LobeAzureOpenAI);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should initialize with AzureAI correctly', async () => {
|
|
106
|
+
const jwtPayload = {
|
|
107
|
+
apikey: 'user-azure-key',
|
|
108
|
+
endpoint: 'user-azure-endpoint',
|
|
109
|
+
};
|
|
110
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Azure, {
|
|
111
|
+
azure: jwtPayload,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeAzureOpenAI);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('ZhiPu AI provider', () => {
|
|
119
|
+
it('should initialize correctly', async () => {
|
|
120
|
+
const jwtPayload: JWTPayload = { apiKey: 'zhipu.user-key' };
|
|
121
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.ZhiPu, {
|
|
122
|
+
zhipu: jwtPayload,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 假设 LobeZhipuAI 是 ZhiPu 提供者的实现类
|
|
126
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeZhipuAI);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('Google provider', () => {
|
|
131
|
+
it('should initialize correctly', async () => {
|
|
132
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-google-key' };
|
|
133
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Google, {
|
|
134
|
+
google: jwtPayload,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 假设 LobeGoogleAI 是 Google 提供者的实现类
|
|
138
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeGoogleAI);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Moonshot AI provider', () => {
|
|
143
|
+
it('should initialize correctly', async () => {
|
|
144
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-moonshot-key' };
|
|
145
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Moonshot, {
|
|
146
|
+
moonshot: jwtPayload,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 假设 LobeMoonshotAI 是 Moonshot 提供者的实现类
|
|
150
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeMoonshotAI);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('Bedrock AI provider', () => {
|
|
155
|
+
it('should initialize correctly with payload apiKey', async () => {
|
|
156
|
+
const jwtPayload: LobeBedrockAIParams = {
|
|
157
|
+
accessKeyId: 'user-aws-id',
|
|
158
|
+
accessKeySecret: 'user-aws-secret',
|
|
159
|
+
region: 'user-aws-region',
|
|
160
|
+
};
|
|
161
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Bedrock, {
|
|
162
|
+
bedrock: jwtPayload,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 假设 LobeBedrockAI 是 Bedrock 提供者的实现类
|
|
166
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeBedrockAI);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('Ollama provider', () => {
|
|
171
|
+
it('should initialize correctly', async () => {
|
|
172
|
+
const jwtPayload: JWTPayload = { endpoint: 'user-ollama-url' };
|
|
173
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Ollama, {
|
|
174
|
+
ollama: jwtPayload,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 假设 LobeOllamaAI 是 Ollama 提供者的实现类
|
|
178
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeOllamaAI);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('Perplexity AI provider', () => {
|
|
183
|
+
it('should initialize correctly', async () => {
|
|
184
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-perplexity-key' };
|
|
185
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Perplexity, {
|
|
186
|
+
perplexity: jwtPayload,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// 假设 LobePerplexityAI 是 Perplexity 提供者的实现类
|
|
190
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobePerplexityAI);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Anthropic AI provider', () => {
|
|
195
|
+
it('should initialize correctly', async () => {
|
|
196
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-anthropic-key' };
|
|
197
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Anthropic, {
|
|
198
|
+
anthropic: jwtPayload,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 假设 LobeAnthropicAI 是 Anthropic 提供者的实现类
|
|
202
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeAnthropicAI);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('Mistral AI provider', () => {
|
|
207
|
+
it('should initialize correctly', async () => {
|
|
208
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-mistral-key' };
|
|
209
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.Mistral, {
|
|
210
|
+
mistral: jwtPayload,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 假设 LobeMistralAI 是 Mistral 提供者的实现类
|
|
214
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeMistralAI);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('OpenRouter AI provider', () => {
|
|
219
|
+
it('should initialize correctly', async () => {
|
|
220
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-openrouter-key' };
|
|
221
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.OpenRouter, {
|
|
222
|
+
openrouter: jwtPayload,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 假设 LobeOpenRouterAI 是 OpenRouter 提供者的实现类
|
|
226
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeOpenRouterAI);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Together AI provider', () => {
|
|
231
|
+
it('should initialize correctly', async () => {
|
|
232
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-togetherai-key' };
|
|
233
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.TogetherAI, {
|
|
234
|
+
togetherai: jwtPayload,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 假设 LobeTogetherAI 是 TogetherAI 提供者的实现类
|
|
238
|
+
expect(runtime['_runtime']).toBeInstanceOf(LobeTogetherAI);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('AgentRuntime chat method', () => {
|
|
244
|
+
it('should run correctly', async () => {
|
|
245
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-openai-key', endpoint: 'user-endpoint' };
|
|
246
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.OpenAI, {
|
|
247
|
+
openai: jwtPayload,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const payload: ChatStreamPayload = {
|
|
251
|
+
messages: [{ role: 'user', content: 'Hello, world!' }],
|
|
252
|
+
model: 'text-davinci-002',
|
|
253
|
+
temperature: 0,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
vi.spyOn(LobeOpenAI.prototype, 'chat').mockResolvedValue(new Response(''));
|
|
257
|
+
|
|
258
|
+
await runtime.chat(payload);
|
|
259
|
+
});
|
|
260
|
+
it('should handle options correctly', async () => {
|
|
261
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-openai-key', endpoint: 'user-endpoint' };
|
|
262
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.OpenAI, {
|
|
263
|
+
openai: jwtPayload,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const payload: ChatStreamPayload = {
|
|
267
|
+
messages: [{ role: 'user', content: 'Hello, world!' }],
|
|
268
|
+
model: 'text-davinci-002',
|
|
269
|
+
temperature: 0,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const options: AgentChatOptions = {
|
|
273
|
+
provider: 'openai',
|
|
274
|
+
trace: {
|
|
275
|
+
traceId: 'test-trace-id',
|
|
276
|
+
traceName: TraceNameMap.Conversation,
|
|
277
|
+
sessionId: 'test-session-id',
|
|
278
|
+
topicId: 'test-topic-id',
|
|
279
|
+
tags: [],
|
|
280
|
+
userId: 'test-user-id',
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
vi.spyOn(LobeOpenAI.prototype, 'chat').mockResolvedValue(new Response(''));
|
|
285
|
+
|
|
286
|
+
await runtime.chat(payload, createTraceOptions(payload, options));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('callback', async () => {
|
|
290
|
+
const jwtPayload: JWTPayload = { apiKey: 'user-openai-key', endpoint: 'user-endpoint' };
|
|
291
|
+
const runtime = await AgentRuntime.initializeWithProviderOptions(ModelProvider.OpenAI, {
|
|
292
|
+
openai: jwtPayload,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const payload: ChatStreamPayload = {
|
|
296
|
+
messages: [{ role: 'user', content: 'Hello, world!' }],
|
|
297
|
+
model: 'text-davinci-002',
|
|
298
|
+
temperature: 0,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const options: AgentChatOptions = {
|
|
302
|
+
provider: 'openai',
|
|
303
|
+
trace: {
|
|
304
|
+
traceId: 'test-trace-id',
|
|
305
|
+
traceName: TraceNameMap.Conversation,
|
|
306
|
+
sessionId: 'test-session-id',
|
|
307
|
+
topicId: 'test-topic-id',
|
|
308
|
+
tags: [],
|
|
309
|
+
userId: 'test-user-id',
|
|
310
|
+
},
|
|
311
|
+
enableTrace: true,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const updateMock = vi.fn();
|
|
315
|
+
beforeEach(() => {
|
|
316
|
+
vi.mocked(getServerConfig).mockReturnValue({
|
|
317
|
+
ENABLE_LANGFUSE: true,
|
|
318
|
+
LANGFUSE_PUBLIC_KEY: 'abc',
|
|
319
|
+
LANGFUSE_SECRET_KEY: 'DDD',
|
|
320
|
+
} as any);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should call experimental_onToolCall correctly', async () => {
|
|
324
|
+
// 使用 spyOn 模拟 chat 方法
|
|
325
|
+
vi.spyOn(LobeOpenAI.prototype, 'chat').mockImplementation(
|
|
326
|
+
async (payload, { callback }: any) => {
|
|
327
|
+
// 模拟 experimental_onToolCall 回调的触发
|
|
328
|
+
if (callback?.experimental_onToolCall) {
|
|
329
|
+
await callback.experimental_onToolCall();
|
|
330
|
+
}
|
|
331
|
+
return new Response('abc');
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
vi.spyOn(LangfuseTraceClient.prototype, 'update').mockImplementation(updateMock);
|
|
335
|
+
|
|
336
|
+
await runtime.chat(payload, createTraceOptions(payload, options));
|
|
337
|
+
|
|
338
|
+
expect(updateMock).toHaveBeenCalledWith({ tags: ['Tools Call'] });
|
|
339
|
+
});
|
|
340
|
+
it('should call onStart correctly', async () => {
|
|
341
|
+
vi.spyOn(LangfuseGenerationClient.prototype, 'update').mockImplementation(updateMock);
|
|
342
|
+
vi.spyOn(LobeOpenAI.prototype, 'chat').mockImplementation(
|
|
343
|
+
async (payload, { callback }: any) => {
|
|
344
|
+
if (callback?.onStart) {
|
|
345
|
+
callback.onStart();
|
|
346
|
+
}
|
|
347
|
+
return new Response('Success');
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
await runtime.chat(payload, createTraceOptions(payload, options));
|
|
352
|
+
|
|
353
|
+
// Verify onStart was called
|
|
354
|
+
expect(updateMock).toHaveBeenCalledWith({ completionStartTime: expect.any(Date) });
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should call onCompletion correctly', async () => {
|
|
358
|
+
// Spy on the chat method and trigger onCompletion callback
|
|
359
|
+
vi.spyOn(LangfuseGenerationClient.prototype, 'update').mockImplementation(updateMock);
|
|
360
|
+
vi.spyOn(LobeOpenAI.prototype, 'chat').mockImplementation(
|
|
361
|
+
async (payload, { callback }: any) => {
|
|
362
|
+
if (callback?.onCompletion) {
|
|
363
|
+
await callback.onCompletion('Test completion');
|
|
364
|
+
}
|
|
365
|
+
return new Response('Success');
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
await runtime.chat(payload, createTraceOptions(payload, options));
|
|
370
|
+
|
|
371
|
+
// Verify onCompletion was called with expected output
|
|
372
|
+
expect(updateMock).toHaveBeenCalledWith({
|
|
373
|
+
endTime: expect.any(Date),
|
|
374
|
+
metadata: {
|
|
375
|
+
provider: 'openai',
|
|
376
|
+
tools: undefined,
|
|
377
|
+
},
|
|
378
|
+
output: 'Test completion',
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
it('should call onFinal correctly', async () => {
|
|
382
|
+
vi.spyOn(LobeOpenAI.prototype, 'chat').mockImplementation(
|
|
383
|
+
async (payload, { callback }: any) => {
|
|
384
|
+
if (callback?.onFinal) {
|
|
385
|
+
await callback.onFinal('Test completion');
|
|
386
|
+
}
|
|
387
|
+
return new Response('Success');
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
const shutdownAsyncMock = vi.fn();
|
|
391
|
+
vi.spyOn(Langfuse.prototype, 'shutdownAsync').mockImplementation(shutdownAsyncMock);
|
|
392
|
+
|
|
393
|
+
await runtime.chat(payload, createTraceOptions(payload, options));
|
|
394
|
+
|
|
395
|
+
// Verify onCompletion was called with expected output
|
|
396
|
+
expect(shutdownAsyncMock).toHaveBeenCalled();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { ClientOptions } from 'openai';
|
|
2
|
+
|
|
3
|
+
import type { TracePayload } from '@/const/trace';
|
|
4
|
+
|
|
5
|
+
import { LobeRuntimeAI } from './BaseAI';
|
|
6
|
+
import { LobeAnthropicAI } from './anthropic';
|
|
7
|
+
import { LobeAzureOpenAI } from './azureOpenai';
|
|
8
|
+
import { LobeBedrockAI, LobeBedrockAIParams } from './bedrock';
|
|
9
|
+
import { LobeGoogleAI } from './google';
|
|
10
|
+
import { LobeGroq } from './groq';
|
|
11
|
+
import { LobeMistralAI } from './mistral';
|
|
12
|
+
import { LobeMoonshotAI } from './moonshot';
|
|
13
|
+
import { LobeOllamaAI } from './ollama';
|
|
14
|
+
import { LobeOpenAI } from './openai';
|
|
15
|
+
import { LobeOpenRouterAI } from './openrouter';
|
|
16
|
+
import { LobePerplexityAI } from './perplexity';
|
|
17
|
+
import { LobeTogetherAI } from './togetherai';
|
|
18
|
+
import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from './types';
|
|
19
|
+
import { LobeZeroOneAI } from './zeroone';
|
|
20
|
+
import { LobeZhipuAI } from './zhipu';
|
|
21
|
+
|
|
22
|
+
export interface AgentChatOptions {
|
|
23
|
+
enableTrace?: boolean;
|
|
24
|
+
provider: string;
|
|
25
|
+
trace?: TracePayload;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class AgentRuntime {
|
|
29
|
+
private _runtime: LobeRuntimeAI;
|
|
30
|
+
|
|
31
|
+
constructor(runtime: LobeRuntimeAI) {
|
|
32
|
+
this._runtime = runtime;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initiates a chat session with the agent.
|
|
37
|
+
*
|
|
38
|
+
* @param payload - The payload containing the chat stream data.
|
|
39
|
+
* @param options - Optional chat competition options.
|
|
40
|
+
* @returns A Promise that resolves to the chat response.
|
|
41
|
+
*
|
|
42
|
+
* @example - Use without trace
|
|
43
|
+
* ```ts
|
|
44
|
+
* const agentRuntime = await initializeWithClientStore(provider, payload);
|
|
45
|
+
* const data = payload as ChatStreamPayload;
|
|
46
|
+
* return await agentRuntime.chat(data);
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example - Use Langfuse trace
|
|
50
|
+
* ```ts
|
|
51
|
+
* // ============ 1. init chat model ============ //
|
|
52
|
+
* const agentRuntime = await initAgentRuntimeWithUserPayload(provider, jwtPayload);
|
|
53
|
+
* // ============ 2. create chat completion ============ //
|
|
54
|
+
* const data = {
|
|
55
|
+
* // your trace options here
|
|
56
|
+
* } as ChatStreamPayload;
|
|
57
|
+
* const tracePayload = getTracePayload(req);
|
|
58
|
+
* return await agentRuntime.chat(data, createTraceOptions(data, {
|
|
59
|
+
* provider,
|
|
60
|
+
* trace: tracePayload,
|
|
61
|
+
* }));
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
|
|
65
|
+
return this._runtime.chat(payload, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async models() {
|
|
69
|
+
return this._runtime.models?.();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @description Initialize the runtime with the provider and the options
|
|
74
|
+
* @param provider choose a model provider
|
|
75
|
+
* @param params options of the choosed provider
|
|
76
|
+
* @returns the runtime instance
|
|
77
|
+
* Try to initialize the runtime with the provider and the options.
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const runtime = await AgentRuntime.initializeWithProviderOptions(provider, {
|
|
81
|
+
* [provider]: {...options},
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
84
|
+
* **Note**: If you try to get a AgentRuntime instance from client or server,
|
|
85
|
+
* you should use the methods to get the runtime instance at first.
|
|
86
|
+
* - `src/app/api/chat/agentRuntime.ts: initAgentRuntimeWithUserPayload` on server
|
|
87
|
+
* - `src/services/chat.ts: initializeWithClientStore` on client
|
|
88
|
+
*/
|
|
89
|
+
static async initializeWithProviderOptions(
|
|
90
|
+
provider: string,
|
|
91
|
+
params: Partial<{
|
|
92
|
+
anthropic: Partial<ClientOptions>;
|
|
93
|
+
azure: { apiVersion?: string; apikey?: string; endpoint?: string };
|
|
94
|
+
bedrock: Partial<LobeBedrockAIParams>;
|
|
95
|
+
google: { apiKey?: string; baseURL?: string };
|
|
96
|
+
groq: Partial<ClientOptions>;
|
|
97
|
+
mistral: Partial<ClientOptions>;
|
|
98
|
+
moonshot: Partial<ClientOptions>;
|
|
99
|
+
ollama: Partial<ClientOptions>;
|
|
100
|
+
openai: Partial<ClientOptions>;
|
|
101
|
+
openrouter: Partial<ClientOptions>;
|
|
102
|
+
perplexity: Partial<ClientOptions>;
|
|
103
|
+
togetherai: Partial<ClientOptions>;
|
|
104
|
+
zeroone: Partial<ClientOptions>;
|
|
105
|
+
zhipu: Partial<ClientOptions>;
|
|
106
|
+
}>,
|
|
107
|
+
) {
|
|
108
|
+
let runtimeModel: LobeRuntimeAI;
|
|
109
|
+
|
|
110
|
+
switch (provider) {
|
|
111
|
+
default:
|
|
112
|
+
case ModelProvider.OpenAI: {
|
|
113
|
+
// Will use the openai as default provider
|
|
114
|
+
runtimeModel = new LobeOpenAI(params.openai ?? (params as any)[provider]);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case ModelProvider.Azure: {
|
|
119
|
+
runtimeModel = new LobeAzureOpenAI(
|
|
120
|
+
params.azure?.endpoint,
|
|
121
|
+
params.azure?.apikey,
|
|
122
|
+
params.azure?.apiVersion,
|
|
123
|
+
);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case ModelProvider.ZhiPu: {
|
|
128
|
+
runtimeModel = await LobeZhipuAI.fromAPIKey(params.zhipu ?? {});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case ModelProvider.Google: {
|
|
133
|
+
runtimeModel = new LobeGoogleAI(params.google ?? {});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case ModelProvider.Moonshot: {
|
|
138
|
+
runtimeModel = new LobeMoonshotAI(params.moonshot ?? {});
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case ModelProvider.Bedrock: {
|
|
143
|
+
runtimeModel = new LobeBedrockAI(params.bedrock ?? {});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case ModelProvider.Ollama: {
|
|
148
|
+
runtimeModel = new LobeOllamaAI(params.ollama ?? {});
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case ModelProvider.Perplexity: {
|
|
153
|
+
runtimeModel = new LobePerplexityAI(params.perplexity ?? {});
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case ModelProvider.Anthropic: {
|
|
158
|
+
runtimeModel = new LobeAnthropicAI(params.anthropic ?? {});
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case ModelProvider.Mistral: {
|
|
163
|
+
runtimeModel = new LobeMistralAI(params.mistral ?? {});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case ModelProvider.Groq: {
|
|
168
|
+
runtimeModel = new LobeGroq(params.groq ?? {});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case ModelProvider.OpenRouter: {
|
|
173
|
+
runtimeModel = new LobeOpenRouterAI(params.openrouter ?? {});
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case ModelProvider.TogetherAI: {
|
|
178
|
+
runtimeModel = new LobeTogetherAI(params.togetherai ?? {});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case ModelProvider.ZeroOne: {
|
|
183
|
+
runtimeModel = new LobeZeroOneAI(params.zeroone ?? {});
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return new AgentRuntime(runtimeModel);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default AgentRuntime;
|
package/src/libs/swr/index.ts
CHANGED
|
@@ -32,3 +32,12 @@ export const useActionSWR: SWRHook = (key, fetch, config) =>
|
|
|
32
32
|
revalidateOnReconnect: false,
|
|
33
33
|
...config,
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
export interface SWRRefreshParams<T, A = (...args: any[]) => any> {
|
|
37
|
+
action: A;
|
|
38
|
+
optimisticData?: (data: T | undefined) => T;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SWRefreshMethod<T> = <A extends (...args: any[]) => Promise<any>>(
|
|
42
|
+
params?: SWRRefreshParams<T, A>,
|
|
43
|
+
) => ReturnType<A>;
|