@opentiny/next-sdk 0.1.14 → 0.1.15-beta.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/agent/AgentModelProvider.ts +546 -22
- package/agent/type.ts +12 -6
- package/agent/utils/generateReActPrompt.ts +55 -0
- package/agent/utils/parseReActAction.ts +34 -0
- package/dist/McpSdk.d.ts +14 -0
- package/dist/WebAgent.d.ts +5 -0
- package/dist/WebMcp.d.ts +20 -0
- package/dist/WebMcpClient.d.ts +389 -1152
- package/dist/WebMcpServer.d.ts +79 -78
- package/dist/Zod.d.ts +1 -0
- package/dist/agent/AgentModelProvider.d.ts +40 -4
- package/dist/agent/type.d.ts +13 -3
- package/dist/agent/utils/generateReActPrompt.d.ts +9 -0
- package/dist/agent/utils/getAISDKTools.d.ts +1 -0
- package/dist/agent/utils/parseReActAction.d.ts +14 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.es.dev.js +36353 -34348
- package/dist/index.es.js +28821 -25466
- package/dist/index.js +3880 -25
- package/dist/index.umd.dev.js +34230 -32225
- package/dist/index.umd.js +220 -128
- package/dist/mcpsdk@1.24.3.dev.js +22539 -0
- package/dist/mcpsdk@1.24.3.es.dev.js +22537 -0
- package/dist/mcpsdk@1.24.3.es.js +16781 -0
- package/dist/mcpsdk@1.24.3.js +43 -0
- package/dist/remoter/createRemoter.d.ts +9 -0
- package/dist/remoter/tooltips.d.ts +36 -0
- package/dist/script/utils.d.ts +1 -0
- package/dist/transport/ExtensionClientTransport.d.ts +3 -2
- package/dist/transport/ExtensionContentServerTransport.d.ts +3 -2
- package/dist/transport/ExtensionPageServerTransport.d.ts +4 -4
- package/dist/vite-build-tsc.d.ts +2 -0
- package/dist/vite.config.d.ts +2 -0
- package/dist/vite.config.mcpSdk.d.ts +2 -0
- package/dist/vite.config.webAgent.d.ts +2 -0
- package/dist/vite.config.webMcp.d.ts +2 -0
- package/dist/vite.config.webMcpFull.d.ts +2 -0
- package/dist/vite.config.zod.d.ts +2 -0
- package/dist/webagent.dev.js +24569 -20836
- package/dist/webagent.es.dev.js +23907 -20174
- package/dist/webagent.es.js +25326 -20723
- package/dist/webagent.js +209 -110
- package/dist/webmcp-full.dev.js +21225 -20021
- package/dist/webmcp-full.es.dev.js +21223 -20019
- package/dist/webmcp-full.es.js +16710 -14437
- package/dist/webmcp-full.js +42 -15
- package/dist/webmcp.dev.js +14 -22
- package/dist/webmcp.es.dev.js +12 -20
- package/dist/webmcp.es.js +172 -179
- package/dist/webmcp.js +1 -1
- package/dist/zod@3.25.76.dev.js +30 -32
- package/dist/zod@3.25.76.es.dev.js +28 -30
- package/dist/zod@3.25.76.es.js +143 -145
- package/dist/zod@3.25.76.js +1 -1
- package/package.json +11 -9
- package/remoter/createRemoter.ts +126 -71
- package/remoter/tooltips.ts +260 -0
- package/transport/ExtensionPageServerTransport.ts +2 -4
- package/tsconfig.json +5 -3
- package/vite-build-tsc.ts +60 -0
- package/vite-env.d.ts +5 -0
- package/dist/WebMcpClient.js +0 -363
- package/dist/WebMcpServer.js +0 -283
- package/dist/agent/AgentModelProvider.js +0 -293
- package/dist/agent/type.js +0 -1
- package/dist/agent/utils/getAISDKTools.js +0 -36
- package/dist/mcpsdk@1.17.0.dev.js +0 -21391
- package/dist/mcpsdk@1.17.0.es.dev.js +0 -21389
- package/dist/mcpsdk@1.17.0.es.js +0 -14505
- package/dist/mcpsdk@1.17.0.js +0 -16
- package/dist/remoter/QrCode.js +0 -55
- package/dist/remoter/createRemoter.js +0 -743
- package/dist/transport/ExtensionClientTransport.js +0 -81
- package/dist/transport/ExtensionContentServerTransport.js +0 -128
- package/dist/transport/ExtensionPageServerTransport.js +0 -118
- package/dist/transport/messages.js +0 -51
- package/dist/utils/uuid.js +0 -10
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { streamText, stepCountIs, generateText, StreamTextResult } from 'ai'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
experimental_MCPClientConfig as MCPClientConfig,
|
|
4
|
+
experimental_createMCPClient as createMCPClient
|
|
5
|
+
} from '@ai-sdk/mcp'
|
|
3
6
|
import type { ToolSet } from 'ai'
|
|
4
7
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
5
8
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
@@ -12,6 +15,8 @@ import { ExtensionClientTransport } from '../transport/ExtensionClientTransport'
|
|
|
12
15
|
import { MessageChannelTransport } from '@opentiny/next'
|
|
13
16
|
import { WebMcpClient } from '../WebMcpClient'
|
|
14
17
|
import { getAISDKTools } from './utils/getAISDKTools'
|
|
18
|
+
import { generateReActToolsPrompt } from './utils/generateReActPrompt'
|
|
19
|
+
import { parseReActAction } from './utils/parseReActAction'
|
|
15
20
|
|
|
16
21
|
export const AIProviderFactories = {
|
|
17
22
|
['openai']: createOpenAI,
|
|
@@ -43,6 +48,8 @@ export class AgentModelProvider {
|
|
|
43
48
|
onClientDisconnected?: (serverName: string, reason?: string) => void
|
|
44
49
|
/** 缓存 ai-sdk response 中的 多轮会话的上下文 */
|
|
45
50
|
messages: any[] = []
|
|
51
|
+
/** 是否使用 ReAct 模式(通过提示词而非 function calling 进行工具调用) */
|
|
52
|
+
useReActMode: boolean = false
|
|
46
53
|
|
|
47
54
|
constructor({ llmConfig, mcpServers }: IAgentModelProviderOption) {
|
|
48
55
|
if (!llmConfig) {
|
|
@@ -70,6 +77,9 @@ export class AgentModelProvider {
|
|
|
70
77
|
} else {
|
|
71
78
|
throw new Error('Either llmConfig.llm or llmConfig.providerType must be provided')
|
|
72
79
|
}
|
|
80
|
+
|
|
81
|
+
// 读取 ReAct 模式配置
|
|
82
|
+
this.useReActMode = (llmConfig as any).useReActMode ?? false
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/** 创建一个 ai-sdk的 mcpClient, 创建失败则返回 null */
|
|
@@ -85,16 +95,29 @@ export class AgentModelProvider {
|
|
|
85
95
|
transport = serverConfig as MCPClientConfig['transport']
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{ capabilities: { roots: { listChanged: true }, sampling: {}, elicitation: {} } }
|
|
91
|
-
)
|
|
92
|
-
await client.connect(transport)
|
|
93
|
-
|
|
94
|
-
//@ts-ignore
|
|
95
|
-
client['__transport__'] = transport
|
|
98
|
+
// 根据 useAISdkClient 配置决定使用哪种 client 创建方式
|
|
99
|
+
const useAISdkClient = serverConfig.useAISdkClient ?? false
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
if (useAISdkClient) {
|
|
102
|
+
// 使用 ai-sdk 的 createMCPClient
|
|
103
|
+
const client = await createMCPClient({ transport: transport as MCPClientConfig['transport'] })
|
|
104
|
+
//@ts-ignore
|
|
105
|
+
client['__transport__'] = transport
|
|
106
|
+
return client
|
|
107
|
+
} else {
|
|
108
|
+
// 使用 WebMcpClient
|
|
109
|
+
const client = new WebMcpClient(
|
|
110
|
+
{ name: 'mcp-web-client', version: '1.0.0' },
|
|
111
|
+
{ capabilities: { roots: { listChanged: true }, sampling: {}, elicitation: {} } }
|
|
112
|
+
)
|
|
113
|
+
// @ts-ignore transport 已经在前面的条件分支中转换为 Transport 实例,类型系统无法正确推断
|
|
114
|
+
await client.connect(transport)
|
|
115
|
+
|
|
116
|
+
//@ts-ignore
|
|
117
|
+
client['__transport__'] = transport
|
|
118
|
+
|
|
119
|
+
return client
|
|
120
|
+
}
|
|
98
121
|
} catch (error: unknown) {
|
|
99
122
|
if (this.onError) {
|
|
100
123
|
this.onError((error as Error)?.message || `Failed to create MCP client`, error)
|
|
@@ -108,7 +131,8 @@ export class AgentModelProvider {
|
|
|
108
131
|
try {
|
|
109
132
|
const transport = client['__transport__']
|
|
110
133
|
|
|
111
|
-
// 如果是 InMemoryTransport,不关闭传输层
|
|
134
|
+
// 如果是 InMemoryTransport 或 MessageChannelTransport,不关闭传输层
|
|
135
|
+
// 因为它们是配对的,关闭一端会影响另一端(服务端)
|
|
112
136
|
if (
|
|
113
137
|
(transport && transport instanceof InMemoryTransport) ||
|
|
114
138
|
(transport && transport instanceof MessageChannelTransport)
|
|
@@ -116,6 +140,11 @@ export class AgentModelProvider {
|
|
|
116
140
|
return
|
|
117
141
|
}
|
|
118
142
|
|
|
143
|
+
// 因为它们是基于 Chrome 扩展的消息传递机制,关闭会影响服务端的连接
|
|
144
|
+
if (transport && transport instanceof ExtensionClientTransport) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
119
148
|
// 其他类型的传输正常关闭
|
|
120
149
|
await transport?.terminateSession?.()
|
|
121
150
|
await transport?.close?.()
|
|
@@ -138,21 +167,37 @@ export class AgentModelProvider {
|
|
|
138
167
|
this.mcpClients[serverName] = client
|
|
139
168
|
})
|
|
140
169
|
}
|
|
170
|
+
/** 兼容两种 client 类型的 tools 获取方法 */
|
|
171
|
+
private async _getClientTools(client: any, serverName: string): Promise<ToolSet | null> {
|
|
172
|
+
if (!client) {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// 判断是否为 ai-sdk 的 client(有 tools 方法)
|
|
178
|
+
if (typeof client.tools === 'function') {
|
|
179
|
+
// ai-sdk 的 client,直接调用 tools() 方法
|
|
180
|
+
return await client.tools()
|
|
181
|
+
} else {
|
|
182
|
+
// WebMcpClient,使用 getAISDKTools 函数
|
|
183
|
+
return await getAISDKTools(client)
|
|
184
|
+
}
|
|
185
|
+
} catch (error: unknown) {
|
|
186
|
+
if (this.onError) {
|
|
187
|
+
this.onError((error as Error)?.message || `Failed to query tools for ${serverName}`, error)
|
|
188
|
+
}
|
|
189
|
+
console.error(`Failed to query tools for ${serverName}`, error)
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
141
194
|
/** 查询所有 mcpClients 的 tools, 失败则保存为null */
|
|
142
195
|
private async _createMpcTools() {
|
|
143
196
|
const clientEntries = Object.entries(this.mcpClients)
|
|
144
197
|
const tools = await Promise.all(
|
|
145
198
|
clientEntries.map(async ([serverName, client]) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return { serverName, tools: result }
|
|
149
|
-
} catch (error: unknown) {
|
|
150
|
-
if (this.onError) {
|
|
151
|
-
this.onError((error as Error)?.message || `Failed to query tools`, error)
|
|
152
|
-
}
|
|
153
|
-
console.error(`Failed to query tools`, error)
|
|
154
|
-
return { serverName, tools: null }
|
|
155
|
-
}
|
|
199
|
+
const result = await this._getClientTools(client, serverName)
|
|
200
|
+
return { serverName, tools: result }
|
|
156
201
|
})
|
|
157
202
|
)
|
|
158
203
|
// 将结果存储到对象中,使用 serverName 作为键
|
|
@@ -206,7 +251,8 @@ export class AgentModelProvider {
|
|
|
206
251
|
return null
|
|
207
252
|
}
|
|
208
253
|
this.mcpClients[serverName] = client
|
|
209
|
-
|
|
254
|
+
// 使用兼容的工具获取方法
|
|
255
|
+
const tools = await this._getClientTools(client, serverName)
|
|
210
256
|
// 工具列表可能为 null,统一兜底为空对象,确保类型安全
|
|
211
257
|
this.mcpTools[serverName] = tools && typeof tools === 'object' ? (tools as Record<string, any>) : {}
|
|
212
258
|
this.mcpServers[serverName] = mcpServer
|
|
@@ -255,10 +301,488 @@ export class AgentModelProvider {
|
|
|
255
301
|
return toolsResult
|
|
256
302
|
}
|
|
257
303
|
|
|
304
|
+
/** 生成 ReAct 模式的系统提示词(包含工具描述) */
|
|
305
|
+
private _generateReActSystemPrompt(tools: ToolSet, modelName: string, baseSystemPrompt?: string): string {
|
|
306
|
+
// 统一使用 XML 格式的 ReAct 提示词(所有 ReAct 模式都使用相同格式)
|
|
307
|
+
const toolsPrompt = generateReActToolsPrompt(tools)
|
|
308
|
+
|
|
309
|
+
if (baseSystemPrompt) {
|
|
310
|
+
return `${baseSystemPrompt}${toolsPrompt}`
|
|
311
|
+
}
|
|
312
|
+
return `你是一个智能助手,可以通过调用工具来完成任务。\n${toolsPrompt}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 执行 ReAct 模式下的工具调用 */
|
|
316
|
+
private async _executeReActToolCall(
|
|
317
|
+
toolName: string,
|
|
318
|
+
args: any,
|
|
319
|
+
tools: ToolSet
|
|
320
|
+
): Promise<{ success: boolean; result?: any; error?: string }> {
|
|
321
|
+
const tool = tools[toolName]
|
|
322
|
+
if (!tool) {
|
|
323
|
+
return { success: false, error: `工具 ${toolName} 不存在` }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const toolInfo = tool as any
|
|
328
|
+
const executeFn = toolInfo.execute || toolInfo.call
|
|
329
|
+
if (typeof executeFn !== 'function') {
|
|
330
|
+
return { success: false, error: `工具 ${toolName} 没有可执行的函数` }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const result = await executeFn(args, {})
|
|
334
|
+
return { success: true, result }
|
|
335
|
+
} catch (error: any) {
|
|
336
|
+
const errorMsg = error?.message || String(error) || '工具执行失败'
|
|
337
|
+
return { success: false, error: errorMsg }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** ReAct 模式的对话实现 */
|
|
342
|
+
private async _chatReAct(
|
|
343
|
+
chatMethod: ChatMethodFn,
|
|
344
|
+
{ model, maxSteps = 5, ...options }: Parameters<typeof generateText>[0] & { maxSteps?: number; message?: string }
|
|
345
|
+
): Promise<any> {
|
|
346
|
+
if (!this.llm) {
|
|
347
|
+
throw new Error('LLM is not initialized')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await this.initClientsAndTools()
|
|
351
|
+
|
|
352
|
+
// 合并所有可用工具
|
|
353
|
+
const allTools = this._tempMergeTools(options.tools) as ToolSet
|
|
354
|
+
const toolNames = Object.keys(allTools)
|
|
355
|
+
|
|
356
|
+
// 如果没有工具,回退到普通模式
|
|
357
|
+
if (toolNames.length === 0) {
|
|
358
|
+
return this._chat(chatMethod, { model, maxSteps, ...options })
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 准备消息历史
|
|
362
|
+
let currentMessages: any[] = []
|
|
363
|
+
if (options.message && !options.messages) {
|
|
364
|
+
currentMessages.push({ role: 'user', content: options.message })
|
|
365
|
+
} else if (options.messages) {
|
|
366
|
+
currentMessages = [...options.messages]
|
|
367
|
+
} else {
|
|
368
|
+
currentMessages = [...this.messages]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 确保 model 是字符串类型(ReAct 模式下 model 应该是模型名称字符串)
|
|
372
|
+
const modelName = typeof model === 'string' ? model : (model as any)?.modelId || 'default-model'
|
|
373
|
+
|
|
374
|
+
// 生成包含工具描述的系统提示词
|
|
375
|
+
const systemPrompt = this._generateReActSystemPrompt(allTools, modelName, options.system as string)
|
|
376
|
+
|
|
377
|
+
const systemMessage = { role: 'system', content: systemPrompt }
|
|
378
|
+
|
|
379
|
+
// 确保第一条消息是系统提示词
|
|
380
|
+
const messagesWithSystem =
|
|
381
|
+
currentMessages[0]?.role === 'system' ? currentMessages : [systemMessage, ...currentMessages]
|
|
382
|
+
|
|
383
|
+
// 判断是否为流式输出
|
|
384
|
+
const isStream = chatMethod === streamText
|
|
385
|
+
|
|
386
|
+
if (isStream) {
|
|
387
|
+
// 流式输出模式:创建一个包装的流
|
|
388
|
+
return this._chatReActStream(messagesWithSystem, allTools, modelName, maxSteps, options)
|
|
389
|
+
} else {
|
|
390
|
+
// 非流式输出模式:循环对话直到完成
|
|
391
|
+
return this._chatReActNonStream(messagesWithSystem, allTools, modelName, maxSteps, options)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 检查消息内容是否包含图片
|
|
397
|
+
* @param content 消息内容
|
|
398
|
+
* @returns 是否包含图片
|
|
399
|
+
*/
|
|
400
|
+
private _messageHasImage(content: any): boolean {
|
|
401
|
+
if (!content) return false
|
|
402
|
+
|
|
403
|
+
// 如果 content 是数组,检查是否有 image 类型的项
|
|
404
|
+
if (Array.isArray(content)) {
|
|
405
|
+
return content.some((item) => item && item.type === 'image')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 从消息中移除图片,但保留文本内容
|
|
413
|
+
* @param message 原始消息
|
|
414
|
+
* @returns 移除图片后的消息(如果只有图片没有文本,返回 null)
|
|
415
|
+
*/
|
|
416
|
+
private _removeImageFromMessage(message: any): any | null {
|
|
417
|
+
if (!message || !message.content) {
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 如果 content 不是数组,直接返回(没有图片)
|
|
422
|
+
if (!Array.isArray(message.content)) {
|
|
423
|
+
return message
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 过滤掉图片类型的内容,保留文本
|
|
427
|
+
const textContent = message.content.filter((item: any) => item && item.type !== 'image')
|
|
428
|
+
|
|
429
|
+
// 如果过滤后没有内容,返回 null
|
|
430
|
+
if (textContent.length === 0) {
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 返回只包含文本的消息副本
|
|
435
|
+
return {
|
|
436
|
+
...message,
|
|
437
|
+
content: textContent
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 构建用于模型调用的消息列表(magentic-ui 风格)
|
|
443
|
+
* 策略:保留所有文本消息,仅限制图片数量(类似 magentic-ui 的 maybe_remove_old_screenshots)
|
|
444
|
+
*
|
|
445
|
+
* @param systemMessage 系统提示词
|
|
446
|
+
* @param allMessages 所有消息历史(包括初始消息和后续对话)
|
|
447
|
+
* @param maxImages 最多保留的图片数量(默认3张)
|
|
448
|
+
* @returns 构建好的消息列表
|
|
449
|
+
*/
|
|
450
|
+
private _buildMessagesForModel(systemMessage: any | null, allMessages: any[], maxImages: number = 3): any[] {
|
|
451
|
+
const messages: any[] = []
|
|
452
|
+
|
|
453
|
+
// 1. 添加系统提示词
|
|
454
|
+
if (systemMessage) {
|
|
455
|
+
messages.push(systemMessage)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 2. 保留所有文本消息,但限制图片数量
|
|
459
|
+
// 从后往前遍历,优先保留最新的图片
|
|
460
|
+
let imageCount = 0
|
|
461
|
+
const processedMessages: any[] = []
|
|
462
|
+
|
|
463
|
+
for (let i = allMessages.length - 1; i >= 0; i--) {
|
|
464
|
+
const msg = allMessages[i]
|
|
465
|
+
|
|
466
|
+
// 检查消息是否包含图片
|
|
467
|
+
const hasImage = this._messageHasImage(msg.content)
|
|
468
|
+
|
|
469
|
+
if (hasImage) {
|
|
470
|
+
if (imageCount < maxImages) {
|
|
471
|
+
// 图片数量未超限,保留完整消息
|
|
472
|
+
processedMessages.unshift(msg)
|
|
473
|
+
imageCount++
|
|
474
|
+
} else {
|
|
475
|
+
// 图片数量超限,移除图片但保留文本(如果有)
|
|
476
|
+
const textOnly = this._removeImageFromMessage(msg)
|
|
477
|
+
if (textOnly) {
|
|
478
|
+
processedMessages.unshift(textOnly)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
// 非图片消息:全部保留
|
|
483
|
+
processedMessages.unshift(msg)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
messages.push(...processedMessages)
|
|
488
|
+
|
|
489
|
+
return messages
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** ReAct 模式非流式对话 */
|
|
493
|
+
private async _chatReActNonStream(
|
|
494
|
+
messages: any[],
|
|
495
|
+
tools: ToolSet,
|
|
496
|
+
model: string,
|
|
497
|
+
maxSteps: number,
|
|
498
|
+
options: any
|
|
499
|
+
): Promise<any> {
|
|
500
|
+
// 保存完整的消息历史(用于最终返回和传递给模型)
|
|
501
|
+
let fullMessageHistory = [...messages]
|
|
502
|
+
// 提取系统提示词(第一条消息)
|
|
503
|
+
const systemMessage = messages[0]?.role === 'system' ? messages[0] : null
|
|
504
|
+
// 提取所有非系统消息
|
|
505
|
+
const allUserMessages = systemMessage ? messages.slice(1) : messages
|
|
506
|
+
|
|
507
|
+
let stepCount = 0
|
|
508
|
+
// 配置:最多保留的图片数量(默认3张,类似 magentic-ui)
|
|
509
|
+
const maxImages = (options as any).maxImages ?? 3
|
|
510
|
+
|
|
511
|
+
while (stepCount < maxSteps) {
|
|
512
|
+
stepCount++
|
|
513
|
+
|
|
514
|
+
// 构建用于模型调用的消息列表(magentic-ui 风格:保留所有文本,限制图片)
|
|
515
|
+
const messagesForModel = this._buildMessagesForModel(systemMessage, allUserMessages, maxImages)
|
|
516
|
+
|
|
517
|
+
// 调用 LLM(ReAct 模式下不传递 tools,因为工具调用通过提示词实现)
|
|
518
|
+
// 参考 magentic-ui:保留所有文本历史(上下文完整),仅限制图片数量(优化 token)
|
|
519
|
+
const { tools: _, ...restOptions } = options
|
|
520
|
+
const result = await generateText({
|
|
521
|
+
// @ts-ignore ProviderV2 是所有llm的父类,在每一个具体的llm类都有一个选择model的函数用法
|
|
522
|
+
model: this.llm(model),
|
|
523
|
+
messages: messagesForModel,
|
|
524
|
+
...restOptions
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
const assistantMessage = result.text
|
|
528
|
+
// 添加到所有消息和完整历史
|
|
529
|
+
const assistantMsg = { role: 'assistant', content: assistantMessage }
|
|
530
|
+
allUserMessages.push(assistantMsg)
|
|
531
|
+
fullMessageHistory.push(assistantMsg)
|
|
532
|
+
|
|
533
|
+
// 解析工具调用
|
|
534
|
+
const action = parseReActAction(assistantMessage, tools)
|
|
535
|
+
|
|
536
|
+
if (!action) {
|
|
537
|
+
// 没有工具调用,返回最终结果
|
|
538
|
+
this.messages = fullMessageHistory
|
|
539
|
+
return {
|
|
540
|
+
text: assistantMessage,
|
|
541
|
+
response: { messages: fullMessageHistory }
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 执行工具调用
|
|
546
|
+
const toolResult = await this._executeReActToolCall(action.toolName, action.arguments, tools)
|
|
547
|
+
|
|
548
|
+
// 统一使用 XML 格式的 Observation
|
|
549
|
+
const resultString = toolResult.success ? JSON.stringify(toolResult.result) : `工具执行失败 - ${toolResult.error}`
|
|
550
|
+
const observation = `<tool_response>\n${resultString}\n</tool_response>`
|
|
551
|
+
|
|
552
|
+
// 添加到所有消息和完整历史
|
|
553
|
+
const observationMessage = {
|
|
554
|
+
role: 'user',
|
|
555
|
+
content: observation
|
|
556
|
+
}
|
|
557
|
+
allUserMessages.push(observationMessage)
|
|
558
|
+
fullMessageHistory.push(observationMessage)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 达到最大步数,返回最后一条消息
|
|
562
|
+
this.messages = fullMessageHistory
|
|
563
|
+
const lastMessage = fullMessageHistory[fullMessageHistory.length - 2]?.content || ''
|
|
564
|
+
return {
|
|
565
|
+
text: lastMessage,
|
|
566
|
+
response: { messages: fullMessageHistory }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** ReAct 模式流式对话 */
|
|
571
|
+
private _chatReActStream(messages: any[], tools: ToolSet, model: string, maxSteps: number, options: any): any {
|
|
572
|
+
// 保存 this 引用,以便在异步生成器中使用
|
|
573
|
+
const self = this
|
|
574
|
+
// @ts-ignore ProviderV2 是所有llm的父类,在每一个具体的llm类都有一个选择model的函数用法
|
|
575
|
+
const llmModel = this.llm(model)
|
|
576
|
+
|
|
577
|
+
// 创建一个 Promise 来跟踪流完成状态,用于触发 onFinish
|
|
578
|
+
let streamCompleteResolver: (value: any) => void
|
|
579
|
+
let streamCompleteRejecter: (error: any) => void
|
|
580
|
+
const streamCompletePromise = new Promise((resolve, reject) => {
|
|
581
|
+
streamCompleteResolver = resolve
|
|
582
|
+
streamCompleteRejecter = reject
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
// 创建一个异步生成器来模拟流式输出
|
|
586
|
+
const stream = new ReadableStream({
|
|
587
|
+
async start(controller) {
|
|
588
|
+
// 保存完整的消息历史(用于最终返回和传递给模型)
|
|
589
|
+
let fullMessageHistory = [...messages]
|
|
590
|
+
// 提取系统提示词(第一条消息)
|
|
591
|
+
const systemMessage = messages[0]?.role === 'system' ? messages[0] : null
|
|
592
|
+
// 提取所有非系统消息
|
|
593
|
+
const allUserMessages = systemMessage ? messages.slice(1) : [...messages]
|
|
594
|
+
|
|
595
|
+
let stepCount = 0
|
|
596
|
+
let accumulatedText = ''
|
|
597
|
+
// 配置:最多保留的图片数量(默认3张,类似 magentic-ui)
|
|
598
|
+
const maxImages = (options as any).maxImages ?? 3
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
while (stepCount < maxSteps) {
|
|
602
|
+
stepCount++
|
|
603
|
+
|
|
604
|
+
// 构建用于模型调用的消息列表(magentic-ui 风格:保留所有文本,限制图片)
|
|
605
|
+
const messagesForModel = self._buildMessagesForModel(systemMessage, allUserMessages, maxImages)
|
|
606
|
+
|
|
607
|
+
// 移除 tools 选项,ReAct 模式下不传递 tools
|
|
608
|
+
const { tools: _, ...restOptions } = options
|
|
609
|
+
// 调用流式 LLM
|
|
610
|
+
// 参考 magentic-ui:保留所有文本历史(上下文完整),仅限制图片数量(优化 token)
|
|
611
|
+
delete restOptions.system
|
|
612
|
+
const result = await streamText({
|
|
613
|
+
...restOptions,
|
|
614
|
+
model: llmModel,
|
|
615
|
+
messages: messagesForModel
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
// 收集流式输出
|
|
619
|
+
let assistantText = ''
|
|
620
|
+
for await (const part of result.fullStream) {
|
|
621
|
+
if (part.type === 'text-delta') {
|
|
622
|
+
assistantText += part.text || ''
|
|
623
|
+
// 转发文本增量
|
|
624
|
+
controller.enqueue({
|
|
625
|
+
type: 'text-delta',
|
|
626
|
+
text: part.text
|
|
627
|
+
})
|
|
628
|
+
} else if (part.type === 'text-start') {
|
|
629
|
+
controller.enqueue({ type: 'text-start' })
|
|
630
|
+
} else if (part.type === 'text-end') {
|
|
631
|
+
// 暂时不关闭,等待检查是否有工具调用
|
|
632
|
+
} else {
|
|
633
|
+
// 转发其他类型的事件
|
|
634
|
+
controller.enqueue(part)
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
accumulatedText += assistantText
|
|
639
|
+
// 添加到所有消息和完整历史
|
|
640
|
+
const assistantMsg = { role: 'assistant', content: accumulatedText }
|
|
641
|
+
allUserMessages.push(assistantMsg)
|
|
642
|
+
fullMessageHistory.push(assistantMsg)
|
|
643
|
+
|
|
644
|
+
// 解析工具调用
|
|
645
|
+
const action = parseReActAction(accumulatedText, tools)
|
|
646
|
+
|
|
647
|
+
if (!action) {
|
|
648
|
+
// 没有工具调用,结束流
|
|
649
|
+
controller.enqueue({ type: 'text-end' })
|
|
650
|
+
controller.close()
|
|
651
|
+
self.messages = fullMessageHistory
|
|
652
|
+
// 触发 onFinish 回调
|
|
653
|
+
streamCompleteResolver({ messages: fullMessageHistory })
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 特殊处理: computer 工具的 terminate 操作
|
|
658
|
+
if (action.toolName === 'computer' && action.arguments?.action === 'terminate') {
|
|
659
|
+
// 视为对话结束
|
|
660
|
+
controller.enqueue({ type: 'text-end' })
|
|
661
|
+
controller.close()
|
|
662
|
+
self.messages = fullMessageHistory
|
|
663
|
+
streamCompleteResolver({ messages: fullMessageHistory })
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// 发送工具调用开始事件(符合 tiny-robot 格式)
|
|
668
|
+
const toolCallId = `react-${Date.now()}`
|
|
669
|
+
controller.enqueue({
|
|
670
|
+
type: 'tool-input-start',
|
|
671
|
+
id: toolCallId,
|
|
672
|
+
toolName: action.toolName
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
// 发送工具调用参数(显示调用中状态)
|
|
676
|
+
const argsString = JSON.stringify(action.arguments, null, 2)
|
|
677
|
+
controller.enqueue({
|
|
678
|
+
type: 'tool-input-delta',
|
|
679
|
+
id: toolCallId,
|
|
680
|
+
delta: argsString
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
// 执行工具调用
|
|
684
|
+
const toolResult = await self._executeReActToolCall(action.toolName, action.arguments, tools)
|
|
685
|
+
|
|
686
|
+
// 如果结果包含 screenshot,先提取出来,避免 JSON stringify 导致过大
|
|
687
|
+
let screenshot = undefined
|
|
688
|
+
let resultData = toolResult.result
|
|
689
|
+
if (
|
|
690
|
+
toolResult.success &&
|
|
691
|
+
toolResult.result &&
|
|
692
|
+
typeof toolResult.result === 'object' &&
|
|
693
|
+
toolResult.result.screenshot
|
|
694
|
+
) {
|
|
695
|
+
screenshot = toolResult.result.screenshot
|
|
696
|
+
const { screenshot: _, ...rest } = toolResult.result
|
|
697
|
+
resultData = rest
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// 构造 Observation 文本(统一使用 XML 格式)
|
|
701
|
+
let observationText = ''
|
|
702
|
+
|
|
703
|
+
if (toolResult.success) {
|
|
704
|
+
// 尝试从 resultData 中提取纯文本信息
|
|
705
|
+
if (
|
|
706
|
+
resultData &&
|
|
707
|
+
Array.isArray(resultData.content) &&
|
|
708
|
+
resultData.content.length > 0 &&
|
|
709
|
+
resultData.content[0].text
|
|
710
|
+
) {
|
|
711
|
+
observationText = resultData.content[0].text
|
|
712
|
+
} else {
|
|
713
|
+
observationText = JSON.stringify(resultData)
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
observationText = `工具执行失败 - ${toolResult.error}`
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 统一使用 XML 格式的 Observation,如果有截图,添加验证提示
|
|
720
|
+
let finalObservation = `<tool_response>\n${observationText}\n</tool_response>`
|
|
721
|
+
|
|
722
|
+
if (screenshot) {
|
|
723
|
+
finalObservation += `\n请检查截图以确认操作是否成功。如果成功,请继续下一步;如果失败,请重试。`
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 发送工具结果(符合 tiny-robot 格式,给 UI 展示用的,不包含 base64 防止卡顿)
|
|
727
|
+
controller.enqueue({
|
|
728
|
+
type: 'tool-result',
|
|
729
|
+
toolCallId: toolCallId,
|
|
730
|
+
result: finalObservation
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
// 添加工具结果到消息历史(ReAct 模式下,工具结果作为 user 消息添加)
|
|
734
|
+
const observationMessage = screenshot
|
|
735
|
+
? {
|
|
736
|
+
role: 'user',
|
|
737
|
+
content: [
|
|
738
|
+
{ type: 'text', text: finalObservation },
|
|
739
|
+
{ type: 'image', image: screenshot }
|
|
740
|
+
]
|
|
741
|
+
}
|
|
742
|
+
: {
|
|
743
|
+
role: 'user',
|
|
744
|
+
content: finalObservation
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 添加到所有消息和完整历史
|
|
748
|
+
allUserMessages.push(observationMessage)
|
|
749
|
+
fullMessageHistory.push(observationMessage)
|
|
750
|
+
|
|
751
|
+
// 重置累积文本,准备下一轮
|
|
752
|
+
accumulatedText = ''
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// 达到最大步数
|
|
756
|
+
controller.enqueue({ type: 'text-end' })
|
|
757
|
+
controller.close()
|
|
758
|
+
self.messages = fullMessageHistory
|
|
759
|
+
// 触发 onFinish 回调
|
|
760
|
+
streamCompleteResolver({ messages: fullMessageHistory })
|
|
761
|
+
} catch (error: any) {
|
|
762
|
+
controller.error(error)
|
|
763
|
+
streamCompleteRejecter(error)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
// 返回一个类似 streamText 的结果对象
|
|
769
|
+
// response Promise 需要在流结束时 resolve,这样才能触发 onFinish 回调
|
|
770
|
+
return {
|
|
771
|
+
fullStream: stream,
|
|
772
|
+
response: streamCompletePromise
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
258
776
|
private async _chat(
|
|
259
777
|
chatMethod: ChatMethodFn,
|
|
260
778
|
{ model, maxSteps = 5, ...options }: Parameters<typeof generateText>[0] & { maxSteps?: number; message?: string }
|
|
261
779
|
): Promise<any> {
|
|
780
|
+
// 如果启用 ReAct 模式,使用 ReAct 实现
|
|
781
|
+
if (this.useReActMode) {
|
|
782
|
+
return this._chatReAct(chatMethod, { model, maxSteps, ...options })
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 否则使用原有的 function calling 模式
|
|
262
786
|
if (!this.llm) {
|
|
263
787
|
throw new Error('LLM is not initialized')
|
|
264
788
|
}
|
package/agent/type.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export type { experimental_MCPClient as MCPClient } from 'ai'
|
|
2
1
|
import type { ProviderV2 } from '@ai-sdk/provider'
|
|
3
|
-
import type {
|
|
2
|
+
import type { experimental_MCPClientConfig as MCPClientConfig } from '@ai-sdk/mcp'
|
|
3
|
+
|
|
4
|
+
// 从 MCPClientConfig 中提取 transport 类型
|
|
5
|
+
export type MCPTransport = MCPClientConfig['transport']
|
|
4
6
|
|
|
5
7
|
type ProviderFactory = 'openai' | 'deepseek' | ((options: any) => ProviderV2)
|
|
6
8
|
|
|
@@ -13,6 +15,8 @@ type LlmFactoryConfig = {
|
|
|
13
15
|
providerType: ProviderFactory
|
|
14
16
|
/** 互斥:当使用 providerType 分支时不允许传入 llm */
|
|
15
17
|
llm?: never
|
|
18
|
+
/** 是否使用 ReAct 模式(通过提示词而非 function calling 进行工具调用),默认为 false */
|
|
19
|
+
useReActMode?: boolean
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
type LlmInstanceConfig = {
|
|
@@ -22,6 +26,8 @@ type LlmInstanceConfig = {
|
|
|
22
26
|
apiKey?: never
|
|
23
27
|
baseURL?: never
|
|
24
28
|
providerType?: never
|
|
29
|
+
/** 是否使用 ReAct 模式(通过提示词而非 function calling 进行工具调用),默认为 false */
|
|
30
|
+
useReActMode?: boolean
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** 代理模型提供器的大语言配置对象, 通过 XOR 表达二选一 */
|
|
@@ -29,10 +35,10 @@ export type IAgentModelProviderLlmConfig = LlmFactoryConfig | LlmInstanceConfig
|
|
|
29
35
|
|
|
30
36
|
/** Mcp Server的配置对象 */
|
|
31
37
|
export type McpServerConfig =
|
|
32
|
-
| { type: 'streamableHttp'; url: string }
|
|
33
|
-
| { type: 'sse'; url: string }
|
|
34
|
-
| { type: 'extension'; url: string; sessionId: string }
|
|
35
|
-
| { transport: MCPTransport }
|
|
38
|
+
| { type: 'streamableHttp'; url: string; useAISdkClient?: boolean }
|
|
39
|
+
| { type: 'sse'; url: string; useAISdkClient?: boolean }
|
|
40
|
+
| { type: 'extension'; url: string; sessionId: string; useAISdkClient?: boolean }
|
|
41
|
+
| { transport: MCPTransport; useAISdkClient?: boolean }
|
|
36
42
|
|
|
37
43
|
/** */
|
|
38
44
|
export interface IAgentModelProviderOption {
|