@agentscope-ai/agentscope 0.0.2
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/dist/agent/index.d.mts +234 -0
- package/dist/agent/index.d.ts +234 -0
- package/dist/agent/index.js +1412 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/index.mjs +1375 -0
- package/dist/agent/index.mjs.map +1 -0
- package/dist/base-BOx3UzOl.d.mts +41 -0
- package/dist/base-BoIps2RL.d.ts +41 -0
- package/dist/base-C7jwyH4Z.d.mts +52 -0
- package/dist/base-Cwi4bjze.d.ts +127 -0
- package/dist/base-DYlBMCy_.d.mts +127 -0
- package/dist/base-NX-knWOv.d.ts +52 -0
- package/dist/block-VsnHrllL.d.mts +48 -0
- package/dist/block-VsnHrllL.d.ts +48 -0
- package/dist/event/index.d.mts +181 -0
- package/dist/event/index.d.ts +181 -0
- package/dist/event/index.js +58 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/index.mjs +33 -0
- package/dist/event/index.mjs.map +1 -0
- package/dist/formatter/index.d.mts +187 -0
- package/dist/formatter/index.d.ts +187 -0
- package/dist/formatter/index.js +647 -0
- package/dist/formatter/index.js.map +1 -0
- package/dist/formatter/index.mjs +616 -0
- package/dist/formatter/index.mjs.map +1 -0
- package/dist/index-BTJDlKvQ.d.mts +195 -0
- package/dist/index-BcatlwXQ.d.ts +195 -0
- package/dist/index-CAxQAkiP.d.mts +21 -0
- package/dist/index-CAxQAkiP.d.ts +21 -0
- package/dist/mcp/index.d.mts +9 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.js +432 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/index.mjs +408 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/message/index.d.mts +10 -0
- package/dist/message/index.d.ts +10 -0
- package/dist/message/index.js +67 -0
- package/dist/message/index.js.map +1 -0
- package/dist/message/index.mjs +37 -0
- package/dist/message/index.mjs.map +1 -0
- package/dist/message-CkN21KaY.d.mts +99 -0
- package/dist/message-CzLeTlua.d.ts +99 -0
- package/dist/model/index.d.mts +377 -0
- package/dist/model/index.d.ts +377 -0
- package/dist/model/index.js +1880 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/index.mjs +1849 -0
- package/dist/model/index.mjs.map +1 -0
- package/dist/storage/index.d.mts +68 -0
- package/dist/storage/index.d.ts +68 -0
- package/dist/storage/index.js +250 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +212 -0
- package/dist/storage/index.mjs.map +1 -0
- package/dist/tool/index.d.mts +311 -0
- package/dist/tool/index.d.ts +311 -0
- package/dist/tool/index.js +1494 -0
- package/dist/tool/index.js.map +1 -0
- package/dist/tool/index.mjs +1447 -0
- package/dist/tool/index.mjs.map +1 -0
- package/dist/toolkit-CEpulFi0.d.ts +99 -0
- package/dist/toolkit-CGEZSZPa.d.mts +99 -0
- package/jest.config.js +11 -0
- package/package.json +92 -0
- package/src/_utils/common.ts +104 -0
- package/src/_utils/index.ts +1 -0
- package/src/agent/agent-base.ts +0 -0
- package/src/agent/agent.test.ts +1028 -0
- package/src/agent/agent.ts +1032 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/interfaces.ts +23 -0
- package/src/agent/test-compression.ts +72 -0
- package/src/event/index.ts +250 -0
- package/src/formatter/base.ts +133 -0
- package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
- package/src/formatter/dashscope-chat-formatter.ts +163 -0
- package/src/formatter/deepseek-chat-formatter.ts +130 -0
- package/src/formatter/index.ts +5 -0
- package/src/formatter/ollama-chat-formatter.ts +67 -0
- package/src/formatter/openai-chat-formatter.test.ts +263 -0
- package/src/formatter/openai-chat-formatter.ts +301 -0
- package/src/formatter/openai.md +767 -0
- package/src/mcp/base.ts +114 -0
- package/src/mcp/http.test.ts +303 -0
- package/src/mcp/http.ts +224 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/stdio.test.ts +91 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/message/block.ts +60 -0
- package/src/message/enums.ts +4 -0
- package/src/message/index.ts +12 -0
- package/src/message/message.test.ts +80 -0
- package/src/message/message.ts +131 -0
- package/src/model/base.ts +226 -0
- package/src/model/dashscope-model.test.ts +335 -0
- package/src/model/dashscope-model.ts +441 -0
- package/src/model/deepseek-model.test.ts +279 -0
- package/src/model/deepseek-model.ts +401 -0
- package/src/model/index.ts +7 -0
- package/src/model/ollama-model.test.ts +307 -0
- package/src/model/ollama-model.ts +356 -0
- package/src/model/openai-model.ts +327 -0
- package/src/model/response.ts +22 -0
- package/src/model/usage.ts +12 -0
- package/src/storage/base.ts +52 -0
- package/src/storage/file-system.test.ts +587 -0
- package/src/storage/file-system.ts +269 -0
- package/src/storage/index.ts +2 -0
- package/src/tool/base.ts +23 -0
- package/src/tool/bash.test.ts +174 -0
- package/src/tool/bash.ts +152 -0
- package/src/tool/edit.test.ts +83 -0
- package/src/tool/edit.ts +95 -0
- package/src/tool/glob.test.ts +63 -0
- package/src/tool/glob.ts +166 -0
- package/src/tool/grep.test.ts +74 -0
- package/src/tool/grep.ts +256 -0
- package/src/tool/index.ts +10 -0
- package/src/tool/read.test.ts +77 -0
- package/src/tool/read.ts +117 -0
- package/src/tool/response.ts +82 -0
- package/src/tool/task.test.ts +299 -0
- package/src/tool/task.ts +399 -0
- package/src/tool/toolkit.test.ts +636 -0
- package/src/tool/toolkit.ts +601 -0
- package/src/tool/write.test.ts +52 -0
- package/src/tool/write.ts +57 -0
- package/src/type/index.ts +52 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +20 -0
- package/typedoc.json +52 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { ChatResponse, StructuredResponse } from './response';
|
|
4
|
+
import { FormatterBase } from '../formatter';
|
|
5
|
+
import { getTextContent, Msg } from '../message';
|
|
6
|
+
import { ToolChoice, ToolInputSchema, ToolSchema } from '../type';
|
|
7
|
+
|
|
8
|
+
export interface ChatModelOptions {
|
|
9
|
+
modelName: string;
|
|
10
|
+
stream?: boolean;
|
|
11
|
+
maxRetries?: number;
|
|
12
|
+
fallbackModelName?: string;
|
|
13
|
+
formatter?: FormatterBase;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The chat model call options interface
|
|
17
|
+
export interface ChatModelCallOptions {
|
|
18
|
+
messages: Msg[];
|
|
19
|
+
tools?: ToolSchema[];
|
|
20
|
+
toolChoice?: ToolChoice;
|
|
21
|
+
|
|
22
|
+
// The additional options can be added as needed
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChatModelCallStructuredOptions {
|
|
27
|
+
messages: Msg[];
|
|
28
|
+
schema: z.ZodObject;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Internal API request options after formatting
|
|
32
|
+
export interface ChatModelRequestOptions<T> {
|
|
33
|
+
messages: T[];
|
|
34
|
+
tools?: ToolSchema[];
|
|
35
|
+
toolChoice?: ToolChoice;
|
|
36
|
+
|
|
37
|
+
// The additional options can be added as needed
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The base class for chat models.
|
|
43
|
+
*/
|
|
44
|
+
export abstract class ChatModelBase {
|
|
45
|
+
public modelName: string;
|
|
46
|
+
public stream: boolean;
|
|
47
|
+
public maxRetries: number;
|
|
48
|
+
public fallbackModelName?: string;
|
|
49
|
+
public formatter?: FormatterBase;
|
|
50
|
+
/**
|
|
51
|
+
* Initializes a new instance of the ChatModelBase class.
|
|
52
|
+
*
|
|
53
|
+
* @param options - The chat model options, including model name, streaming option, max retries, fallback
|
|
54
|
+
* model name, and formatter.
|
|
55
|
+
*
|
|
56
|
+
* @param options.modelName
|
|
57
|
+
* @param options.stream
|
|
58
|
+
* @param options.maxRetries
|
|
59
|
+
* @param options.fallbackModelName
|
|
60
|
+
* @param options.formatter
|
|
61
|
+
*/
|
|
62
|
+
protected constructor({
|
|
63
|
+
modelName,
|
|
64
|
+
stream,
|
|
65
|
+
maxRetries,
|
|
66
|
+
fallbackModelName,
|
|
67
|
+
formatter,
|
|
68
|
+
}: ChatModelOptions) {
|
|
69
|
+
this.modelName = modelName;
|
|
70
|
+
this.stream = stream ?? true;
|
|
71
|
+
this.maxRetries = maxRetries ?? 0;
|
|
72
|
+
this.fallbackModelName = fallbackModelName;
|
|
73
|
+
this.formatter = formatter;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calls the chat model with the given messages.
|
|
78
|
+
* This is the main method to interact with the model.
|
|
79
|
+
*
|
|
80
|
+
* @param options - The chat model call options.
|
|
81
|
+
* @returns A promise that resolves to the model's response.
|
|
82
|
+
*/
|
|
83
|
+
async call(
|
|
84
|
+
options: ChatModelCallOptions
|
|
85
|
+
): Promise<ChatResponse | AsyncGenerator<ChatResponse>> {
|
|
86
|
+
// Format messages using the formatter if available
|
|
87
|
+
let formattedMessages: unknown[];
|
|
88
|
+
if (this.formatter) {
|
|
89
|
+
formattedMessages = await this.formatter.format({ msgs: options.messages });
|
|
90
|
+
} else {
|
|
91
|
+
// If no formatter is provided, pass messages as-is
|
|
92
|
+
formattedMessages = options.messages as unknown[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const requestOptions: ChatModelRequestOptions<unknown> = {
|
|
96
|
+
...options,
|
|
97
|
+
messages: formattedMessages,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let lastError: unknown;
|
|
101
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
102
|
+
try {
|
|
103
|
+
return await this._callAPI(this.modelName, requestOptions);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
lastError = error;
|
|
106
|
+
if (attempt === this.maxRetries) {
|
|
107
|
+
throw error;
|
|
108
|
+
} else {
|
|
109
|
+
console.log(
|
|
110
|
+
`Attempt ${attempt + 1} failed for model ${this.modelName}. Retrying...`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Use the fallback model if specified
|
|
117
|
+
if (this.fallbackModelName) {
|
|
118
|
+
console.log(
|
|
119
|
+
`Using fallback model ${this.fallbackModelName} after ${this.maxRetries} failed attempts.`
|
|
120
|
+
);
|
|
121
|
+
return await this._callAPI(this.fallbackModelName, requestOptions);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// This line should never be reached, but it ensures TypeScript knows the function always returns
|
|
125
|
+
throw lastError;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Abstract method to call the underlying API with the given parameters.
|
|
130
|
+
*/
|
|
131
|
+
protected abstract _callAPI(
|
|
132
|
+
modelName: string,
|
|
133
|
+
options: ChatModelRequestOptions<unknown>
|
|
134
|
+
): Promise<ChatResponse | AsyncGenerator<ChatResponse>>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format the AgentScope tool choice parameter to the expected API format.
|
|
138
|
+
*
|
|
139
|
+
* @param toolChoice - The tool choice option.
|
|
140
|
+
* @returns The formatted tool choice.
|
|
141
|
+
*/
|
|
142
|
+
abstract _formatToolChoice(toolChoice: ToolChoice): unknown;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A heuristic method to count the number of the tokens
|
|
146
|
+
* Note the multimodal content is ignored in the token counting
|
|
147
|
+
* @param options
|
|
148
|
+
* @param options.messages
|
|
149
|
+
* @param options.tools
|
|
150
|
+
* @returns The estimated number of tokens in the input messages and tools.
|
|
151
|
+
*/
|
|
152
|
+
async countTokens(options: { messages: Msg[]; tools?: ToolSchema[] }): Promise<number> {
|
|
153
|
+
let accText: string = '';
|
|
154
|
+
for (const msg of options.messages) {
|
|
155
|
+
accText += getTextContent(msg) || '';
|
|
156
|
+
}
|
|
157
|
+
if (options.tools) {
|
|
158
|
+
accText += JSON.stringify(options.tools);
|
|
159
|
+
}
|
|
160
|
+
const chineseMatches =
|
|
161
|
+
accText.match(/[\u4e00-\u9fff\u3400-\u4dbf\u{20000}-\u{2a6df}]/gu)?.length ?? 0;
|
|
162
|
+
const englishMatches = accText.match(/[a-zA-Z]+/g)?.length ?? 0;
|
|
163
|
+
|
|
164
|
+
return chineseMatches * 2 + englishMatches * 1.5;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Format the tool schemas to the expected API format.
|
|
169
|
+
* @param tools
|
|
170
|
+
* @returns The formatted tool schemas.
|
|
171
|
+
*/
|
|
172
|
+
abstract _formatToolSchemas(tools: ToolSchema[]): unknown[];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A default implementation of the structured call method. For those supporting structured output, the model should
|
|
176
|
+
* override this method.
|
|
177
|
+
* @param options
|
|
178
|
+
* @returns The structured response from the model, which should conform to the provided Zod schema.
|
|
179
|
+
*/
|
|
180
|
+
async callStructured(options: ChatModelCallStructuredOptions): Promise<StructuredResponse> {
|
|
181
|
+
// Prepare a tool schema that wraps the provided Zod schema
|
|
182
|
+
const toolSchema: ToolSchema = {
|
|
183
|
+
type: 'function',
|
|
184
|
+
function: {
|
|
185
|
+
name: 'GenerateStructuredResponse',
|
|
186
|
+
description: 'Generate required structured response by this toll.',
|
|
187
|
+
parameters: options.schema.toJSONSchema({
|
|
188
|
+
target: 'openapi-3.0',
|
|
189
|
+
}) as ToolInputSchema,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const res = await this.call({
|
|
194
|
+
messages: options.messages,
|
|
195
|
+
tools: [toolSchema],
|
|
196
|
+
toolChoice: 'GenerateStructuredResponse',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let completedResponse: ChatResponse;
|
|
200
|
+
if (this.stream) {
|
|
201
|
+
while (true) {
|
|
202
|
+
const { value, done } = await (res as AsyncGenerator<ChatResponse>).next();
|
|
203
|
+
if (done) {
|
|
204
|
+
completedResponse = value;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
completedResponse = res as ChatResponse;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Find the tool call
|
|
213
|
+
for (const block of completedResponse.content) {
|
|
214
|
+
if (block.type === 'tool_call' && block.name === 'GenerateStructuredResponse') {
|
|
215
|
+
const structuredContent = JSON.parse(block.input);
|
|
216
|
+
return {
|
|
217
|
+
...completedResponse,
|
|
218
|
+
content: structuredContent,
|
|
219
|
+
type: 'structured',
|
|
220
|
+
} as StructuredResponse;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw new Error(`Failed to generate the structured response`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { DashScopeChatModel } from './dashscope-model';
|
|
2
|
+
import { ChatResponse } from './response';
|
|
3
|
+
import { createMsg } from '../message';
|
|
4
|
+
|
|
5
|
+
// Mock fetch for streaming responses
|
|
6
|
+
global.fetch = jest.fn();
|
|
7
|
+
|
|
8
|
+
describe('DashScopeChatModel', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('Test stream generation with delta output', async () => {
|
|
14
|
+
// Mock streaming response with multiple chunks
|
|
15
|
+
// Chunk 1: First part of thinking
|
|
16
|
+
// Chunk 2: Second part of thinking
|
|
17
|
+
// Chunk 3: Tool call metadata (id, name) + first part of arguments
|
|
18
|
+
// Chunk 4: Second part of arguments
|
|
19
|
+
// Chunk 5: Usage info
|
|
20
|
+
const mockStreamChunks = [
|
|
21
|
+
'data:{"output":{"choices":[{"message":{"reasoning_content":"Mock thinking: Analyzing"}}]}}\n\n',
|
|
22
|
+
'data:{"output":{"choices":[{"message":{"reasoning_content":" the weather query for Beijing"}}]}}\n\n',
|
|
23
|
+
'data:{"output":{"choices":[{"message":{"tool_calls":[{"index":0,"id":"call-123","function":{"name":"get_current_weather","arguments":"{\\"location\\""}}]}}]}}\n\n',
|
|
24
|
+
'data:{"output":{"choices":[{"message":{"tool_calls":[{"index":0,"function":{"arguments":":\\"Beijing\\"}"}}]}}]}}\n\n',
|
|
25
|
+
'data:{"output":{"choices":[{"message":{}}]},"usage":{"input_tokens":100,"output_tokens":50}}\n\n',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const mockReadableStream = new ReadableStream({
|
|
29
|
+
start(controller) {
|
|
30
|
+
mockStreamChunks.forEach(chunk =>
|
|
31
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
32
|
+
);
|
|
33
|
+
controller.close();
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
38
|
+
ok: true,
|
|
39
|
+
body: mockReadableStream,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const model = new DashScopeChatModel({
|
|
43
|
+
modelName: 'qwen3.5-plus',
|
|
44
|
+
apiKey: 'test-api-key',
|
|
45
|
+
stream: true,
|
|
46
|
+
thinkingConfig: {
|
|
47
|
+
enableThinking: true,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const res = await model.call({
|
|
52
|
+
messages: [
|
|
53
|
+
createMsg({
|
|
54
|
+
name: 'user',
|
|
55
|
+
role: 'user',
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
id: crypto.randomUUID(),
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: "How's the weather today in Beijing?",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
tools: [
|
|
66
|
+
{
|
|
67
|
+
type: 'function',
|
|
68
|
+
function: {
|
|
69
|
+
name: 'get_current_weather',
|
|
70
|
+
description: 'Get the current weather in a given location',
|
|
71
|
+
parameters: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
location: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
required: ['location'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
toolChoice: 'get_current_weather',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const generator = res as AsyncGenerator<ChatResponse, ChatResponse>;
|
|
88
|
+
let completeResponse: ChatResponse | undefined;
|
|
89
|
+
const yieldedChunks: ChatResponse[] = [];
|
|
90
|
+
|
|
91
|
+
// Manual iteration to capture both yielded and returned values
|
|
92
|
+
while (true) {
|
|
93
|
+
const result = await generator.next();
|
|
94
|
+
if (result.done) {
|
|
95
|
+
completeResponse = result.value;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
yieldedChunks.push(result.value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Verify we received 5 yielded chunks (before the final return)
|
|
102
|
+
expect(yieldedChunks.length).toBe(5);
|
|
103
|
+
|
|
104
|
+
// Chunk 1: First part of thinking
|
|
105
|
+
expect(yieldedChunks[0].content.length).toBe(1);
|
|
106
|
+
expect(yieldedChunks[0].content[0]).toMatchObject({
|
|
107
|
+
type: 'thinking',
|
|
108
|
+
thinking: 'Mock thinking: Analyzing',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Chunk 2: Second part of thinking (delta)
|
|
112
|
+
expect(yieldedChunks[1].content.length).toBe(1);
|
|
113
|
+
expect(yieldedChunks[1].content[0]).toMatchObject({
|
|
114
|
+
type: 'thinking',
|
|
115
|
+
thinking: ' the weather query for Beijing',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Chunk 3: Tool call with first part of arguments
|
|
119
|
+
expect(yieldedChunks[2].content.length).toBe(1);
|
|
120
|
+
expect(yieldedChunks[2].content[0]).toMatchObject({
|
|
121
|
+
type: 'tool_call',
|
|
122
|
+
name: 'get_current_weather',
|
|
123
|
+
id: 'call-123',
|
|
124
|
+
input: '{"location"',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Chunk 4: Tool call with second part of arguments (delta)
|
|
128
|
+
expect(yieldedChunks[3].content.length).toBe(1);
|
|
129
|
+
expect(yieldedChunks[3].content[0]).toMatchObject({
|
|
130
|
+
type: 'tool_call',
|
|
131
|
+
name: 'get_current_weather',
|
|
132
|
+
id: 'call-123',
|
|
133
|
+
input: ':"Beijing"}',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Chunk 5: Empty content with usage info
|
|
137
|
+
expect(yieldedChunks[4].content.length).toBe(0);
|
|
138
|
+
expect(yieldedChunks[4].usage).toBeDefined();
|
|
139
|
+
expect(yieldedChunks[4].usage?.inputTokens).toBe(100);
|
|
140
|
+
expect(yieldedChunks[4].usage?.outputTokens).toBe(50);
|
|
141
|
+
|
|
142
|
+
// Verify the final complete response has correct structure
|
|
143
|
+
expect(completeResponse.content.length).toBe(2);
|
|
144
|
+
|
|
145
|
+
// Check thinking block - should be complete after accumulation
|
|
146
|
+
const thinkingBlock = completeResponse.content.find(b => b.type === 'thinking');
|
|
147
|
+
expect(thinkingBlock).toBeDefined();
|
|
148
|
+
expect(thinkingBlock).toMatchObject({
|
|
149
|
+
type: 'thinking',
|
|
150
|
+
thinking: 'Mock thinking: Analyzing the weather query for Beijing',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Check tool_call block - input should be complete after accumulation
|
|
154
|
+
const toolCallBlock = completeResponse.content.find(b => b.type === 'tool_call');
|
|
155
|
+
expect(toolCallBlock).toBeDefined();
|
|
156
|
+
expect(toolCallBlock).toMatchObject({
|
|
157
|
+
type: 'tool_call',
|
|
158
|
+
name: 'get_current_weather',
|
|
159
|
+
id: 'call-123',
|
|
160
|
+
input: '{"location":"Beijing"}',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Verify usage
|
|
164
|
+
expect(completeResponse.usage).toBeDefined();
|
|
165
|
+
expect(completeResponse.usage?.inputTokens).toBe(100);
|
|
166
|
+
expect(completeResponse.usage?.outputTokens).toBe(50);
|
|
167
|
+
}, 10000);
|
|
168
|
+
|
|
169
|
+
test('Test non-streaming generation', async () => {
|
|
170
|
+
// Mock non-streaming response
|
|
171
|
+
const mockResponse = {
|
|
172
|
+
output: {
|
|
173
|
+
choices: [
|
|
174
|
+
{
|
|
175
|
+
message: {
|
|
176
|
+
reasoning_content:
|
|
177
|
+
'Mock thinking: Analyzing the weather query for Beijing',
|
|
178
|
+
tool_calls: [
|
|
179
|
+
{
|
|
180
|
+
index: 0,
|
|
181
|
+
id: 'call-123',
|
|
182
|
+
function: {
|
|
183
|
+
name: 'get_current_weather',
|
|
184
|
+
arguments: '{"location":"Beijing"}',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
usage: {
|
|
193
|
+
input_tokens: 100,
|
|
194
|
+
output_tokens: 50,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
199
|
+
ok: true,
|
|
200
|
+
json: async () => mockResponse,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const model = new DashScopeChatModel({
|
|
204
|
+
modelName: 'qwen3.5-plus',
|
|
205
|
+
apiKey: 'test-api-key',
|
|
206
|
+
stream: false,
|
|
207
|
+
thinkingConfig: {
|
|
208
|
+
enableThinking: true,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const res = await model.call({
|
|
213
|
+
messages: [
|
|
214
|
+
createMsg({
|
|
215
|
+
name: 'user',
|
|
216
|
+
role: 'user',
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
id: crypto.randomUUID(),
|
|
220
|
+
type: 'text',
|
|
221
|
+
text: "How's the weather today in Beijing?",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
tools: [
|
|
227
|
+
{
|
|
228
|
+
type: 'function',
|
|
229
|
+
function: {
|
|
230
|
+
name: 'get_current_weather',
|
|
231
|
+
description: 'Get the current weather in a given location',
|
|
232
|
+
parameters: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
location: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
required: ['location'],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
toolChoice: 'get_current_weather',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const completeResponse = res as ChatResponse;
|
|
249
|
+
|
|
250
|
+
// Verify complete response structure
|
|
251
|
+
expect(completeResponse.content.length).toBe(2);
|
|
252
|
+
|
|
253
|
+
// Check thinking block
|
|
254
|
+
const thinkingBlock = completeResponse.content.find(b => b.type === 'thinking');
|
|
255
|
+
expect(thinkingBlock).toBeDefined();
|
|
256
|
+
expect(thinkingBlock).toMatchObject({
|
|
257
|
+
type: 'thinking',
|
|
258
|
+
thinking: 'Mock thinking: Analyzing the weather query for Beijing',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Check tool_call block
|
|
262
|
+
const toolCallBlock = completeResponse.content.find(b => b.type === 'tool_call');
|
|
263
|
+
expect(toolCallBlock).toBeDefined();
|
|
264
|
+
expect(toolCallBlock).toMatchObject({
|
|
265
|
+
type: 'tool_call',
|
|
266
|
+
name: 'get_current_weather',
|
|
267
|
+
id: 'call-123',
|
|
268
|
+
input: '{"location":"Beijing"}',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Verify usage
|
|
272
|
+
expect(completeResponse.usage).toBeDefined();
|
|
273
|
+
expect(completeResponse.usage?.inputTokens).toBe(100);
|
|
274
|
+
expect(completeResponse.usage?.outputTokens).toBe(50);
|
|
275
|
+
}, 10000);
|
|
276
|
+
|
|
277
|
+
test('Test formatToolChoice function', () => {
|
|
278
|
+
const model = new DashScopeChatModel({
|
|
279
|
+
modelName: 'qwen3.5-plus',
|
|
280
|
+
apiKey: 'test-api-key',
|
|
281
|
+
stream: false,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Test 'auto' case
|
|
285
|
+
expect(model['_formatToolChoice']('auto')).toBe('auto');
|
|
286
|
+
|
|
287
|
+
// Test 'none' case
|
|
288
|
+
expect(model['_formatToolChoice']('none')).toBe('none');
|
|
289
|
+
|
|
290
|
+
// Test specific function name case
|
|
291
|
+
expect(model['_formatToolChoice']('get_current_weather')).toEqual({
|
|
292
|
+
type: 'function',
|
|
293
|
+
function: {
|
|
294
|
+
name: 'get_current_weather',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Test undefined case (should default to 'auto')
|
|
299
|
+
expect(model['_formatToolChoice'](undefined)).toBe('auto');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('Test formatToolSchemas function', () => {
|
|
303
|
+
const model = new DashScopeChatModel({
|
|
304
|
+
modelName: 'qwen3.5-plus',
|
|
305
|
+
apiKey: 'test-api-key',
|
|
306
|
+
stream: false,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const toolSchemas = [
|
|
310
|
+
{
|
|
311
|
+
type: 'function' as const,
|
|
312
|
+
function: {
|
|
313
|
+
name: 'get_current_weather',
|
|
314
|
+
description: 'Get the current weather in a given location',
|
|
315
|
+
parameters: {
|
|
316
|
+
type: 'object' as const,
|
|
317
|
+
properties: {
|
|
318
|
+
location: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
required: ['location'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
// Test with tool schemas
|
|
330
|
+
expect(model['_formatToolSchemas'](toolSchemas)).toEqual(toolSchemas);
|
|
331
|
+
|
|
332
|
+
// Test with undefined (should return empty array)
|
|
333
|
+
expect(model['_formatToolSchemas'](undefined)).toEqual([]);
|
|
334
|
+
});
|
|
335
|
+
});
|