@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,307 @@
|
|
|
1
|
+
import { OllamaChatModel } from './ollama-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('OllamaChatModel', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('Test stream generation with delta output', async () => {
|
|
14
|
+
// Mock streaming response with multiple chunks (NDJSON format)
|
|
15
|
+
// Ollama returns newline-delimited JSON
|
|
16
|
+
const mockStreamChunks = [
|
|
17
|
+
'{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":"Let me"},"done":false}\n',
|
|
18
|
+
'{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":" check the weather"},"done":false}\n',
|
|
19
|
+
'{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"get_current_weather","arguments":{"location":"Beijing"}}}]},"done":false}\n',
|
|
20
|
+
'{"model":"qwen3:1.7b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":100,"eval_count":50}\n',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const mockReadableStream = new ReadableStream({
|
|
24
|
+
start(controller) {
|
|
25
|
+
mockStreamChunks.forEach(chunk =>
|
|
26
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
27
|
+
);
|
|
28
|
+
controller.close();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
33
|
+
ok: true,
|
|
34
|
+
body: mockReadableStream,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const model = new OllamaChatModel({
|
|
38
|
+
modelName: 'qwen3:1.7b',
|
|
39
|
+
stream: true,
|
|
40
|
+
host: 'http://localhost:11434',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const res = await model.call({
|
|
44
|
+
messages: [
|
|
45
|
+
createMsg({
|
|
46
|
+
name: 'user',
|
|
47
|
+
role: 'user',
|
|
48
|
+
content: [
|
|
49
|
+
{ type: 'text', text: "How's the weather today?", id: crypto.randomUUID() },
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
],
|
|
53
|
+
tools: [
|
|
54
|
+
{
|
|
55
|
+
type: 'function',
|
|
56
|
+
function: {
|
|
57
|
+
name: 'get_current_weather',
|
|
58
|
+
description: 'Get the current weather in a given location',
|
|
59
|
+
parameters: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
location: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['location'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const generator = res as AsyncGenerator<ChatResponse, ChatResponse>;
|
|
75
|
+
let completeResponse: ChatResponse | undefined;
|
|
76
|
+
const yieldedChunks: ChatResponse[] = [];
|
|
77
|
+
|
|
78
|
+
// Manual iteration to capture both yielded and returned values
|
|
79
|
+
while (true) {
|
|
80
|
+
const result = await generator.next();
|
|
81
|
+
if (result.done) {
|
|
82
|
+
completeResponse = result.value;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
yieldedChunks.push(result.value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verify we received multiple yielded chunks
|
|
89
|
+
expect(yieldedChunks.length).toBeGreaterThan(0);
|
|
90
|
+
|
|
91
|
+
// Verify the final complete response has correct structure
|
|
92
|
+
expect(completeResponse.content.length).toBe(2);
|
|
93
|
+
|
|
94
|
+
// Check text block - should be complete after accumulation
|
|
95
|
+
const textBlock = completeResponse.content.find(b => b.type === 'text');
|
|
96
|
+
expect(textBlock).toBeDefined();
|
|
97
|
+
expect(textBlock).toMatchObject({
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: 'Let me check the weather',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Check tool_call block
|
|
103
|
+
const toolCallBlock = completeResponse.content.find(b => b.type === 'tool_call');
|
|
104
|
+
expect(toolCallBlock).toBeDefined();
|
|
105
|
+
expect(toolCallBlock).toMatchObject({
|
|
106
|
+
type: 'tool_call',
|
|
107
|
+
name: 'get_current_weather',
|
|
108
|
+
input: '{"location":"Beijing"}',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Verify usage
|
|
112
|
+
expect(completeResponse.usage).toBeDefined();
|
|
113
|
+
expect(completeResponse.usage?.inputTokens).toBe(100);
|
|
114
|
+
expect(completeResponse.usage?.outputTokens).toBe(50);
|
|
115
|
+
}, 10000);
|
|
116
|
+
|
|
117
|
+
test('Test non-streaming generation', async () => {
|
|
118
|
+
// Mock non-streaming response
|
|
119
|
+
const mockResponse = {
|
|
120
|
+
model: 'qwen3:8b',
|
|
121
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
122
|
+
message: {
|
|
123
|
+
role: 'assistant',
|
|
124
|
+
content: '你好!我是一个AI助手。',
|
|
125
|
+
},
|
|
126
|
+
done: true,
|
|
127
|
+
prompt_eval_count: 50,
|
|
128
|
+
eval_count: 30,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
132
|
+
ok: true,
|
|
133
|
+
json: async () => mockResponse,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const model = new OllamaChatModel({
|
|
137
|
+
modelName: 'qwen3:8b',
|
|
138
|
+
stream: false,
|
|
139
|
+
host: 'http://localhost:11434',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const res = await model.call({
|
|
143
|
+
messages: [
|
|
144
|
+
createMsg({
|
|
145
|
+
name: 'user',
|
|
146
|
+
role: 'user',
|
|
147
|
+
content: [
|
|
148
|
+
{ type: 'text', text: '你好,请简单介绍一下自己', id: crypto.randomUUID() },
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const completeResponse = res as ChatResponse;
|
|
155
|
+
|
|
156
|
+
// Verify complete response structure
|
|
157
|
+
expect(completeResponse.content.length).toBe(1);
|
|
158
|
+
|
|
159
|
+
// Check text block
|
|
160
|
+
const textBlock = completeResponse.content.find(b => b.type === 'text');
|
|
161
|
+
expect(textBlock).toBeDefined();
|
|
162
|
+
expect(textBlock).toMatchObject({
|
|
163
|
+
type: 'text',
|
|
164
|
+
text: '你好!我是一个AI助手。',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Verify usage
|
|
168
|
+
expect(completeResponse.usage).toBeDefined();
|
|
169
|
+
expect(completeResponse.usage?.inputTokens).toBe(50);
|
|
170
|
+
expect(completeResponse.usage?.outputTokens).toBe(30);
|
|
171
|
+
}, 10000);
|
|
172
|
+
|
|
173
|
+
test('Test with thinking enabled', async () => {
|
|
174
|
+
// Mock streaming response with thinking
|
|
175
|
+
const mockStreamChunks = [
|
|
176
|
+
'{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","thinking":"计算"},"done":false}\n',
|
|
177
|
+
'{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","thinking":" 123 * 456"},"done":false}\n',
|
|
178
|
+
'{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":"答案是 56088"},"done":false}\n',
|
|
179
|
+
'{"model":"qwen3:8b","created_at":"2024-01-01T00:00:00Z","message":{"role":"assistant","content":""},"done":true,"prompt_eval_count":80,"eval_count":40}\n',
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const mockReadableStream = new ReadableStream({
|
|
183
|
+
start(controller) {
|
|
184
|
+
mockStreamChunks.forEach(chunk =>
|
|
185
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
186
|
+
);
|
|
187
|
+
controller.close();
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
(global.fetch as jest.Mock).mockResolvedValue({
|
|
192
|
+
ok: true,
|
|
193
|
+
body: mockReadableStream,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const model = new OllamaChatModel({
|
|
197
|
+
modelName: 'qwen3:8b',
|
|
198
|
+
stream: true,
|
|
199
|
+
thinkingConfig: {
|
|
200
|
+
enableThinking: true,
|
|
201
|
+
},
|
|
202
|
+
host: 'http://localhost:11434',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const res = await model.call({
|
|
206
|
+
messages: [
|
|
207
|
+
createMsg({
|
|
208
|
+
name: 'user',
|
|
209
|
+
role: 'user',
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: '计算 123 * 456 等于多少?',
|
|
214
|
+
id: crypto.randomUUID(),
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}),
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const generator = res as AsyncGenerator<ChatResponse, ChatResponse>;
|
|
222
|
+
let completeResponse: ChatResponse | undefined;
|
|
223
|
+
const yieldedChunks: ChatResponse[] = [];
|
|
224
|
+
|
|
225
|
+
while (true) {
|
|
226
|
+
const result = await generator.next();
|
|
227
|
+
if (result.done) {
|
|
228
|
+
completeResponse = result.value;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
yieldedChunks.push(result.value);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Verify we received multiple yielded chunks
|
|
235
|
+
expect(yieldedChunks.length).toBeGreaterThan(0);
|
|
236
|
+
|
|
237
|
+
// Verify the final complete response has correct structure
|
|
238
|
+
expect(completeResponse.content.length).toBe(2);
|
|
239
|
+
|
|
240
|
+
// Check thinking block
|
|
241
|
+
const thinkingBlock = completeResponse.content.find(b => b.type === 'thinking');
|
|
242
|
+
expect(thinkingBlock).toBeDefined();
|
|
243
|
+
expect(thinkingBlock).toMatchObject({
|
|
244
|
+
type: 'thinking',
|
|
245
|
+
thinking: '计算 123 * 456',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Check text block
|
|
249
|
+
const textBlock = completeResponse.content.find(b => b.type === 'text');
|
|
250
|
+
expect(textBlock).toBeDefined();
|
|
251
|
+
expect(textBlock).toMatchObject({
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: '答案是 56088',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Verify usage
|
|
257
|
+
expect(completeResponse.usage).toBeDefined();
|
|
258
|
+
expect(completeResponse.usage?.inputTokens).toBe(80);
|
|
259
|
+
expect(completeResponse.usage?.outputTokens).toBe(40);
|
|
260
|
+
}, 10000);
|
|
261
|
+
|
|
262
|
+
test('Test formatToolChoice function', () => {
|
|
263
|
+
const model = new OllamaChatModel({
|
|
264
|
+
modelName: 'qwen3:1.7b',
|
|
265
|
+
host: 'http://localhost:11434',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Ollama's _formatToolChoice always returns undefined
|
|
269
|
+
expect(model._formatToolChoice('auto')).toBeUndefined();
|
|
270
|
+
expect(model._formatToolChoice('none')).toBeUndefined();
|
|
271
|
+
expect(model._formatToolChoice('my_function')).toBeUndefined();
|
|
272
|
+
expect(model._formatToolChoice(undefined)).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('Test formatToolSchemas function', () => {
|
|
276
|
+
const model = new OllamaChatModel({
|
|
277
|
+
modelName: 'qwen3:1.7b',
|
|
278
|
+
host: 'http://localhost:11434',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const toolSchemas = [
|
|
282
|
+
{
|
|
283
|
+
type: 'function' as const,
|
|
284
|
+
function: {
|
|
285
|
+
name: 'get_current_weather',
|
|
286
|
+
description: 'Get the current weather in a given location',
|
|
287
|
+
parameters: {
|
|
288
|
+
type: 'object' as const,
|
|
289
|
+
properties: {
|
|
290
|
+
location: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ['location'],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
// Test with tool schemas
|
|
302
|
+
expect(model._formatToolSchemas(toolSchemas)).toEqual(toolSchemas);
|
|
303
|
+
|
|
304
|
+
// Test with undefined (should return empty array)
|
|
305
|
+
expect(model._formatToolSchemas(undefined)).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { Ollama, ChatResponse as OllamaChatResponse, AbortableAsyncIterator } from 'ollama';
|
|
2
|
+
|
|
3
|
+
import { ChatModelBase, ChatModelOptions, ChatModelRequestOptions } from './base';
|
|
4
|
+
import { ChatResponse } from './response';
|
|
5
|
+
import { TextBlock, ThinkingBlock, ToolCallBlock } from '../message';
|
|
6
|
+
import { ToolChoice, ToolSchema } from '../type';
|
|
7
|
+
import { ChatUsage } from './usage';
|
|
8
|
+
import { OllamaChatFormatter } from '../formatter';
|
|
9
|
+
|
|
10
|
+
interface OllamaThinkingConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Whether to enable thinking or not.
|
|
13
|
+
*/
|
|
14
|
+
enableThinking: boolean;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Thinking level for Ollama models (high, medium, low).
|
|
18
|
+
* Only applicable when enableThinking is true.
|
|
19
|
+
*/
|
|
20
|
+
thinkingLevel?: 'high' | 'medium' | 'low';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface OllamaChatModelOptions extends ChatModelOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Additional parameters to pass to the Ollama API (e.g., temperature).
|
|
26
|
+
*/
|
|
27
|
+
options?: Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Duration to keep the model loaded in memory (e.g., "5m", "1h").
|
|
31
|
+
*/
|
|
32
|
+
keepAlive?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Thinking configuration for Ollama models.
|
|
36
|
+
*/
|
|
37
|
+
thinkingConfig?: OllamaThinkingConfig;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The host address of the Ollama server.
|
|
41
|
+
*/
|
|
42
|
+
host?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extra keyword arguments to initialize the Ollama client.
|
|
46
|
+
*/
|
|
47
|
+
clientKwargs?: Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extra keyword arguments used in Ollama API generation.
|
|
51
|
+
*/
|
|
52
|
+
generateKwargs?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The Ollama chat model class in AgentScope.
|
|
57
|
+
*/
|
|
58
|
+
export class OllamaChatModel extends ChatModelBase {
|
|
59
|
+
protected client: Ollama;
|
|
60
|
+
protected options?: Record<string, unknown>;
|
|
61
|
+
protected keepAlive: string;
|
|
62
|
+
protected thinkingConfig: OllamaThinkingConfig;
|
|
63
|
+
protected generateKwargs: Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initializes a new instance of the OllamaChatModel class.
|
|
67
|
+
* @param root0
|
|
68
|
+
* @param root0.modelName
|
|
69
|
+
* @param root0.stream
|
|
70
|
+
* @param root0.options
|
|
71
|
+
* @param root0.keepAlive
|
|
72
|
+
* @param root0.thinkingConfig
|
|
73
|
+
* @param root0.host
|
|
74
|
+
* @param root0.maxRetries
|
|
75
|
+
* @param root0.fallbackModelName
|
|
76
|
+
* @param root0.clientKwargs
|
|
77
|
+
* @param root0.generateKwargs
|
|
78
|
+
* @param root0.formatter
|
|
79
|
+
*/
|
|
80
|
+
constructor({
|
|
81
|
+
modelName,
|
|
82
|
+
stream = true,
|
|
83
|
+
options,
|
|
84
|
+
keepAlive = '5m',
|
|
85
|
+
thinkingConfig,
|
|
86
|
+
host,
|
|
87
|
+
maxRetries = 0,
|
|
88
|
+
fallbackModelName,
|
|
89
|
+
clientKwargs,
|
|
90
|
+
generateKwargs,
|
|
91
|
+
formatter,
|
|
92
|
+
}: OllamaChatModelOptions) {
|
|
93
|
+
// If no formatter is provided, create a default OllamaChatFormatter
|
|
94
|
+
const defaultFormatter = formatter || new OllamaChatFormatter();
|
|
95
|
+
super({
|
|
96
|
+
modelName,
|
|
97
|
+
stream,
|
|
98
|
+
maxRetries,
|
|
99
|
+
fallbackModelName,
|
|
100
|
+
formatter: defaultFormatter,
|
|
101
|
+
} as ChatModelOptions);
|
|
102
|
+
|
|
103
|
+
this.options = options;
|
|
104
|
+
this.keepAlive = keepAlive;
|
|
105
|
+
this.thinkingConfig = thinkingConfig || {
|
|
106
|
+
enableThinking: false,
|
|
107
|
+
};
|
|
108
|
+
this.generateKwargs = generateKwargs || {};
|
|
109
|
+
|
|
110
|
+
// Initialize Ollama client
|
|
111
|
+
this.client = new Ollama({
|
|
112
|
+
host: host,
|
|
113
|
+
...clientKwargs,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Calls the Ollama API with the given parameters.
|
|
119
|
+
* @param modelName
|
|
120
|
+
* @param options
|
|
121
|
+
* @returns A promise that resolves to either a ChatResponse or an AsyncGenerator of ChatResponses.
|
|
122
|
+
*/
|
|
123
|
+
async _callAPI(
|
|
124
|
+
modelName: string,
|
|
125
|
+
options: ChatModelRequestOptions<Record<string, unknown>>
|
|
126
|
+
): Promise<ChatResponse | AsyncGenerator<ChatResponse, ChatResponse>> {
|
|
127
|
+
const kwargs: Record<string, unknown> = {
|
|
128
|
+
model: modelName,
|
|
129
|
+
messages: options.messages,
|
|
130
|
+
stream: this.stream,
|
|
131
|
+
options: this.options,
|
|
132
|
+
keep_alive: this.keepAlive,
|
|
133
|
+
...this.generateKwargs,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (this.thinkingConfig.enableThinking) {
|
|
137
|
+
// If thinkingLevel is specified, use it; otherwise use true
|
|
138
|
+
kwargs.think = this.thinkingConfig.thinkingLevel || true;
|
|
139
|
+
} else {
|
|
140
|
+
kwargs.think = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.tools) {
|
|
144
|
+
kwargs.tools = this._formatToolSchemas(options.tools);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (options.toolChoice) {
|
|
148
|
+
console.warn('Ollama does not support tool_choice yet, ignored.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
|
|
153
|
+
if (this.stream) {
|
|
154
|
+
const response = (await this.client.chat({
|
|
155
|
+
...kwargs,
|
|
156
|
+
stream: true,
|
|
157
|
+
} as Parameters<
|
|
158
|
+
typeof this.client.chat
|
|
159
|
+
>[0])) as unknown as AbortableAsyncIterator<OllamaChatResponse>;
|
|
160
|
+
return this._parseOllamaStreamResponse(response, startTime);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const response = (await this.client.chat({
|
|
164
|
+
...kwargs,
|
|
165
|
+
stream: false,
|
|
166
|
+
} as Parameters<typeof this.client.chat>[0])) as unknown as OllamaChatResponse;
|
|
167
|
+
return this._parseOllamaResponse(response, startTime);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse Ollama streaming response.
|
|
172
|
+
* @param stream
|
|
173
|
+
* @param startTime
|
|
174
|
+
* @returns An async generator that yields delta ChatResponse objects and returns the complete ChatResponse.
|
|
175
|
+
*/
|
|
176
|
+
async *_parseOllamaStreamResponse(
|
|
177
|
+
stream: AbortableAsyncIterator<OllamaChatResponse>,
|
|
178
|
+
startTime: number
|
|
179
|
+
): AsyncGenerator<ChatResponse, ChatResponse> {
|
|
180
|
+
let accText = '';
|
|
181
|
+
let accThinking = '';
|
|
182
|
+
const toolCalls: Map<string, ToolCallBlock> = new Map();
|
|
183
|
+
let lastUsage: ChatUsage | null = null;
|
|
184
|
+
|
|
185
|
+
for await (const chunk of stream) {
|
|
186
|
+
const msg = chunk.message;
|
|
187
|
+
|
|
188
|
+
// Delta data for this chunk
|
|
189
|
+
let deltaText = '';
|
|
190
|
+
let deltaThinking = '';
|
|
191
|
+
const deltaToolCalls: Map<string, ToolCallBlock> = new Map();
|
|
192
|
+
|
|
193
|
+
// Accumulate text and thinking
|
|
194
|
+
if (msg.thinking) {
|
|
195
|
+
deltaThinking = msg.thinking;
|
|
196
|
+
accThinking += msg.thinking;
|
|
197
|
+
}
|
|
198
|
+
if (msg.content) {
|
|
199
|
+
deltaText = msg.content;
|
|
200
|
+
accText += msg.content;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle tool calls
|
|
204
|
+
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
205
|
+
for (let idx = 0; idx < msg.tool_calls.length; idx++) {
|
|
206
|
+
const toolCall = msg.tool_calls[idx];
|
|
207
|
+
const func = toolCall.function;
|
|
208
|
+
const toolId = `${idx}_${func.name}`;
|
|
209
|
+
|
|
210
|
+
const toolCallBlock = {
|
|
211
|
+
type: 'tool_call' as const,
|
|
212
|
+
id: toolId,
|
|
213
|
+
name: func.name,
|
|
214
|
+
input: JSON.stringify(func.arguments),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
toolCalls.set(toolId, toolCallBlock);
|
|
218
|
+
deltaToolCalls.set(toolId, toolCallBlock);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Calculate usage
|
|
223
|
+
const currentTime = (Date.now() - startTime) / 1000;
|
|
224
|
+
lastUsage = {
|
|
225
|
+
type: 'chat_usage',
|
|
226
|
+
inputTokens: chunk.prompt_eval_count || 0,
|
|
227
|
+
outputTokens: chunk.eval_count || 0,
|
|
228
|
+
time: currentTime,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Yield delta response
|
|
232
|
+
const deltaBlocks = this._buildContentBlocks(deltaText, deltaThinking, deltaToolCalls);
|
|
233
|
+
yield {
|
|
234
|
+
type: 'chat',
|
|
235
|
+
id: crypto.randomUUID(),
|
|
236
|
+
createdAt: new Date().toISOString(),
|
|
237
|
+
content: deltaBlocks,
|
|
238
|
+
usage: lastUsage,
|
|
239
|
+
} as ChatResponse;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Return complete response
|
|
243
|
+
const blocks = this._buildContentBlocks(accText, accThinking, toolCalls);
|
|
244
|
+
return {
|
|
245
|
+
type: 'chat',
|
|
246
|
+
id: crypto.randomUUID(),
|
|
247
|
+
createdAt: new Date().toISOString(),
|
|
248
|
+
content: blocks,
|
|
249
|
+
usage: lastUsage,
|
|
250
|
+
} as ChatResponse;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse Ollama non-streaming response.
|
|
255
|
+
* @param response
|
|
256
|
+
* @param startTime
|
|
257
|
+
* @returns A ChatResponse object containing the content blocks and usage.
|
|
258
|
+
*/
|
|
259
|
+
_parseOllamaResponse(response: OllamaChatResponse, startTime: number): ChatResponse {
|
|
260
|
+
const blocks: Array<TextBlock | ThinkingBlock | ToolCallBlock> = [];
|
|
261
|
+
|
|
262
|
+
if (response.message.thinking) {
|
|
263
|
+
blocks.push({
|
|
264
|
+
id: crypto.randomUUID(),
|
|
265
|
+
type: 'thinking',
|
|
266
|
+
thinking: response.message.thinking,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (response.message.content) {
|
|
271
|
+
blocks.push({
|
|
272
|
+
id: crypto.randomUUID(),
|
|
273
|
+
type: 'text',
|
|
274
|
+
text: response.message.content,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle tool calls
|
|
279
|
+
if (response.message.tool_calls && Array.isArray(response.message.tool_calls)) {
|
|
280
|
+
for (let idx = 0; idx < response.message.tool_calls.length; idx++) {
|
|
281
|
+
const toolCall = response.message.tool_calls[idx];
|
|
282
|
+
blocks.push({
|
|
283
|
+
type: 'tool_call',
|
|
284
|
+
id: `${idx}_${toolCall.function.name}`,
|
|
285
|
+
name: toolCall.function.name,
|
|
286
|
+
input: JSON.stringify(toolCall.function.arguments),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const usage =
|
|
292
|
+
response.prompt_eval_count !== undefined && response.eval_count !== undefined
|
|
293
|
+
? {
|
|
294
|
+
type: 'chat_usage',
|
|
295
|
+
inputTokens: response.prompt_eval_count || 0,
|
|
296
|
+
outputTokens: response.eval_count || 0,
|
|
297
|
+
time: (Date.now() - startTime) / 1000,
|
|
298
|
+
}
|
|
299
|
+
: undefined;
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
type: 'chat',
|
|
303
|
+
id: crypto.randomUUID(),
|
|
304
|
+
createdAt: new Date().toISOString(),
|
|
305
|
+
content: blocks,
|
|
306
|
+
usage,
|
|
307
|
+
} as ChatResponse;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Build content blocks from accumulated data.
|
|
312
|
+
* @param text
|
|
313
|
+
* @param thinking
|
|
314
|
+
* @param toolCalls
|
|
315
|
+
* @returns An array of content blocks.
|
|
316
|
+
*/
|
|
317
|
+
_buildContentBlocks(
|
|
318
|
+
text: string,
|
|
319
|
+
thinking: string,
|
|
320
|
+
toolCalls: Map<string, ToolCallBlock>
|
|
321
|
+
): Array<TextBlock | ThinkingBlock | ToolCallBlock> {
|
|
322
|
+
const blocks: Array<TextBlock | ThinkingBlock | ToolCallBlock> = [];
|
|
323
|
+
|
|
324
|
+
if (thinking) {
|
|
325
|
+
blocks.push({ id: crypto.randomUUID(), type: 'thinking', thinking });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (text) {
|
|
329
|
+
blocks.push({ id: crypto.randomUUID(), type: 'text', text });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
toolCalls.forEach(toolCall => {
|
|
333
|
+
blocks.push(toolCall);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return blocks;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Format tool choice parameter (not supported by Ollama).
|
|
341
|
+
* @param _toolChoice
|
|
342
|
+
* @returns undefined as Ollama does not support tool choice.
|
|
343
|
+
*/
|
|
344
|
+
_formatToolChoice(_toolChoice?: ToolChoice): unknown {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Format tool schemas for Ollama API (no special formatting needed).
|
|
350
|
+
* @param tools
|
|
351
|
+
* @returns The same array of tool schemas, or an empty array if undefined.
|
|
352
|
+
*/
|
|
353
|
+
_formatToolSchemas(tools: ToolSchema[] | undefined): ToolSchema[] {
|
|
354
|
+
return tools || [];
|
|
355
|
+
}
|
|
356
|
+
}
|