@lobehub/chat 0.134.1 → 0.135.1

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.
@@ -2,15 +2,23 @@ import {
2
2
  BedrockRuntimeClient,
3
3
  InvokeModelWithResponseStreamCommand,
4
4
  } from '@aws-sdk/client-bedrock-runtime';
5
- import { AWSBedrockAnthropicStream, AWSBedrockLlama2Stream, StreamingTextResponse } from 'ai';
6
- import { experimental_buildAnthropicPrompt, experimental_buildLlama2Prompt } from 'ai/prompts';
5
+ import {
6
+ AWSBedrockLlama2Stream,
7
+ AWSBedrockStream,
8
+ StreamingTextResponse
9
+ } from 'ai';
10
+ import { experimental_buildLlama2Prompt } from 'ai/prompts';
7
11
 
8
12
  import { LobeRuntimeAI } from '../BaseAI';
9
13
  import { AgentRuntimeErrorType } from '../error';
10
- import { ChatStreamPayload, ModelProvider } from '../types';
14
+ import {
15
+ ChatCompetitionOptions,
16
+ ChatStreamPayload,
17
+ ModelProvider,
18
+ } from '../types';
11
19
  import { AgentRuntimeError } from '../utils/createError';
12
20
  import { debugStream } from '../utils/debugStream';
13
- import { DEBUG_CHAT_COMPLETION } from '../utils/env';
21
+ import { buildAnthropicMessages } from '../utils/anthropicHelpers';
14
22
 
15
23
  export interface LobeBedrockAIParams {
16
24
  accessKeyId?: string;
@@ -38,23 +46,32 @@ export class LobeBedrockAI implements LobeRuntimeAI {
38
46
  });
39
47
  }
40
48
 
41
- async chat(payload: ChatStreamPayload) {
49
+ async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) {
42
50
  if (payload.model.startsWith('meta')) return this.invokeLlamaModel(payload);
43
51
 
44
- return this.invokeClaudeModel(payload);
52
+ return this.invokeClaudeModel(payload, options);
45
53
  }
46
54
 
