Add manual opener tool calls to Assistant model and API
- Introduced `manual_opener_tool_calls` field in the Assistant model to support custom tool calls. - Updated AssistantBase and AssistantUpdate schemas to include the new field. - Implemented normalization and migration logic for handling manual opener tool calls in the API. - Enhanced runtime metadata to include manual opener tool calls in responses. - Updated tests to validate the new functionality and ensure proper handling of tool calls. - Refactored tool ID normalization to support legacy tool names for backward compatibility.
This commit is contained in:
@@ -3,7 +3,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Plus, Search, Play, Square, Copy, Trash2, Mic, MessageSquare, Save, Video, PhoneOff, Camera, ArrowLeftRight, Send, Phone, Rocket, AlertTriangle, PhoneCall, CameraOff, Image, Images, CloudSun, Calendar, TrendingUp, Coins, Wrench, Globe, Terminal, X, ClipboardCheck, Sparkles, Volume2, Timer, ChevronDown, Database, Server, Zap, ExternalLink, Key, BrainCircuit, Ear, Book, Filter } from 'lucide-react';
|
||||
import { Button, Input, Badge, Drawer, Dialog, Switch } from '../components/UI';
|
||||
import { ASRModel, Assistant, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types';
|
||||
import { ASRModel, Assistant, AssistantOpenerToolCall, KnowledgeBase, LLMModel, TabValue, Tool, Voice } from '../types';
|
||||
import { createAssistant, deleteAssistant, fetchASRModels, fetchAssistantOpenerAudioPcmBuffer, fetchAssistants, fetchKnowledgeBases, fetchLLMModels, fetchTools, fetchVoices, generateAssistantOpenerAudio, previewVoice, updateAssistant as updateAssistantApi } from '../services/backendApi';
|
||||
|
||||
const isOpenAICompatibleVendor = (vendor?: string) => {
|
||||
@@ -85,6 +85,80 @@ const renderToolIcon = (icon: string) => {
|
||||
return map[icon] || <Wrench className={className} />;
|
||||
};
|
||||
|
||||
const TOOL_ID_ALIASES: Record<string, string> = {
|
||||
voice_message_prompt: 'voice_msg_prompt',
|
||||
};
|
||||
|
||||
const normalizeToolId = (raw: unknown): string => {
|
||||
const toolId = String(raw || '').trim();
|
||||
if (!toolId) return '';
|
||||
return TOOL_ID_ALIASES[toolId] || toolId;
|
||||
};
|
||||
|
||||
const OPENER_TOOL_ARGUMENT_TEMPLATES: Record<string, Record<string, any>> = {
|
||||
text_msg_prompt: {
|
||||
msg: '您好,请先描述您要咨询的问题。',
|
||||
},
|
||||
voice_msg_prompt: {
|
||||
msg: '您好,请先描述您要咨询的问题。',
|
||||
},
|
||||
text_choice_prompt: {
|
||||
question: '请选择需要办理的业务',
|
||||
options: [
|
||||
{ id: 'billing', label: '账单咨询', value: 'billing' },
|
||||
{ id: 'repair', label: '故障报修', value: 'repair' },
|
||||
{ id: 'manual', label: '人工客服', value: 'manual' },
|
||||
],
|
||||
},
|
||||
voice_choice_prompt: {
|
||||
question: '请选择需要办理的业务',
|
||||
options: [
|
||||
{ id: 'billing', label: '账单咨询', value: 'billing' },
|
||||
{ id: 'repair', label: '故障报修', value: 'repair' },
|
||||
{ id: 'manual', label: '人工客服', value: 'manual' },
|
||||
],
|
||||
voice_text: '请从以下选项中选择:账单咨询、故障报修或人工客服。',
|
||||
},
|
||||
};
|
||||
|
||||
const normalizeManualOpenerToolCallsForRuntime = (
|
||||
calls: AssistantOpenerToolCall[] | undefined,
|
||||
options?: { strictJson?: boolean }
|
||||
): { calls: Array<{ toolName: string; arguments: Record<string, any> }>; error?: string } => {
|
||||
const strictJson = options?.strictJson === true;
|
||||
const normalized: Array<{ toolName: string; arguments: Record<string, any> }> = [];
|
||||
if (!Array.isArray(calls)) return { calls: normalized };
|
||||
|
||||
for (let i = 0; i < calls.length; i += 1) {
|
||||
const item = calls[i];
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const toolName = normalizeToolId(item.toolName || '');
|
||||
if (!toolName) continue;
|
||||
|
||||
const argsRaw = item.arguments;
|
||||
let args: Record<string, any> = {};
|
||||
if (argsRaw && typeof argsRaw === 'object' && !Array.isArray(argsRaw)) {
|
||||
args = argsRaw as Record<string, any>;
|
||||
} else if (typeof argsRaw === 'string' && argsRaw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(argsRaw);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
args = parsed as Record<string, any>;
|
||||
} else if (strictJson) {
|
||||
return { calls: normalized, error: `Opener tool call #${i + 1} arguments must be a JSON object.` };
|
||||
}
|
||||
} catch {
|
||||
if (strictJson) {
|
||||
return { calls: normalized, error: `Opener tool call #${i + 1} has invalid JSON arguments.` };
|
||||
}
|
||||
}
|
||||
}
|
||||
normalized.push({ toolName, arguments: args });
|
||||
}
|
||||
|
||||
return { calls: normalized.slice(0, 8) };
|
||||
};
|
||||
|
||||
export const AssistantsPage: React.FC = () => {
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const [voices, setVoices] = useState<Voice[]>([]);
|
||||
@@ -173,6 +247,7 @@ export const AssistantsPage: React.FC = () => {
|
||||
name: 'New Assistant',
|
||||
firstTurnMode: 'bot_first',
|
||||
opener: '',
|
||||
manualOpenerToolCalls: [],
|
||||
generatedOpenerEnabled: false,
|
||||
openerAudioEnabled: false,
|
||||
prompt: '',
|
||||
@@ -201,9 +276,17 @@ export const AssistantsPage: React.FC = () => {
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedAssistant) return;
|
||||
const normalizedManualCalls = normalizeManualOpenerToolCallsForRuntime(selectedAssistant.manualOpenerToolCalls, { strictJson: true });
|
||||
if (normalizedManualCalls.error) {
|
||||
alert(normalizedManualCalls.error);
|
||||
return;
|
||||
}
|
||||
setSaveLoading(true);
|
||||
try {
|
||||
const updated = await updateAssistantApi(selectedAssistant.id, selectedAssistant);
|
||||
const updated = await updateAssistantApi(selectedAssistant.id, {
|
||||
...selectedAssistant,
|
||||
manualOpenerToolCalls: normalizedManualCalls.calls,
|
||||
});
|
||||
setAssistants((prev) => prev.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)));
|
||||
setPersistedAssistantSnapshotById((prev) => ({ ...prev, [updated.id]: serializeAssistant(updated) }));
|
||||
} catch (error) {
|
||||
@@ -436,17 +519,19 @@ export const AssistantsPage: React.FC = () => {
|
||||
|
||||
const toggleTool = (toolId: string) => {
|
||||
if (!selectedAssistant) return;
|
||||
const currentTools = selectedAssistant.tools || [];
|
||||
const newTools = currentTools.includes(toolId)
|
||||
? currentTools.filter(id => id !== toolId)
|
||||
: [...currentTools, toolId];
|
||||
const canonicalToolId = normalizeToolId(toolId);
|
||||
const currentTools = (selectedAssistant.tools || []).map((id) => normalizeToolId(id));
|
||||
const newTools = currentTools.includes(canonicalToolId)
|
||||
? currentTools.filter(id => id !== canonicalToolId)
|
||||
: [...currentTools, canonicalToolId];
|
||||
updateAssistant('tools', newTools);
|
||||
};
|
||||
|
||||
const removeImportedTool = (e: React.MouseEvent, tool: Tool) => {
|
||||
e.stopPropagation();
|
||||
if (!selectedAssistant) return;
|
||||
updateAssistant('tools', (selectedAssistant.tools || []).filter((id) => id !== tool.id));
|
||||
const canonicalToolId = normalizeToolId(tool.id);
|
||||
updateAssistant('tools', (selectedAssistant.tools || []).filter((id) => normalizeToolId(id) !== canonicalToolId));
|
||||
};
|
||||
|
||||
const addHotword = () => {
|
||||
@@ -462,13 +547,76 @@ export const AssistantsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const addManualOpenerToolCall = () => {
|
||||
if (!selectedAssistant) return;
|
||||
const current = selectedAssistant.manualOpenerToolCalls || [];
|
||||
if (current.length >= 8) return;
|
||||
const fallbackTool = normalizeToolId(
|
||||
(selectedAssistant.tools || []).find((id) =>
|
||||
tools.some((tool) => normalizeToolId(tool.id) === normalizeToolId(id) && tool.enabled !== false)
|
||||
) || ''
|
||||
);
|
||||
updateAssistant('manualOpenerToolCalls', [
|
||||
...current,
|
||||
{
|
||||
toolName: fallbackTool,
|
||||
arguments: '{}',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateManualOpenerToolCall = (index: number, patch: Partial<AssistantOpenerToolCall>) => {
|
||||
if (!selectedAssistant) return;
|
||||
const current = selectedAssistant.manualOpenerToolCalls || [];
|
||||
if (index < 0 || index >= current.length) return;
|
||||
const next = [...current];
|
||||
const normalizedPatch = { ...patch };
|
||||
if (Object.prototype.hasOwnProperty.call(normalizedPatch, 'toolName')) {
|
||||
normalizedPatch.toolName = normalizeToolId(normalizedPatch.toolName || '');
|
||||
}
|
||||
next[index] = { ...next[index], ...normalizedPatch };
|
||||
updateAssistant('manualOpenerToolCalls', next);
|
||||
};
|
||||
|
||||
const removeManualOpenerToolCall = (index: number) => {
|
||||
if (!selectedAssistant) return;
|
||||
const current = selectedAssistant.manualOpenerToolCalls || [];
|
||||
updateAssistant('manualOpenerToolCalls', current.filter((_, idx) => idx !== index));
|
||||
};
|
||||
|
||||
const applyManualOpenerToolTemplate = (index: number) => {
|
||||
if (!selectedAssistant) return;
|
||||
const current = selectedAssistant.manualOpenerToolCalls || [];
|
||||
if (index < 0 || index >= current.length) return;
|
||||
const toolName = normalizeToolId(current[index]?.toolName || '');
|
||||
const template = OPENER_TOOL_ARGUMENT_TEMPLATES[toolName];
|
||||
if (!template) return;
|
||||
updateManualOpenerToolCall(index, {
|
||||
arguments: JSON.stringify(template, null, 2),
|
||||
});
|
||||
};
|
||||
|
||||
const systemTools = tools.filter((t) => t.enabled !== false && t.category === 'system');
|
||||
const queryTools = tools.filter((t) => t.enabled !== false && t.category === 'query');
|
||||
const selectedToolIds = selectedAssistant?.tools || [];
|
||||
const activeSystemTools = systemTools.filter((tool) => selectedToolIds.includes(tool.id));
|
||||
const activeQueryTools = queryTools.filter((tool) => selectedToolIds.includes(tool.id));
|
||||
const availableSystemTools = systemTools.filter((tool) => !selectedToolIds.includes(tool.id));
|
||||
const availableQueryTools = queryTools.filter((tool) => !selectedToolIds.includes(tool.id));
|
||||
const selectedToolIds = (selectedAssistant?.tools || []).map((id) => normalizeToolId(id));
|
||||
const activeSystemTools = systemTools.filter((tool) => selectedToolIds.includes(normalizeToolId(tool.id)));
|
||||
const activeQueryTools = queryTools.filter((tool) => selectedToolIds.includes(normalizeToolId(tool.id)));
|
||||
const availableSystemTools = systemTools.filter((tool) => !selectedToolIds.includes(normalizeToolId(tool.id)));
|
||||
const availableQueryTools = queryTools.filter((tool) => !selectedToolIds.includes(normalizeToolId(tool.id)));
|
||||
const openerToolOptions = Array.from(
|
||||
new Map(
|
||||
tools
|
||||
.filter(
|
||||
(tool) =>
|
||||
tool.enabled !== false &&
|
||||
selectedToolIds.some((selectedId) => normalizeToolId(selectedId) === normalizeToolId(tool.id))
|
||||
)
|
||||
.map((tool) => {
|
||||
const toolId = normalizeToolId(tool.id);
|
||||
return [toolId, { id: toolId, label: `${tool.name} (${toolId})` }];
|
||||
})
|
||||
).values()
|
||||
);
|
||||
|
||||
const isExternalConfig = selectedAssistant?.configMode === 'dify' || selectedAssistant?.configMode === 'fastgpt';
|
||||
const isNoneConfig = selectedAssistant?.configMode === 'none' || !selectedAssistant?.configMode;
|
||||
@@ -949,6 +1097,96 @@ export const AssistantsPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedAssistant.generatedOpenerEnabled !== true && (
|
||||
<div className="mt-3 p-3 rounded-lg border border-white/10 bg-white/[0.03] space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="text-xs font-semibold text-white flex items-center">
|
||||
<Wrench className="w-4 h-4 mr-2 text-primary" />
|
||||
开场工具调用(手动模式)
|
||||
</label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={addManualOpenerToolCall}
|
||||
disabled={(selectedAssistant.manualOpenerToolCalls || []).length >= 8}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 mr-1.5" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
{(selectedAssistant.manualOpenerToolCalls || []).length === 0 ? (
|
||||
<div className="text-[11px] text-muted-foreground border border-dashed border-white/10 rounded-md px-2 py-2">
|
||||
未配置。可添加 text_msg_prompt / voice_msg_prompt 等工具作为开场动作。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{(selectedAssistant.manualOpenerToolCalls || []).map((call, idx) => (
|
||||
<div key={`manual-opener-tool-${idx}`} className="rounded-md border border-white/10 bg-black/20 p-2 space-y-2">
|
||||
<div className="grid grid-cols-[1fr_auto] gap-2 items-center">
|
||||
<select
|
||||
className="h-9 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-foreground"
|
||||
value={normalizeToolId(call.toolName || '')}
|
||||
onChange={(e) => updateManualOpenerToolCall(idx, { toolName: e.target.value })}
|
||||
>
|
||||
<option value="">选择已启用工具</option>
|
||||
{normalizeToolId(call.toolName || '') && !openerToolOptions.some((tool) => tool.id === normalizeToolId(call.toolName || '')) && (
|
||||
<option value={normalizeToolId(call.toolName || '')}>
|
||||
{`${normalizeToolId(call.toolName || '')} (未启用/不存在)`}
|
||||
</option>
|
||||
)}
|
||||
{openerToolOptions.map((tool) => (
|
||||
<option key={tool.id} value={tool.id}>
|
||||
{tool.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-muted-foreground hover:text-red-300"
|
||||
onClick={() => removeManualOpenerToolCall(idx)}
|
||||
title="删除该工具调用"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[11px] text-muted-foreground">参数 JSON</p>
|
||||
{OPENER_TOOL_ARGUMENT_TEMPLATES[normalizeToolId(call.toolName || '')] && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-[11px]"
|
||||
onClick={() => applyManualOpenerToolTemplate(idx)}
|
||||
>
|
||||
填充模板
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments || {}, null, 2)}
|
||||
onChange={(e) => updateManualOpenerToolCall(idx, { arguments: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full rounded-md border border-white/10 bg-white/5 px-2 py-1.5 text-xs text-foreground"
|
||||
placeholder='参数 JSON(可选),例如 {"msg":"您好,请先选择业务类型"}。可点“填充模板”自动生成。'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{openerToolOptions.length === 0 && (
|
||||
<p className="text-[11px] text-amber-300/90">
|
||||
当前助手未启用任何工具,请先在“工具配置”里添加后再选择。
|
||||
</p>
|
||||
)}
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
按列表顺序执行。参数必须是 JSON 对象;保存时会校验格式。text_msg_prompt / voice_msg_prompt / text_choice_prompt / voice_choice_prompt 支持一键填充模板。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedAssistant.generatedOpenerEnabled === true
|
||||
? '通话接通后将根据提示词自动生成开场白。'
|
||||
@@ -1682,7 +1920,7 @@ const TOOL_PARAMETER_HINTS: Record<string, any> = {
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
voice_message_prompt: {
|
||||
voice_msg_prompt: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
msg: { type: 'string', description: 'Message text to speak' },
|
||||
@@ -1766,7 +2004,7 @@ const DEBUG_CLIENT_TOOLS = [
|
||||
{ id: 'turn_off_camera', name: 'turn_off_camera', description: '关闭摄像头' },
|
||||
{ id: 'increase_volume', name: 'increase_volume', description: '调高音量' },
|
||||
{ id: 'decrease_volume', name: 'decrease_volume', description: '调低音量' },
|
||||
{ id: 'voice_message_prompt', name: 'voice_message_prompt', description: '语音消息提示' },
|
||||
{ id: 'voice_msg_prompt', name: 'voice_msg_prompt', description: '语音消息提示' },
|
||||
{ id: 'text_msg_prompt', name: 'text_msg_prompt', description: '文本消息提示' },
|
||||
{ id: 'voice_choice_prompt', name: 'voice_choice_prompt', description: '语音选项提示(原子)' },
|
||||
{ id: 'text_choice_prompt', name: 'text_choice_prompt', description: '文本选项提示(等待选择)' },
|
||||
@@ -2208,11 +2446,13 @@ export const DebugDrawer: React.FC<{
|
||||
const lastUserFinalRef = useRef<string>('');
|
||||
const debugVolumePercentRef = useRef<number>(50);
|
||||
const clientToolEnabledMapRef = useRef<Record<string, boolean>>(clientToolEnabledMap);
|
||||
const isClientToolEnabled = (toolId: string) => clientToolEnabledMap[toolId] !== false;
|
||||
const isClientToolEnabledLive = (toolId: string) => clientToolEnabledMapRef.current[toolId] !== false;
|
||||
const isClientToolEnabled = (toolId: string) => clientToolEnabledMap[normalizeToolId(toolId)] !== false;
|
||||
const isClientToolEnabledLive = (toolId: string) => clientToolEnabledMapRef.current[normalizeToolId(toolId)] !== false;
|
||||
const selectedToolSchemas = useMemo(() => {
|
||||
const ids = Array.from(new Set([...(assistant.tools || []), ...DEBUG_CLIENT_TOOLS.map((item) => item.id)]));
|
||||
const byId = new Map(tools.map((t) => [t.id, t]));
|
||||
const ids = Array.from(
|
||||
new Set([...(assistant.tools || []).map((id) => normalizeToolId(id)), ...DEBUG_CLIENT_TOOLS.map((item) => item.id)])
|
||||
);
|
||||
const byId = new Map(tools.map((t) => [normalizeToolId(t.id), { ...t, id: normalizeToolId(t.id) }]));
|
||||
return ids.map((id) => {
|
||||
const item = byId.get(id);
|
||||
const toolId = item?.id || id;
|
||||
@@ -3006,6 +3246,7 @@ export const DebugDrawer: React.FC<{
|
||||
'firstTurnMode',
|
||||
'greeting',
|
||||
'generatedOpenerEnabled',
|
||||
'manualOpenerToolCalls',
|
||||
'systemPrompt',
|
||||
'output',
|
||||
'bargeIn',
|
||||
@@ -3146,6 +3387,11 @@ export const DebugDrawer: React.FC<{
|
||||
const warnings: string[] = [];
|
||||
const ttsEnabled = Boolean(textTtsEnabled);
|
||||
const generatedOpenerEnabled = assistant.generatedOpenerEnabled === true;
|
||||
const normalizedManualCalls = normalizeManualOpenerToolCallsForRuntime(assistant.manualOpenerToolCalls, { strictJson: true });
|
||||
if (normalizedManualCalls.error) {
|
||||
setDynamicVariablesError(normalizedManualCalls.error);
|
||||
throw createDynamicVariablesError(normalizedManualCalls.error);
|
||||
}
|
||||
const knowledgeBaseId = String(assistant.knowledgeBaseId || '').trim();
|
||||
const knowledge = knowledgeBaseId
|
||||
? { enabled: true, kbId: knowledgeBaseId, nResults: 5 }
|
||||
@@ -3162,6 +3408,7 @@ export const DebugDrawer: React.FC<{
|
||||
firstTurnMode: assistant.firstTurnMode || 'bot_first',
|
||||
greeting: generatedOpenerEnabled ? '' : (assistant.opener || ''),
|
||||
generatedOpenerEnabled,
|
||||
manualOpenerToolCalls: generatedOpenerEnabled ? [] : normalizedManualCalls.calls,
|
||||
bargeIn: {
|
||||
enabled: assistant.botCannotBeInterrupted !== true,
|
||||
minDurationMs: Math.max(0, Number(assistant.interruptionSensitivity ?? 180)),
|
||||
@@ -3363,7 +3610,7 @@ export const DebugDrawer: React.FC<{
|
||||
if (type === 'assistant.tool_call') {
|
||||
const toolCall = payload?.tool_call || {};
|
||||
const toolCallId = String(payload?.tool_call_id || toolCall?.id || '').trim();
|
||||
const toolName = String(toolCall?.function?.name || toolCall?.name || 'unknown_tool');
|
||||
const toolName = normalizeToolId(toolCall?.function?.name || toolCall?.name || 'unknown_tool');
|
||||
const toolDisplayName = String(payload?.tool_display_name || toolCall?.displayName || toolName);
|
||||
const executor = String(toolCall?.executor || 'server').toLowerCase();
|
||||
const rawArgs = String(toolCall?.function?.arguments || '');
|
||||
@@ -3478,7 +3725,7 @@ export const DebugDrawer: React.FC<{
|
||||
level: debugVolumePercentRef.current,
|
||||
};
|
||||
resultPayload.status = { code: 200, message: 'ok' };
|
||||
} else if (toolName === 'voice_message_prompt') {
|
||||
} else if (toolName === 'voice_msg_prompt' || toolName === 'voice_message_prompt') {
|
||||
const msg = String(parsedArgs?.msg || '').trim();
|
||||
if (!msg) {
|
||||
resultPayload.output = { message: "Missing required argument 'msg'" };
|
||||
@@ -3574,7 +3821,7 @@ export const DebugDrawer: React.FC<{
|
||||
|
||||
if (type === 'assistant.tool_result') {
|
||||
const result = payload?.result || {};
|
||||
const toolName = String(result?.name || 'unknown_tool');
|
||||
const toolName = normalizeToolId(result?.name || 'unknown_tool');
|
||||
const toolDisplayName = String(payload?.tool_display_name || toolName);
|
||||
const statusCode = Number(result?.status?.code || 500);
|
||||
const statusMessage = String(result?.status?.message || 'error');
|
||||
|
||||
Reference in New Issue
Block a user