feat: 实现真实算术工具调用循环,补全协议双向映射闭环
- 新增 tools.ts: calculate 工具定义 + 安全表达式执行引擎 - 重写 sendMessage: 非流式 tool execution loop,最多 5 轮迭代 - Live 模式使用独立系统上下文(仅含 calculate 工具,不依赖 demo) - exportToOpenAIFormat 补上独立 role:tool 消息的导出分支 - 新增 standalone role:tool 导出测试用例 - 49 个测试全部通过
This commit is contained in:
@@ -488,6 +488,55 @@ describe('exportToOpenAIFormat', () => {
|
|||||||
expect(messages[2].content).toBe('B results')
|
expect(messages[2].content).toBe('B results')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('handles standalone role:"tool" message with tool_call_result', () => {
|
||||||
|
const env: PromptEnvelope = {
|
||||||
|
version: '1.0',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
role: 'assistant',
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
kind: 'tool_call_request',
|
||||||
|
toolName: 'calculate',
|
||||||
|
arguments: { expression: '2+3' },
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
role: 'tool',
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
kind: 'tool_call_result',
|
||||||
|
toolName: 'calculate',
|
||||||
|
result: '5',
|
||||||
|
success: true,
|
||||||
|
collapsed: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const { messages } = exportToOpenAIFormat(env)
|
||||||
|
|
||||||
|
// assistant (tool_calls) + tool (result)
|
||||||
|
expect(messages).toHaveLength(2)
|
||||||
|
|
||||||
|
const assistantMsg = messages[0]
|
||||||
|
expect(assistantMsg.role).toBe('assistant')
|
||||||
|
expect(assistantMsg.tool_calls).toHaveLength(1)
|
||||||
|
expect(assistantMsg.tool_calls![0].function.name).toBe('calculate')
|
||||||
|
|
||||||
|
const toolMsg = messages[1]
|
||||||
|
expect(toolMsg.role).toBe('tool')
|
||||||
|
expect(toolMsg.tool_call_id).toBe(assistantMsg.tool_calls![0].id)
|
||||||
|
expect(toolMsg.content).toBe('5')
|
||||||
|
})
|
||||||
|
|
||||||
it('omits tools key when no tool_overview present', () => {
|
it('omits tools key when no tool_overview present', () => {
|
||||||
const env: PromptEnvelope = {
|
const env: PromptEnvelope = {
|
||||||
version: '1.0',
|
version: '1.0',
|
||||||
|
|||||||
+98
-88
@@ -1,18 +1,25 @@
|
|||||||
import { createContext, useContext, useState, useCallback, useRef, type ReactNode } from 'react'
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react'
|
||||||
import type { PromptEnvelope, Message } from '../types/protocol'
|
import type { PromptEnvelope, Message } from '../types/protocol'
|
||||||
import { demos } from '../data/demos'
|
import { demos } from '../data/demos'
|
||||||
import { getApiConfig, hasApiKey } from '../services/api-config'
|
import { getApiConfig, hasApiKey } from '../services/api-config'
|
||||||
import { sendChatRequestStream } from '../services/api'
|
import { sendChatRequest } from '../services/api'
|
||||||
|
import { importToolResult } from '../utils/import'
|
||||||
|
import { executeToolCall, buildLiveSystemMessage } from '../services/tools'
|
||||||
|
|
||||||
interface ChatContextValue {
|
interface ChatContextValue {
|
||||||
// 现有(保持不变)
|
|
||||||
envelope: PromptEnvelope
|
envelope: PromptEnvelope
|
||||||
setEnvelope: (e: PromptEnvelope) => void
|
setEnvelope: (e: PromptEnvelope) => void
|
||||||
demos: typeof demos
|
demos: typeof demos
|
||||||
activeDemo: number
|
activeDemo: number
|
||||||
setActiveDemo: (i: number) => void
|
setActiveDemo: (i: number) => void
|
||||||
|
|
||||||
// 新增 —— Live 模式
|
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
setIsLive: (v: boolean) => void
|
setIsLive: (v: boolean) => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
@@ -29,6 +36,9 @@ function genMsgId(): string {
|
|||||||
return `live_${Date.now()}_${++msgCounter}`
|
return `live_${Date.now()}_${++msgCounter}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tool loop 最大迭代次数,防止死循环 */
|
||||||
|
const MAX_TOOL_ITERATIONS = 5
|
||||||
|
|
||||||
export function ChatProvider({ children }: { children: ReactNode }) {
|
export function ChatProvider({ children }: { children: ReactNode }) {
|
||||||
const [activeDemo, setActiveDemo] = useState(0)
|
const [activeDemo, setActiveDemo] = useState(0)
|
||||||
const [envelope, setEnvelope] = useState<PromptEnvelope>(demos[0].envelope)
|
const [envelope, setEnvelope] = useState<PromptEnvelope>(demos[0].envelope)
|
||||||
@@ -36,45 +46,35 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
// 保存 Live 模式下的对话历史消息(用于 track 最新的消息列表)
|
// Live 模式下的完整消息列表(system + 所有 user/assistant/tool 对话历史)
|
||||||
const liveMessagesRef = useRef<Message[]>([])
|
const liveMessagesRef = useRef<Message[]>([])
|
||||||
|
|
||||||
const clearError = useCallback(() => setError(null), [])
|
const clearError = useCallback(() => setError(null), [])
|
||||||
|
|
||||||
/**
|
// ========================================================
|
||||||
* 从 demo 的 envelope 中提取系统上下文消息
|
// 模式切换
|
||||||
* (role: "system" 的消息,包含 system_prompt / memory / skills / tool_overview / static_var)
|
// ========================================================
|
||||||
*/
|
|
||||||
const extractSystemContext = useCallback((env: PromptEnvelope): Message[] => {
|
|
||||||
return env.messages.filter((m) => m.role === 'system')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 Live 模式的初始 envelope:
|
* 构建 Live 模式的初始 envelope。
|
||||||
* 保留当前 demo 的系统上下文,但清空所有 user/assistant/tool 消息。
|
* 使用专用的简单系统上下文(只含 calculate 工具),
|
||||||
|
* 不依赖 demo 的 system context。
|
||||||
*/
|
*/
|
||||||
const buildLiveEnvelope = useCallback(
|
const buildLiveEnvelope = useCallback((): PromptEnvelope => {
|
||||||
(demoIndex: number): PromptEnvelope => {
|
|
||||||
const systemMsgs = extractSystemContext(demos[demoIndex].envelope)
|
|
||||||
return {
|
return {
|
||||||
version: '1.0' as const,
|
version: '1.0' as const,
|
||||||
model: demos[demoIndex].envelope.model,
|
model: 'gpt-4-turbo',
|
||||||
messages: [...systemMsgs],
|
messages: [buildLiveSystemMessage()],
|
||||||
}
|
}
|
||||||
},
|
}, [])
|
||||||
[extractSystemContext]
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/** 切换 Demo 场景 */
|
||||||
* 切换 Demo 场景。
|
|
||||||
* - Demo 模式:直接加载 Demo JSON(现有行为)
|
|
||||||
* - Live 模式:替换系统上下文,清空对话历史
|
|
||||||
*/
|
|
||||||
const switchDemo = useCallback(
|
const switchDemo = useCallback(
|
||||||
(i: number) => {
|
(i: number) => {
|
||||||
setActiveDemo(i)
|
setActiveDemo(i)
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
const newEnv = buildLiveEnvelope(i)
|
// Live 模式切换 Demo:清空对话但保持 Live 系统上下文
|
||||||
|
const newEnv = buildLiveEnvelope()
|
||||||
liveMessagesRef.current = newEnv.messages
|
liveMessagesRef.current = newEnv.messages
|
||||||
setEnvelope(newEnv)
|
setEnvelope(newEnv)
|
||||||
} else {
|
} else {
|
||||||
@@ -85,20 +85,16 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
[isLive, buildLiveEnvelope]
|
[isLive, buildLiveEnvelope]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/** Demo ↔ Live 切换 */
|
||||||
* 切换 Demo / Live 模式。
|
|
||||||
*/
|
|
||||||
const handleSetIsLive = useCallback(
|
const handleSetIsLive = useCallback(
|
||||||
(v: boolean) => {
|
(v: boolean) => {
|
||||||
if (v === isLive) return
|
if (v === isLive) return
|
||||||
|
|
||||||
if (v) {
|
if (v) {
|
||||||
// Demo → Live:保留系统上下文,清空对话
|
const newEnv = buildLiveEnvelope()
|
||||||
const newEnv = buildLiveEnvelope(activeDemo)
|
|
||||||
liveMessagesRef.current = newEnv.messages
|
liveMessagesRef.current = newEnv.messages
|
||||||
setEnvelope(newEnv)
|
setEnvelope(newEnv)
|
||||||
} else {
|
} else {
|
||||||
// Live → Demo:恢复完整的 Demo JSON
|
|
||||||
setEnvelope(demos[activeDemo].envelope)
|
setEnvelope(demos[activeDemo].envelope)
|
||||||
liveMessagesRef.current = []
|
liveMessagesRef.current = []
|
||||||
}
|
}
|
||||||
@@ -110,20 +106,14 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
[isLive, activeDemo, buildLiveEnvelope]
|
[isLive, activeDemo, buildLiveEnvelope]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
// ========================================================
|
||||||
* 发送消息(Live 模式)。
|
// 发送消息(含 Tool Execution Loop)
|
||||||
*
|
// ========================================================
|
||||||
* 协议映射流程:
|
|
||||||
* 1. 用户输入 → TextSegment → role:"user" Message
|
|
||||||
* 2. 追加到 envelope → exportToOpenAIFormat → API 请求
|
|
||||||
* 3. API 响应 → StreamingImporter → importFromOpenAIResponse → assistant Message
|
|
||||||
* 4. 追加到 envelope → UI 重新渲染
|
|
||||||
*/
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
async (text: string) => {
|
async (text: string) => {
|
||||||
if (!text.trim() || isLoading) return
|
if (!text.trim() || isLoading) return
|
||||||
|
|
||||||
// 检查 API 配置
|
|
||||||
if (!hasApiKey()) {
|
if (!hasApiKey()) {
|
||||||
setError('请先配置 API Key(点击右上角齿轮图标)')
|
setError('请先配置 API Key(点击右上角齿轮图标)')
|
||||||
return
|
return
|
||||||
@@ -140,62 +130,82 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 构建当前 envelope(包含系统上下文 + 历史 + 新用户消息)
|
// 追加到消息列表并立即渲染
|
||||||
const currentMessages = [
|
liveMessagesRef.current = [...liveMessagesRef.current, userMsg]
|
||||||
...liveMessagesRef.current,
|
setEnvelope({
|
||||||
userMsg,
|
|
||||||
]
|
|
||||||
const currentEnvelope: PromptEnvelope = {
|
|
||||||
version: '1.0',
|
version: '1.0',
|
||||||
model: envelope.model,
|
model: 'gpt-4-turbo',
|
||||||
messages: currentMessages,
|
messages: liveMessagesRef.current,
|
||||||
}
|
})
|
||||||
|
|
||||||
// 立即渲染用户消息
|
|
||||||
liveMessagesRef.current = currentMessages
|
|
||||||
setEnvelope({ ...currentEnvelope })
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
// 3. 发送流式请求
|
// 2. Tool Execution Loop
|
||||||
let lastPartial: Message | null = null
|
try {
|
||||||
|
let iteration = 0
|
||||||
|
|
||||||
await sendChatRequestStream(currentEnvelope, config, {
|
while (iteration < MAX_TOOL_ITERATIONS) {
|
||||||
onToken(partial: Message) {
|
iteration++
|
||||||
lastPartial = partial
|
|
||||||
// 流式渲染:将部分 assistant 消息追加到列表末尾
|
|
||||||
const updatedMessages = [...liveMessagesRef.current, partial]
|
|
||||||
setEnvelope({
|
|
||||||
...currentEnvelope,
|
|
||||||
messages: updatedMessages,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
onDone(finalMessages: Message[]) {
|
const currentEnvelope: PromptEnvelope = {
|
||||||
// 移除可能存在的部分消息,追加最终消息
|
version: '1.0',
|
||||||
const baseMessages = liveMessagesRef.current.filter(
|
model: 'gpt-4-turbo',
|
||||||
(m) => m.id !== lastPartial?.id
|
messages: liveMessagesRef.current,
|
||||||
)
|
}
|
||||||
const updatedMessages = [...baseMessages, ...finalMessages]
|
|
||||||
liveMessagesRef.current = updatedMessages
|
|
||||||
setEnvelope({
|
|
||||||
...currentEnvelope,
|
|
||||||
messages: updatedMessages,
|
|
||||||
})
|
|
||||||
setIsLoading(false)
|
|
||||||
},
|
|
||||||
|
|
||||||
onError(err: Error) {
|
// 发送非流式请求
|
||||||
// 移除流式部分消息,保留用户消息
|
const result = await sendChatRequest(currentEnvelope, config)
|
||||||
|
|
||||||
|
// sendChatRequest 返回 Message[],通常单条 assistant 消息
|
||||||
|
const assistantMsg = result[0]
|
||||||
|
if (!assistantMsg) {
|
||||||
|
throw new Error('API 返回空响应')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加 assistant 消息(包含 text 和/或 tool_call_request segments)
|
||||||
|
liveMessagesRef.current = [...liveMessagesRef.current, assistantMsg]
|
||||||
setEnvelope({
|
setEnvelope({
|
||||||
...currentEnvelope,
|
...currentEnvelope,
|
||||||
messages: liveMessagesRef.current,
|
messages: liveMessagesRef.current,
|
||||||
})
|
})
|
||||||
setError(err.message)
|
|
||||||
setIsLoading(false)
|
// 检查是否有 tool_call 需要执行
|
||||||
},
|
const toolCalls = assistantMsg.segments.filter(
|
||||||
|
(s) => s.kind === 'tool_call_request'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (toolCalls.length === 0) {
|
||||||
|
// 无 tool_call → 对话结束
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行每个 tool_call 并追加结果
|
||||||
|
for (const tc of toolCalls) {
|
||||||
|
const execResult = executeToolCall(tc.toolName, tc.arguments)
|
||||||
|
const toolMsg = importToolResult(
|
||||||
|
tc.toolName,
|
||||||
|
execResult.result,
|
||||||
|
execResult.success
|
||||||
|
)
|
||||||
|
|
||||||
|
liveMessagesRef.current = [...liveMessagesRef.current, toolMsg]
|
||||||
|
setEnvelope({
|
||||||
|
...currentEnvelope,
|
||||||
|
messages: liveMessagesRef.current,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
// 继续循环 —— 将 tool result 送回 API
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iteration >= MAX_TOOL_ITERATIONS) {
|
||||||
|
setError(`工具调用已达最大迭代次数 (${MAX_TOOL_ITERATIONS}),可能陷入循环`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[isLoading, envelope.model, clearError]
|
[isLoading, clearError]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Live 模式工具定义与执行引擎。
|
||||||
|
*
|
||||||
|
* 当前仅提供一个简易算术工具 `calculate`:
|
||||||
|
* - 支持四则运算、幂运算、三角函数、对数、开方等
|
||||||
|
* - 底层通过安全的 new Function() 执行(仅允许数学表达式字符集)
|
||||||
|
* - 错误会被捕获并以文本形式返回(success=false)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Message, ToolItem } from '../types/protocol'
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 工具定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** Live 模式唯一的工具 */
|
||||||
|
export const LIVE_TOOL: ToolItem = {
|
||||||
|
name: 'calculate',
|
||||||
|
description: '执行数学计算表达式,支持加减乘除、幂运算 (**)、三角函数 (Math.sin/cos/tan)、反三角 (Math.asin/acos/atan)、对数 (Math.log/log2/log10)、开方 (Math.sqrt)、取整 (Math.floor/ceil/round)、绝对值 (Math.abs)、常数 (Math.PI, Math.E) 等',
|
||||||
|
parameters: 'expression: string — 数学表达式,例如 "123 * 456"、"Math.sqrt(144)"、"2 ** 10"、"Math.sin(Math.PI / 2)"',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
expression: {
|
||||||
|
type: 'string',
|
||||||
|
description: '数学表达式字符串,可使用 JavaScript Math 对象的所有方法及运算符',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['expression'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 工具执行
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 表达式安全校验 —— 只允许数学表达式常用字符 */
|
||||||
|
function validateExpression(expr: string): string | null {
|
||||||
|
const trimmed = expr.trim()
|
||||||
|
if (!trimmed) return '表达式不能为空'
|
||||||
|
|
||||||
|
// 白名单:字母/数字/运算符/括号/空格/点号/逗号/引号
|
||||||
|
// 允许 Math.xxx 函数调用、数字、运算符
|
||||||
|
if (!/^[a-zA-Z0-9+\-*/%()., \t\n'"\[\]_:!<>=&|^~]+$/.test(trimmed)) {
|
||||||
|
return `表达式包含不允许的字符`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止过长输入
|
||||||
|
if (trimmed.length > 500) {
|
||||||
|
return '表达式过长(最多 500 个字符)'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行工具调用并返回结果。
|
||||||
|
*
|
||||||
|
* @param toolName 工具名称
|
||||||
|
* @param args 工具参数(来自 tool_call_request.arguments)
|
||||||
|
* @returns 执行结果
|
||||||
|
*/
|
||||||
|
export function executeToolCall(
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, unknown>
|
||||||
|
): { result: string; success: boolean } {
|
||||||
|
if (toolName !== 'calculate') {
|
||||||
|
return { result: `未知工具: ${toolName}`, success: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawExpr = args.expression
|
||||||
|
if (typeof rawExpr !== 'string') {
|
||||||
|
return { result: '参数错误: expression 必须是字符串', success: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = validateExpression(rawExpr)
|
||||||
|
if (validationError) {
|
||||||
|
return { result: validationError, success: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 new Function 执行 —— 在已验证字符集的前提下安全
|
||||||
|
// 注入 Math 对象使其可用,这样用户可以直接写 Math.sin(x) 等
|
||||||
|
const fn = new Function('Math', `return (${rawExpr})`)
|
||||||
|
const result = fn(Math)
|
||||||
|
return { result: String(result), success: true }
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e)
|
||||||
|
return { result: `计算错误: ${msg}`, success: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Live 模式系统上下文
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Live 模式的系统消息(替代 demo 的 system context)。
|
||||||
|
*
|
||||||
|
* 包含:
|
||||||
|
* - 一个 system_prompt segment:简短的行为指令
|
||||||
|
* - 一个 tool_overview segment:仅含 calculate 工具
|
||||||
|
*/
|
||||||
|
export function buildLiveSystemMessage(): Message {
|
||||||
|
return {
|
||||||
|
id: 'live-system',
|
||||||
|
role: 'system',
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
kind: 'system_prompt',
|
||||||
|
content:
|
||||||
|
'你是一个数学助手。如果用户问你数学计算问题,使用 calculate 工具来计算结果。\n' +
|
||||||
|
'如果用户只是闲聊,正常回复即可,不要调用工具。\n' +
|
||||||
|
'回复语言:中文。回答要简洁、友好。',
|
||||||
|
collapsed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'tool_overview',
|
||||||
|
items: [LIVE_TOOL],
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -326,6 +326,31 @@ export function exportToOpenAIFormat(envelope: PromptEnvelope): OpenAIExport {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tool (独立 role: "tool" 消息) ──
|
||||||
|
if (msg.role === 'tool') {
|
||||||
|
for (const seg of msg.segments) {
|
||||||
|
if (seg.kind === 'tool_call_result') {
|
||||||
|
// call_id 配对:按 toolName 的 FIFO 队列匹配
|
||||||
|
const queue = pendingCallIds.get(seg.toolName)
|
||||||
|
const callId = queue?.shift() || nextCallId()
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
role: 'tool',
|
||||||
|
tool_call_id: callId,
|
||||||
|
content: seg.result,
|
||||||
|
})
|
||||||
|
} else if (seg.kind === 'tool_call_request') {
|
||||||
|
// tool 消息中包含 tool_call_request 的情况:也生成 tool_calls
|
||||||
|
const callId = nextCallId()
|
||||||
|
const queue = pendingCallIds.get(seg.toolName) || []
|
||||||
|
queue.push(callId)
|
||||||
|
pendingCallIds.set(seg.toolName, queue)
|
||||||
|
// 注意:独立 tool 消息不应该有 tool_call_request,
|
||||||
|
// 这里做防御性处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend merged system message
|
// Prepend merged system message
|
||||||
|
|||||||
Reference in New Issue
Block a user