@lobehub/chat 0.156.2 → 0.157.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 +25 -0
- package/package.json +3 -2
- package/src/config/modelProviders/anthropic.ts +3 -0
- package/src/config/modelProviders/google.ts +3 -0
- package/src/config/modelProviders/groq.ts +5 -1
- package/src/config/modelProviders/minimax.ts +10 -7
- package/src/config/modelProviders/mistral.ts +1 -0
- package/src/config/modelProviders/moonshot.ts +3 -0
- package/src/config/modelProviders/zhipu.ts +2 -6
- package/src/config/server/provider.ts +1 -1
- package/src/database/client/core/db.ts +32 -0
- package/src/database/client/core/schemas.ts +9 -0
- package/src/database/client/models/__tests__/message.test.ts +2 -2
- package/src/database/client/schemas/message.ts +8 -1
- package/src/features/AgentSetting/store/action.ts +15 -6
- package/src/features/Conversation/Actions/Tool.tsx +16 -0
- package/src/features/Conversation/Actions/index.ts +2 -2
- package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +78 -0
- package/src/features/Conversation/Messages/Assistant/ToolCalls/style.ts +25 -0
- package/src/features/Conversation/Messages/Assistant/index.tsx +47 -0
- package/src/features/Conversation/Messages/Default.tsx +4 -1
- package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/index.tsx +34 -35
- package/src/features/Conversation/Messages/Tool/index.tsx +44 -0
- package/src/features/Conversation/Messages/index.ts +3 -2
- package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +1 -1
- package/src/features/Conversation/components/SkeletonList.tsx +2 -2
- package/src/features/Conversation/index.tsx +2 -3
- package/src/libs/agent-runtime/BaseAI.ts +2 -9
- package/src/libs/agent-runtime/anthropic/index.test.ts +195 -0
- package/src/libs/agent-runtime/anthropic/index.ts +71 -15
- package/src/libs/agent-runtime/azureOpenai/index.ts +6 -5
- package/src/libs/agent-runtime/bedrock/index.ts +24 -18
- package/src/libs/agent-runtime/google/index.test.ts +154 -0
- package/src/libs/agent-runtime/google/index.ts +91 -10
- package/src/libs/agent-runtime/groq/index.test.ts +41 -72
- package/src/libs/agent-runtime/groq/index.ts +7 -0
- package/src/libs/agent-runtime/minimax/index.test.ts +2 -2
- package/src/libs/agent-runtime/minimax/index.ts +14 -37
- package/src/libs/agent-runtime/mistral/index.test.ts +0 -53
- package/src/libs/agent-runtime/mistral/index.ts +1 -0
- package/src/libs/agent-runtime/moonshot/index.test.ts +1 -71
- package/src/libs/agent-runtime/ollama/index.test.ts +197 -0
- package/src/libs/agent-runtime/ollama/index.ts +3 -3
- package/src/libs/agent-runtime/openai/index.test.ts +0 -53
- package/src/libs/agent-runtime/openrouter/index.test.ts +1 -53
- package/src/libs/agent-runtime/perplexity/index.test.ts +0 -71
- package/src/libs/agent-runtime/perplexity/index.ts +2 -3
- package/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap +886 -0
- package/src/libs/agent-runtime/togetherai/fixtures/models.json +8111 -0
- package/src/libs/agent-runtime/togetherai/index.test.ts +16 -54
- package/src/libs/agent-runtime/types/chat.ts +19 -3
- package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +120 -1
- package/src/libs/agent-runtime/utils/anthropicHelpers.ts +67 -4
- package/src/libs/agent-runtime/utils/debugStream.test.ts +70 -0
- package/src/libs/agent-runtime/utils/debugStream.ts +39 -9
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +521 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +76 -5
- package/src/libs/agent-runtime/utils/response.ts +12 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +197 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +91 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/claude.ts +21 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/common.ts +32 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/index.ts +3 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/llama.test.ts +196 -0
- package/src/libs/agent-runtime/utils/streams/bedrock/llama.ts +51 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +97 -0
- package/src/libs/agent-runtime/utils/streams/google-ai.ts +68 -0
- package/src/libs/agent-runtime/utils/streams/index.ts +7 -0
- package/src/libs/agent-runtime/utils/streams/minimax.ts +39 -0
- package/src/libs/agent-runtime/utils/streams/ollama.test.ts +77 -0
- package/src/libs/agent-runtime/utils/streams/ollama.ts +38 -0
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +263 -0
- package/src/libs/agent-runtime/utils/streams/openai.ts +79 -0
- package/src/libs/agent-runtime/utils/streams/protocol.ts +100 -0
- package/src/libs/agent-runtime/zeroone/index.test.ts +1 -53
- package/src/libs/agent-runtime/zhipu/index.test.ts +1 -1
- package/src/libs/agent-runtime/zhipu/index.ts +3 -2
- package/src/locales/default/plugin.ts +3 -4
- package/src/migrations/FromV4ToV5/fixtures/from-v1-to-v5-output.json +245 -0
- package/src/migrations/FromV4ToV5/fixtures/function-input-v4.json +96 -0
- package/src/migrations/FromV4ToV5/fixtures/function-output-v5.json +120 -0
- package/src/migrations/FromV4ToV5/index.ts +58 -0
- package/src/migrations/FromV4ToV5/migrations.test.ts +49 -0
- package/src/migrations/FromV4ToV5/types/v4.ts +21 -0
- package/src/migrations/FromV4ToV5/types/v5.ts +27 -0
- package/src/migrations/index.ts +8 -1
- package/src/services/__tests__/chat.test.ts +10 -20
- package/src/services/chat.ts +78 -65
- package/src/store/chat/slices/enchance/action.ts +15 -10
- package/src/store/chat/slices/message/action.test.ts +36 -86
- package/src/store/chat/slices/message/action.ts +70 -79
- package/src/store/chat/slices/message/reducer.ts +18 -1
- package/src/store/chat/slices/message/selectors.test.ts +38 -68
- package/src/store/chat/slices/message/selectors.ts +1 -22
- package/src/store/chat/slices/plugin/action.test.ts +147 -203
- package/src/store/chat/slices/plugin/action.ts +96 -82
- package/src/store/chat/slices/share/action.test.ts +3 -3
- package/src/store/chat/slices/share/action.ts +1 -1
- package/src/store/chat/slices/topic/action.ts +7 -2
- package/src/store/tool/selectors/tool.ts +6 -24
- package/src/store/tool/slices/builtin/action.test.ts +90 -0
- package/src/types/llm.ts +1 -1
- package/src/types/message/index.ts +9 -4
- package/src/types/message/tools.ts +57 -0
- package/src/types/openai/chat.ts +6 -0
- package/src/utils/fetch.test.ts +245 -1
- package/src/utils/fetch.ts +120 -44
- package/src/utils/toolCall.ts +21 -0
- package/src/features/Conversation/Messages/Assistant.tsx +0 -26
- package/src/features/Conversation/Messages/Function.tsx +0 -35
- package/src/libs/agent-runtime/ollama/stream.ts +0 -31
- /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/PluginResultJSON.tsx +0 -0
- /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/Settings.tsx +0 -0
- /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/style.ts +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Loading3QuartersOutlined } from '@ant-design/icons';
|
|
2
|
-
import {
|
|
3
|
-
import { ActionIcon, Avatar, Highlighter, Icon } from '@lobehub/ui';
|
|
2
|
+
import { ActionIcon, Avatar, Highlighter, Icon, Tag } from '@lobehub/ui';
|
|
4
3
|
import { Tabs } from 'antd';
|
|
5
4
|
import isEqual from 'fast-deep-equal';
|
|
6
5
|
import {
|
|
7
6
|
LucideBug,
|
|
8
7
|
LucideBugOff,
|
|
9
8
|
LucideChevronDown,
|
|
10
|
-
|
|
9
|
+
LucideChevronRight,
|
|
11
10
|
LucideToyBrick,
|
|
12
11
|
} from 'lucide-react';
|
|
13
12
|
import { memo, useState } from 'react';
|
|
@@ -16,6 +15,7 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
16
15
|
|
|
17
16
|
import { pluginHelpers, useToolStore } from '@/store/tool';
|
|
18
17
|
import { pluginSelectors, toolSelectors } from '@/store/tool/selectors';
|
|
18
|
+
import { ChatPluginPayload } from '@/types/message';
|
|
19
19
|
|
|
20
20
|
import PluginResult from './PluginResultJSON';
|
|
21
21
|
import Settings from './Settings';
|
|
@@ -23,36 +23,34 @@ import { useStyles } from './style';
|
|
|
23
23
|
|
|
24
24
|
export interface InspectorProps {
|
|
25
25
|
arguments?: string;
|
|
26
|
-
command?: any;
|
|
27
26
|
content: string;
|
|
28
|
-
|
|
27
|
+
identifier?: string;
|
|
29
28
|
loading?: boolean;
|
|
29
|
+
payload?: ChatPluginPayload;
|
|
30
30
|
setShow?: (showRender: boolean) => void;
|
|
31
31
|
showRender?: boolean;
|
|
32
|
-
type?: LobePluginType;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const Inspector = memo<InspectorProps>(
|
|
36
35
|
({
|
|
37
36
|
arguments: requestArgs = '{}',
|
|
38
|
-
|
|
37
|
+
payload,
|
|
39
38
|
showRender,
|
|
40
39
|
loading,
|
|
41
40
|
setShow,
|
|
42
41
|
content,
|
|
43
|
-
|
|
44
|
-
// type,
|
|
42
|
+
identifier = 'unknown',
|
|
45
43
|
}) => {
|
|
46
44
|
const { t } = useTranslation('plugin');
|
|
47
45
|
const { styles } = useStyles();
|
|
48
46
|
const [open, setOpen] = useState(false);
|
|
49
47
|
|
|
50
|
-
const pluginMeta = useToolStore(toolSelectors.getMetaById(
|
|
48
|
+
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
|
51
49
|
|
|
52
|
-
const showRightAction = useToolStore(pluginSelectors.isPluginHasUI(
|
|
50
|
+
const showRightAction = useToolStore(pluginSelectors.isPluginHasUI(identifier));
|
|
53
51
|
const pluginAvatar = pluginHelpers.getPluginAvatar(pluginMeta);
|
|
54
52
|
|
|
55
|
-
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('
|
|
53
|
+
const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
|
|
56
54
|
|
|
57
55
|
const avatar = pluginAvatar ? (
|
|
58
56
|
<Avatar avatar={pluginAvatar} size={32} />
|
|
@@ -62,7 +60,7 @@ const Inspector = memo<InspectorProps>(
|
|
|
62
60
|
|
|
63
61
|
let args, params;
|
|
64
62
|
try {
|
|
65
|
-
args = JSON.stringify(
|
|
63
|
+
args = JSON.stringify(payload, null, 2);
|
|
66
64
|
params = JSON.stringify(JSON.parse(requestArgs), null, 2);
|
|
67
65
|
} catch {
|
|
68
66
|
args = '';
|
|
@@ -81,29 +79,30 @@ const Inspector = memo<InspectorProps>(
|
|
|
81
79
|
setShow?.(!showRender);
|
|
82
80
|
}}
|
|
83
81
|
>
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
<Flexbox horizontal>
|
|
96
|
-
{/*{type === 'standalone' && <ActionIcon icon={LucideOrbit} />}*/}
|
|
97
|
-
<ActionIcon
|
|
98
|
-
icon={open ? LucideBugOff : LucideBug}
|
|
99
|
-
onClick={() => {
|
|
100
|
-
setOpen(!open);
|
|
101
|
-
}}
|
|
102
|
-
title={t(open ? 'debug.off' : 'debug.on')}
|
|
103
|
-
/>
|
|
104
|
-
<Settings id={id} />
|
|
82
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
83
|
+
{loading ? (
|
|
84
|
+
<div>
|
|
85
|
+
<Loading3QuartersOutlined spin />
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
avatar
|
|
89
|
+
)}
|
|
90
|
+
<div>{pluginTitle}</div>
|
|
91
|
+
<Tag>{payload?.apiName}</Tag>
|
|
105
92
|
</Flexbox>
|
|
106
|
-
|
|
93
|
+
{showRightAction && <Icon icon={showRender ? LucideChevronDown : LucideChevronRight} />}
|
|
94
|
+
</Flexbox>
|
|
95
|
+
|
|
96
|
+
<Flexbox horizontal>
|
|
97
|
+
<ActionIcon
|
|
98
|
+
icon={open ? LucideBugOff : LucideBug}
|
|
99
|
+
onClick={() => {
|
|
100
|
+
setOpen(!open);
|
|
101
|
+
}}
|
|
102
|
+
title={t(open ? 'debug.off' : 'debug.on')}
|
|
103
|
+
/>
|
|
104
|
+
<Settings id={identifier} />
|
|
105
|
+
</Flexbox>
|
|
107
106
|
</Flexbox>
|
|
108
107
|
{open && (
|
|
109
108
|
<Tabs
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Snippet } from '@lobehub/ui';
|
|
2
|
+
import { memo, useState } from 'react';
|
|
3
|
+
import { Flexbox } from 'react-layout-kit';
|
|
4
|
+
|
|
5
|
+
import { useChatStore } from '@/store/chat';
|
|
6
|
+
import { chatSelectors } from '@/store/chat/selectors';
|
|
7
|
+
import { ChatMessage } from '@/types/message';
|
|
8
|
+
|
|
9
|
+
import PluginRender from '../../Plugins/Render';
|
|
10
|
+
import Inspector from './Inspector';
|
|
11
|
+
|
|
12
|
+
export const ToolMessage = memo<ChatMessage>(({ id, content, plugin }) => {
|
|
13
|
+
const loading = useChatStore(chatSelectors.isMessageGenerating(id));
|
|
14
|
+
|
|
15
|
+
const [showRender, setShow] = useState(plugin?.type !== 'default');
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Flexbox gap={12} id={id} width={'100%'}>
|
|
19
|
+
<Inspector
|
|
20
|
+
arguments={plugin?.arguments}
|
|
21
|
+
content={content}
|
|
22
|
+
identifier={plugin?.identifier}
|
|
23
|
+
loading={loading}
|
|
24
|
+
payload={plugin}
|
|
25
|
+
setShow={setShow}
|
|
26
|
+
showRender={showRender}
|
|
27
|
+
/>
|
|
28
|
+
{showRender || loading ? (
|
|
29
|
+
<PluginRender
|
|
30
|
+
content={content}
|
|
31
|
+
id={id}
|
|
32
|
+
identifier={plugin?.identifier}
|
|
33
|
+
loading={loading}
|
|
34
|
+
payload={plugin}
|
|
35
|
+
type={plugin?.type}
|
|
36
|
+
/>
|
|
37
|
+
) : (
|
|
38
|
+
<Flexbox>
|
|
39
|
+
<Snippet>{plugin?.arguments || ''}</Snippet>
|
|
40
|
+
</Flexbox>
|
|
41
|
+
)}
|
|
42
|
+
</Flexbox>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
@@ -6,13 +6,14 @@ import { sessionSelectors } from '@/store/session/selectors';
|
|
|
6
6
|
import { OnAvatarsClick, RenderMessage } from '../types';
|
|
7
7
|
import { AssistantMessage } from './Assistant';
|
|
8
8
|
import { DefaultMessage } from './Default';
|
|
9
|
-
import {
|
|
9
|
+
import { ToolMessage } from './Tool';
|
|
10
10
|
import { UserMessage } from './User';
|
|
11
11
|
|
|
12
12
|
export const renderMessages: Record<string, RenderMessage> = {
|
|
13
13
|
assistant: AssistantMessage,
|
|
14
14
|
default: DefaultMessage,
|
|
15
|
-
function:
|
|
15
|
+
function: DefaultMessage,
|
|
16
|
+
tool: ToolMessage,
|
|
16
17
|
user: UserMessage,
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -126,7 +126,7 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
|
|
|
126
126
|
// we need to know which message to trigger
|
|
127
127
|
if (messageId !== id) return;
|
|
128
128
|
|
|
129
|
-
triggerAIMessage(id);
|
|
129
|
+
triggerAIMessage({ parentId: id });
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
// when plugin want to create an assistant message
|
|
@@ -34,14 +34,14 @@ const SkeletonList = memo<SkeletonListProps>(({ mobile }) => {
|
|
|
34
34
|
<Skeleton
|
|
35
35
|
active
|
|
36
36
|
avatar={{ size: mobile ? 32 : 40 }}
|
|
37
|
-
className={styles.message}
|
|
37
|
+
className={cx(styles.message, styles.user)}
|
|
38
38
|
paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
|
|
39
39
|
title={false}
|
|
40
40
|
/>
|
|
41
41
|
<Skeleton
|
|
42
42
|
active
|
|
43
43
|
avatar={{ size: mobile ? 32 : 40 }}
|
|
44
|
-
className={
|
|
44
|
+
className={styles.message}
|
|
45
45
|
paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
|
|
46
46
|
title={false}
|
|
47
47
|
/>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Suspense
|
|
1
|
+
import { Suspense } from 'react';
|
|
2
2
|
import { Flexbox } from 'react-layout-kit';
|
|
3
3
|
|
|
4
4
|
import SkeletonList from './components/SkeletonList';
|
|
5
|
-
|
|
6
|
-
const ChatList = lazy(() => import('./components/VirtualizedList'));
|
|
5
|
+
import ChatList from './components/VirtualizedList';
|
|
7
6
|
|
|
8
7
|
interface ConversationProps {
|
|
9
8
|
mobile?: boolean;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { StreamingTextResponse } from 'ai';
|
|
2
1
|
import OpenAI from 'openai';
|
|
3
2
|
|
|
4
3
|
import { ChatModelCard } from '@/types/llm';
|
|
@@ -7,10 +6,7 @@ import { ChatCompetitionOptions, ChatStreamPayload } from './types';
|
|
|
7
6
|
|
|
8
7
|
export interface LobeRuntimeAI {
|
|
9
8
|
baseURL?: string;
|
|
10
|
-
chat(
|
|
11
|
-
payload: ChatStreamPayload,
|
|
12
|
-
options?: ChatCompetitionOptions,
|
|
13
|
-
): Promise<StreamingTextResponse>;
|
|
9
|
+
chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions): Promise<Response>;
|
|
14
10
|
|
|
15
11
|
models?(): Promise<any>;
|
|
16
12
|
}
|
|
@@ -19,10 +15,7 @@ export abstract class LobeOpenAICompatibleRuntime {
|
|
|
19
15
|
abstract baseURL: string;
|
|
20
16
|
abstract client: OpenAI;
|
|
21
17
|
|
|
22
|
-
abstract chat(
|
|
23
|
-
payload: ChatStreamPayload,
|
|
24
|
-
options?: ChatCompetitionOptions,
|
|
25
|
-
): Promise<StreamingTextResponse>;
|
|
18
|
+
abstract chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions): Promise<Response>;
|
|
26
19
|
|
|
27
20
|
abstract models(): Promise<ChatModelCard[]>;
|
|
28
21
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
+
import { ChatCompletionTool } from '@/libs/agent-runtime';
|
|
5
|
+
|
|
6
|
+
import * as anthropicHelpers from '../utils/anthropicHelpers';
|
|
4
7
|
import * as debugStreamModule from '../utils/debugStream';
|
|
5
8
|
import { LobeAnthropicAI } from './index';
|
|
6
9
|
|
|
@@ -16,6 +19,10 @@ beforeEach(() => {
|
|
|
16
19
|
|
|
17
20
|
// 使用 vi.spyOn 来模拟 chat.completions.create 方法
|
|
18
21
|
vi.spyOn(instance['client'].messages, 'create').mockReturnValue(new ReadableStream() as any);
|
|
22
|
+
|
|
23
|
+
vi.spyOn(instance['client'].beta.tools.messages, 'create').mockReturnValue({
|
|
24
|
+
content: [],
|
|
25
|
+
} as any);
|
|
19
26
|
});
|
|
20
27
|
|
|
21
28
|
afterEach(() => {
|
|
@@ -233,6 +240,54 @@ describe('LobeAnthropicAI', () => {
|
|
|
233
240
|
process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION = originalDebugValue;
|
|
234
241
|
});
|
|
235
242
|
|
|
243
|
+
describe('chat with tools', () => {
|
|
244
|
+
it('should call client.beta.tools.messages.create when tools are provided', async () => {
|
|
245
|
+
// Arrange
|
|
246
|
+
const tools: ChatCompletionTool[] = [
|
|
247
|
+
{ function: { name: 'tool1', description: 'desc1' }, type: 'function' },
|
|
248
|
+
];
|
|
249
|
+
const spyOn = vi.spyOn(anthropicHelpers, 'buildAnthropicTools');
|
|
250
|
+
|
|
251
|
+
// Act
|
|
252
|
+
await instance.chat({
|
|
253
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
254
|
+
model: 'claude-3-haiku-20240307',
|
|
255
|
+
temperature: 1,
|
|
256
|
+
tools,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Assert
|
|
260
|
+
expect(instance['client'].beta.tools.messages.create).toHaveBeenCalled();
|
|
261
|
+
expect(spyOn).toHaveBeenCalledWith(tools);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should handle text and tool_use content correctly in transformResponseToStream', async () => {
|
|
265
|
+
// Arrange
|
|
266
|
+
const mockResponse = {
|
|
267
|
+
content: [
|
|
268
|
+
{ type: 'text', text: 'Hello' },
|
|
269
|
+
{ type: 'tool_use', id: 'tool1', name: 'tool1', input: 'input1' },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
// @ts-ignore
|
|
273
|
+
vi.spyOn(instance, 'transformResponseToStream').mockReturnValue(new ReadableStream());
|
|
274
|
+
vi.spyOn(instance['client'].beta.tools.messages, 'create').mockResolvedValue(
|
|
275
|
+
mockResponse as any,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Act
|
|
279
|
+
await instance.chat({
|
|
280
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
281
|
+
model: 'claude-3-haiku-20240307',
|
|
282
|
+
temperature: 0,
|
|
283
|
+
tools: [{ function: { name: 'tool1', description: 'desc1' }, type: 'function' }],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Assert
|
|
287
|
+
expect(instance['transformResponseToStream']).toHaveBeenCalledWith(mockResponse);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
236
291
|
describe('Error', () => {
|
|
237
292
|
it('should throw InvalidAnthropicAPIKey error on API_KEY_INVALID error', async () => {
|
|
238
293
|
// Arrange
|
|
@@ -305,5 +360,145 @@ describe('LobeAnthropicAI', () => {
|
|
|
305
360
|
}
|
|
306
361
|
});
|
|
307
362
|
});
|
|
363
|
+
|
|
364
|
+
describe('Error handling', () => {
|
|
365
|
+
it('should throw LocationNotSupportError on 403 error', async () => {
|
|
366
|
+
// Arrange
|
|
367
|
+
const apiError = { status: 403 };
|
|
368
|
+
(instance['client'].messages.create as Mock).mockRejectedValue(apiError);
|
|
369
|
+
|
|
370
|
+
// Act & Assert
|
|
371
|
+
await expect(
|
|
372
|
+
instance.chat({
|
|
373
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
374
|
+
model: 'claude-3-haiku-20240307',
|
|
375
|
+
temperature: 1,
|
|
376
|
+
}),
|
|
377
|
+
).rejects.toEqual({
|
|
378
|
+
endpoint: 'https://api.anthropic.com',
|
|
379
|
+
error: apiError,
|
|
380
|
+
errorType: 'LocationNotSupportError',
|
|
381
|
+
provider,
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should throw AnthropicBizError on other error status codes', async () => {
|
|
386
|
+
// Arrange
|
|
387
|
+
const apiError = { status: 500 };
|
|
388
|
+
(instance['client'].messages.create as Mock).mockRejectedValue(apiError);
|
|
389
|
+
|
|
390
|
+
// Act & Assert
|
|
391
|
+
await expect(
|
|
392
|
+
instance.chat({
|
|
393
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
394
|
+
model: 'claude-3-haiku-20240307',
|
|
395
|
+
temperature: 1,
|
|
396
|
+
}),
|
|
397
|
+
).rejects.toEqual({
|
|
398
|
+
endpoint: 'https://api.anthropic.com',
|
|
399
|
+
error: apiError,
|
|
400
|
+
errorType: 'AnthropicBizError',
|
|
401
|
+
provider,
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should desensitize custom baseURL in error message', async () => {
|
|
406
|
+
// Arrange
|
|
407
|
+
const apiError = { status: 401 };
|
|
408
|
+
const customInstance = new LobeAnthropicAI({
|
|
409
|
+
apiKey: 'test',
|
|
410
|
+
baseURL: 'https://api.custom.com/v1',
|
|
411
|
+
});
|
|
412
|
+
vi.spyOn(customInstance['client'].messages, 'create').mockRejectedValue(apiError);
|
|
413
|
+
|
|
414
|
+
// Act & Assert
|
|
415
|
+
await expect(
|
|
416
|
+
customInstance.chat({
|
|
417
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
418
|
+
model: 'claude-3-haiku-20240307',
|
|
419
|
+
temperature: 0,
|
|
420
|
+
}),
|
|
421
|
+
).rejects.toEqual({
|
|
422
|
+
endpoint: 'https://api.cu****om.com/v1',
|
|
423
|
+
error: apiError,
|
|
424
|
+
errorType: 'InvalidAnthropicAPIKey',
|
|
425
|
+
provider,
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('Options', () => {
|
|
431
|
+
it('should pass signal to API call', async () => {
|
|
432
|
+
// Arrange
|
|
433
|
+
const controller = new AbortController();
|
|
434
|
+
|
|
435
|
+
// Act
|
|
436
|
+
await instance.chat(
|
|
437
|
+
{
|
|
438
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
439
|
+
model: 'claude-3-haiku-20240307',
|
|
440
|
+
temperature: 1,
|
|
441
|
+
},
|
|
442
|
+
{ signal: controller.signal },
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Assert
|
|
446
|
+
expect(instance['client'].messages.create).toHaveBeenCalledWith(
|
|
447
|
+
expect.objectContaining({}),
|
|
448
|
+
{ signal: controller.signal },
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should apply callback to the returned stream', async () => {
|
|
453
|
+
// Arrange
|
|
454
|
+
const callback = vi.fn();
|
|
455
|
+
|
|
456
|
+
// Act
|
|
457
|
+
await instance.chat(
|
|
458
|
+
{
|
|
459
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
460
|
+
model: 'claude-3-haiku-20240307',
|
|
461
|
+
temperature: 0,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
callback: { onStart: callback },
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// Assert
|
|
469
|
+
expect(callback).toHaveBeenCalled();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should set headers on the response', async () => {
|
|
473
|
+
// Arrange
|
|
474
|
+
const headers = { 'X-Test-Header': 'test' };
|
|
475
|
+
|
|
476
|
+
// Act
|
|
477
|
+
const result = await instance.chat(
|
|
478
|
+
{
|
|
479
|
+
messages: [{ content: 'Hello', role: 'user' }],
|
|
480
|
+
model: 'claude-3-haiku-20240307',
|
|
481
|
+
temperature: 1,
|
|
482
|
+
},
|
|
483
|
+
{ headers },
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Assert
|
|
487
|
+
expect(result.headers.get('X-Test-Header')).toBe('test');
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe('Edge cases', () => {
|
|
492
|
+
it('should handle empty messages array', async () => {
|
|
493
|
+
// Act & Assert
|
|
494
|
+
await expect(
|
|
495
|
+
instance.chat({
|
|
496
|
+
messages: [],
|
|
497
|
+
model: 'claude-3-haiku-20240307',
|
|
498
|
+
temperature: 1,
|
|
499
|
+
}),
|
|
500
|
+
).resolves.toBeInstanceOf(Response);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
308
503
|
});
|
|
309
504
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// sort-imports-ignore
|
|
2
2
|
import '@anthropic-ai/sdk/shims/web';
|
|
3
3
|
import Anthropic from '@anthropic-ai/sdk';
|
|
4
|
-
import { AnthropicStream, StreamingTextResponse } from 'ai';
|
|
5
4
|
import { ClientOptions } from 'openai';
|
|
6
5
|
|
|
7
6
|
import { LobeRuntimeAI } from '../BaseAI';
|
|
@@ -10,7 +9,9 @@ import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../typ
|
|
|
10
9
|
import { AgentRuntimeError } from '../utils/createError';
|
|
11
10
|
import { debugStream } from '../utils/debugStream';
|
|
12
11
|
import { desensitizeUrl } from '../utils/desensitizeUrl';
|
|
13
|
-
import { buildAnthropicMessages } from '../utils/anthropicHelpers';
|
|
12
|
+
import { buildAnthropicMessages, buildAnthropicTools } from '../utils/anthropicHelpers';
|
|
13
|
+
import { StreamingResponse } from '../utils/response';
|
|
14
|
+
import { AnthropicStream } from '../utils/streams';
|
|
14
15
|
|
|
15
16
|
const DEFAULT_BASE_URL = 'https://api.anthropic.com';
|
|
16
17
|
|
|
@@ -30,18 +31,40 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
|
30
31
|
try {
|
|
31
32
|
const anthropicPayload = this.buildAnthropicPayload(payload);
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// if there is no tool, we can use the normal chat API
|
|
35
|
+
if (!anthropicPayload.tools || anthropicPayload.tools.length === 0) {
|
|
36
|
+
const response = await this.client.messages.create(
|
|
37
|
+
{ ...anthropicPayload, stream: true },
|
|
38
|
+
{
|
|
39
|
+
signal: options?.signal,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const [prod, debug] = response.tee();
|
|
44
|
+
|
|
45
|
+
if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
|
|
46
|
+
debugStream(debug.toReadableStream()).catch(console.error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return StreamingResponse(AnthropicStream(prod, options?.callback), {
|
|
50
|
+
headers: options?.headers,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// or we should call the tool API
|
|
55
|
+
const response = await this.client.beta.tools.messages.create(
|
|
56
|
+
{ ...anthropicPayload, stream: false },
|
|
35
57
|
{ signal: options?.signal },
|
|
36
58
|
);
|
|
37
59
|
|
|
38
|
-
const [prod, debug] = response.tee();
|
|
39
|
-
|
|
40
60
|
if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
|
|
41
|
-
|
|
61
|
+
console.log('\n[no stream response]\n');
|
|
62
|
+
console.log(JSON.stringify(response) + '\n');
|
|
42
63
|
}
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
const stream = this.transformResponseToStream(response);
|
|
66
|
+
|
|
67
|
+
return StreamingResponse(AnthropicStream(stream, options?.callback), {
|
|
45
68
|
headers: options?.headers,
|
|
46
69
|
});
|
|
47
70
|
} catch (error) {
|
|
@@ -85,20 +108,53 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
private buildAnthropicPayload(payload: ChatStreamPayload) {
|
|
88
|
-
const { messages, model, max_tokens, temperature, top_p } = payload;
|
|
111
|
+
const { messages, model, max_tokens = 4096, temperature, top_p, tools } = payload;
|
|
89
112
|
const system_message = messages.find((m) => m.role === 'system');
|
|
90
113
|
const user_messages = messages.filter((m) => m.role !== 'system');
|
|
91
114
|
|
|
92
115
|
return {
|
|
93
|
-
max_tokens
|
|
116
|
+
max_tokens,
|
|
94
117
|
messages: buildAnthropicMessages(user_messages),
|
|
95
|
-
model
|
|
96
|
-
stream: true,
|
|
118
|
+
model,
|
|
97
119
|
system: system_message?.content as string,
|
|
98
|
-
temperature
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
temperature,
|
|
121
|
+
// TODO: Anthropic sdk don't have tools interface currently
|
|
122
|
+
// @ts-ignore
|
|
123
|
+
tools: buildAnthropicTools(tools),
|
|
124
|
+
top_p,
|
|
125
|
+
} satisfies Anthropic.MessageCreateParams;
|
|
101
126
|
}
|
|
127
|
+
|
|
128
|
+
private transformResponseToStream = (response: Anthropic.Beta.Tools.ToolsBetaMessage) => {
|
|
129
|
+
return new ReadableStream<Anthropic.MessageStreamEvent>({
|
|
130
|
+
start(controller) {
|
|
131
|
+
response.content.forEach((content) => {
|
|
132
|
+
switch (content.type) {
|
|
133
|
+
case 'text': {
|
|
134
|
+
controller.enqueue({
|
|
135
|
+
delta: { text: content.text, type: 'text_delta' },
|
|
136
|
+
type: 'content_block_delta',
|
|
137
|
+
} as Anthropic.ContentBlockDeltaEvent);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'tool_use': {
|
|
141
|
+
controller.enqueue({
|
|
142
|
+
delta: {
|
|
143
|
+
tool_use: { id: content.id, input: content.input, name: content.name },
|
|
144
|
+
type: 'tool_use',
|
|
145
|
+
},
|
|
146
|
+
type: 'content_block_delta',
|
|
147
|
+
} as any);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
controller.enqueue({ type: 'message_stop' } as Anthropic.MessageStopEvent);
|
|
153
|
+
|
|
154
|
+
controller.close();
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
};
|
|
102
158
|
}
|
|
103
159
|
|
|
104
160
|
export default LobeAnthropicAI;
|
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
GetChatCompletionsOptions,
|
|
5
5
|
OpenAIClient,
|
|
6
6
|
} from '@azure/openai';
|
|
7
|
-
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
|
8
7
|
|
|
9
8
|
import { LobeRuntimeAI } from '../BaseAI';
|
|
10
9
|
import { AgentRuntimeErrorType } from '../error';
|
|
11
10
|
import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
|
|
12
11
|
import { AgentRuntimeError } from '../utils/createError';
|
|
13
12
|
import { debugStream } from '../utils/debugStream';
|
|
13
|
+
import { StreamingResponse } from '../utils/response';
|
|
14
|
+
import { OpenAIStream } from '../utils/streams';
|
|
14
15
|
|
|
15
16
|
export class LobeAzureOpenAI implements LobeRuntimeAI {
|
|
16
17
|
client: OpenAIClient;
|
|
@@ -40,15 +41,15 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
|
|
|
40
41
|
{ ...params, abortSignal: options?.signal, maxTokens } as GetChatCompletionsOptions,
|
|
41
42
|
);
|
|
42
43
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const [debug, prod] = stream.tee();
|
|
44
|
+
const [debug, prod] = response.tee();
|
|
46
45
|
|
|
47
46
|
if (process.env.DEBUG_AZURE_CHAT_COMPLETION === '1') {
|
|
48
47
|
debugStream(debug).catch(console.error);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
return
|
|
50
|
+
return StreamingResponse(OpenAIStream(prod, options?.callback), {
|
|
51
|
+
headers: options?.headers,
|
|
52
|
+
});
|
|
52
53
|
} catch (e) {
|
|
53
54
|
let error = e as { [key: string]: any; code: string; message: string };
|
|
54
55
|
|