47
55
  private invokeClaudeModel = async (
48
56
  payload: ChatStreamPayload,
57
+ options?: ChatCompetitionOptions
49
58
  ): Promise<StreamingTextResponse> => {
59
+ const { max_tokens, messages, model, temperature, top_p } = payload;
60
+ const system_message = messages.find((m) => m.role === 'system');
61
+ const user_messages = messages.filter((m) => m.role !== 'system');
62
+
50
63
  const command = new InvokeModelWithResponseStreamCommand({
51
64
  accept: 'application/json',
52
65
  body: JSON.stringify({
53
- max_tokens_to_sample: payload.max_tokens || 400,
54
- prompt: experimental_buildAnthropicPrompt(payload.messages as any),
66
+ anthropic_version: "bedrock-2023-05-31",
67
+ max_tokens: max_tokens || 4096,
68
+ messages: buildAnthropicMessages(user_messages),
69
+ system: system_message?.content as string,
70
+ temperature: temperature,
71
+ top_p: top_p,
55
72
  }),
56
73
  contentType: 'application/json',
57
- modelId: payload.model,
74
+ modelId: model,
58
75
  });
59
76
 
60
77
  try {
@@ -62,11 +79,11 @@ export class LobeBedrockAI implements LobeRuntimeAI {
62
79
  const bedrockResponse = await this.client.send(command);
63
80
 
64
81
  // Convert the response into a friendly text-stream
65
- const stream = AWSBedrockAnthropicStream(bedrockResponse);
82
+ const stream = AWSBedrockStream(bedrockResponse, options?.callback, (chunk) => chunk.delta?.text);
66
83
 
67
84
  const [debug, output] = stream.tee();
68
85
 
69
- if (DEBUG_CHAT_COMPLETION) {
86
+ if (process.env.DEBUG_BEDROCK_CHAT_COMPLETION === '1') {
70
87
  debugStream(debug).catch(console.error);
71
88
  }
72
89
 
@@ -88,15 +105,18 @@ export class LobeBedrockAI implements LobeRuntimeAI {
88
105
  }
89
106
  };
90
107
 
91
- private invokeLlamaModel = async (payload: ChatStreamPayload) => {
108
+ private invokeLlamaModel = async (
109
+ payload: ChatStreamPayload
110
+ ): Promise<StreamingTextResponse> => {
111
+ const { max_tokens, messages, model } = payload;
92
112
  const command = new InvokeModelWithResponseStreamCommand({
93
113
  accept: 'application/json',
94
114
  body: JSON.stringify({
95
- max_gen_len: payload.max_tokens || 400,
96
- prompt: experimental_buildLlama2Prompt(payload.messages as any),
115
+ max_gen_len: max_tokens || 400,
116
+ prompt: experimental_buildLlama2Prompt(messages as any),
97
117
  }),
98
118
  contentType: 'application/json',
99
- modelId: payload.model,
119
+ modelId: model,
100
120
  });
101
121
 
102
122
  try {
@@ -108,7 +128,7 @@ export class LobeBedrockAI implements LobeRuntimeAI {
108
128
 
109
129
  const [debug, output] = stream.tee();
110
130
 
111
- if (DEBUG_CHAT_COMPLETION) {
131
+ if (process.env.DEBUG_BEDROCK_CHAT_COMPLETION === '1') {
112
132
  debugStream(debug).catch(console.error);
113
133
  }
114
134
  // Respond with the stream
@@ -129,6 +149,7 @@ export class LobeBedrockAI implements LobeRuntimeAI {
129
149
  });
130
150
  }
131
151
  };
152
+
132
153
  }
133
154
 
134
155
  export default LobeBedrockAI;
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ buildAnthropicMessage,
5
+ buildAnthropicBlock,
6
+ } from './anthropicHelpers';
7
+
8
+ import { parseDataUri } from './uriParser';
9
+ import {
10
+ OpenAIChatMessage,
11
+ UserMessageContentPart,
12
+ } from '../types/chat';
13
+
14
+ describe('anthropicHelpers', () => {
15
+
16
+ // Mock the parseDataUri function since it's an implementation detail
17
+ vi.mock('./uriParser', () => ({
18
+ parseDataUri: vi.fn().mockReturnValue({
19
+ mimeType: 'image/jpeg',
20
+ base64: 'base64EncodedString',
21
+ }),
22
+ }));
23
+
24
+ describe('buildAnthropicBlock', () => {
25
+ it('should return the content as is for text type', () => {
26
+ const content: UserMessageContentPart =
27
+ { type: 'text', text: 'Hello!' };
28
+ const result = buildAnthropicBlock(content);
29
+ expect(result).toEqual(content);
30
+ });
31
+
32
+ it('should transform an image URL into an Anthropic.ImageBlockParam', () => {
33
+ const content: UserMessageContentPart =
34
+ { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,base64EncodedString' } };
35
+ const result = buildAnthropicBlock(content);
36
+ expect(parseDataUri).toHaveBeenCalledWith(content.image_url.url);
37
+ expect(result).toEqual({
38
+ source: {
39
+ data: 'base64EncodedString',
40
+ media_type: 'image/jpeg',
41
+ type: 'base64',
42
+ },
43
+ type: 'image',
44
+ });
45
+ });
46
+ });
47
+
48
+ describe('buildAnthropicMessage', () => {
49
+ it('should correctly convert system message to assistant message', () => {
50
+ const message: OpenAIChatMessage =
51
+ { content: [{ type: 'text', text: 'Hello!' }], role: 'system' };
52
+ const result = buildAnthropicMessage(message);
53
+ expect(result).toEqual(
54
+ { content: [{ type: 'text', text: 'Hello!' }], role: 'assistant' }
55
+ );
56
+ });
57
+ });
58
+
59
+ });
@@ -0,0 +1,47 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+
3
+ import {
4
+ OpenAIChatMessage,
5
+ UserMessageContentPart,
6
+ } from '../types';
7
+
8
+ import { parseDataUri } from './uriParser';
9
+
10
+ export const buildAnthropicBlock = (
11
+ content: UserMessageContentPart,
12
+ ): Anthropic.ContentBlock | Anthropic.ImageBlockParam => {
13
+ switch (content.type) {
14
+ case 'text': {
15
+ return content;
16
+ }
17
+
18
+ case 'image_url': {
19
+ const { mimeType, base64 } = parseDataUri(content.image_url.url);
20
+
21
+ return {
22
+ source: {
23
+ data: base64 as string,
24
+ media_type: mimeType as Anthropic.ImageBlockParam.Source['media_type'],
25
+ type: 'base64',
26
+ },
27
+ type: 'image',
28
+ };
29
+ }
30
+ }
31
+ }
32
+
33
+ export const buildAnthropicMessage = (
34
+ message: OpenAIChatMessage,
35
+ ): Anthropic.Messages.MessageParam => {
36
+ const content = message.content as string | UserMessageContentPart[];
37
+ return {
38
+ content:
39
+ typeof content === 'string' ? content : content.map((c) => buildAnthropicBlock(c)),
40
+ role: message.role === 'function' || message.role === 'system' ? 'assistant' : message.role,
41
+ };
42
+ };
43
+
44
+ export const buildAnthropicMessages = (
45
+ messages: OpenAIChatMessage[],
46
+ ): Anthropic.Messages.MessageParam[] =>
47
+ messages.map((message) => buildAnthropicMessage(message));
@@ -0,0 +1,18 @@
1
+ import useSWR, { SWRHook } from 'swr';
2
+
3
+ /**
4
+ * 这一类请求方法是比较「死」的请求模式,只会在第一次请求时触发。不会自动刷新,刷新需要搭配 refreshXXX 这样的方法实现,
5
+ * 适用于 messages、topics、sessions 等由用户在客户端交互产生的数据。
6
+ */
7
+ // @ts-ignore
8
+ export const useClientDataSWR: SWRHook = (key, fetch, config) =>
9
+ useSWR(key, fetch, {
10
+ // default is 2000ms ,it makes the user's quick switch don't work correctly.
11
+ // Cause issue like this: https://github.com/lobehub/lobe-chat/issues/532
12
+ // we need to set it to 0.
13
+ dedupingInterval: 0,
14
+ refreshWhenOffline: false,
15
+ revalidateOnFocus: false,
16
+ revalidateOnReconnect: false,
17
+ ...config,
18
+ });
@@ -559,7 +559,7 @@ describe('chatMessage actions', () => {
559
559
  });
560
560
 
561
561
  // 确保 mutate 调用了正确的参数
562
- expect(mutate).toHaveBeenCalledWith([activeId, activeTopicId]);
562
+ expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_MESSAGES', activeId, activeTopicId]);
563
563
  });
564
564
  it('should handle errors during refreshing messages', async () => {
565
565
  useChatStore.setState({ refreshMessages: realRefreshMessages });
@@ -2,12 +2,13 @@
2
2
  // Disable the auto sort key eslint rule to make the code more logic and readable
3
3
  import { copyToClipboard } from '@lobehub/ui';
4
4
  import { template } from 'lodash-es';
5
- import useSWR, { SWRResponse, mutate } from 'swr';
5
+ import { SWRResponse, mutate } from 'swr';
6
6
  import { StateCreator } from 'zustand/vanilla';
7
7
 
8
8
  import { LOADING_FLAT, isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/const/message';
9
9
  import { TraceEventType, TraceNameMap } from '@/const/trace';
10
10
  import { CreateMessageParams } from '@/database/models/message';
11
+ import { useClientDataSWR } from '@/libs/swr';
11
12
  import { chatService } from '@/services/chat';
12
13
  import { messageService } from '@/services/message';
13
14
  import { topicService } from '@/services/topic';
@@ -25,6 +26,8 @@ import { MessageDispatch, messagesReducer } from './reducer';
25
26
 
26
27
  const n = setNamespace('message');
27
28
 
29
+ const SWR_USE_FETCH_MESSAGES = 'SWR_USE_FETCH_MESSAGES';
30
+
28
31
  interface SendMessageParams {
29
32
  message: string;
30
33
  files?: { id: string; url: string }[];
@@ -274,9 +277,9 @@ export const chatMessage: StateCreator<
274
277
  await get().internalUpdateMessageContent(id, content);
275
278
  },
276
279
  useFetchMessages: (sessionId, activeTopicId) =>
277
- useSWR<ChatMessage[]>(
278
- [sessionId, activeTopicId],
279
- async ([sessionId, topicId]: [string, string | undefined]) =>
280
+ useClientDataSWR<ChatMessage[]>(
281
+ [SWR_USE_FETCH_MESSAGES, sessionId, activeTopicId],
282
+ async ([, sessionId, topicId]: [string, string, string | undefined]) =>
280
283
  messageService.getMessages(sessionId, topicId),
281
284
  {
282
285
  onSuccess: (messages, key) => {
@@ -289,14 +292,10 @@ export const chatMessage: StateCreator<
289
292
  }),
290
293
  );
291
294
  },
292
- // default is 2000ms ,it makes the user's quick switch don't work correctly.
293
- // Cause issue like this: https://github.com/lobehub/lobe-chat/issues/532
294
- // we need to set it to 0.
295
- dedupingInterval: 0,
296
295
  },
297
296
  ),
298
297
  refreshMessages: async () => {
299
- await mutate([get().activeId, get().activeTopicId]);
298
+ await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId]);
300
299
  },
301
300
 
302
301
  // the internal process method of the AI message
@@ -6,6 +6,7 @@ import { StateCreator } from 'zustand/vanilla';
6
6
 
7
7
  import { INBOX_SESSION_ID } from '@/const/session';
8
8
  import { SESSION_CHAT_URL } from '@/const/url';
9
+ import { useClientDataSWR } from '@/libs/swr';
9
10
  import { sessionService } from '@/services/session';
10
11
  import { useGlobalStore } from '@/store/global';
11
12
  import { settingsSelectors } from '@/store/global/selectors';
@@ -154,7 +155,7 @@ export const createSessionSlice: StateCreator<
154
155
  },
155
156
 
156
157
  useFetchSessions: () =>
157
- useSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getSessionsWithGroup, {
158
+ useClientDataSWR<ChatSessionList>(FETCH_SESSIONS_KEY, sessionService.getSessionsWithGroup, {
158
159
  onSuccess: (data) => {
159
160
  // 由于 https://github.com/lobehub/lobe-chat/pull/541 的关系
160
161
  // 只有触发了 refreshSessions 才会更新 sessions,进而触发页面 rerender