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,6 +3,44 @@ import { apiRequest, getApiBaseUrl } from './apiClient';
|
||||
|
||||
type AnyRecord = Record<string, any>;
|
||||
const DEFAULT_LIST_LIMIT = 1000;
|
||||
const TOOL_ID_ALIASES: Record<string, string> = {
|
||||
voice_message_prompt: 'voice_msg_prompt',
|
||||
};
|
||||
|
||||
const normalizeToolId = (value: unknown): string => {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
return TOOL_ID_ALIASES[raw] || raw;
|
||||
};
|
||||
|
||||
const normalizeToolIdList = (value: unknown): string[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of value) {
|
||||
const id = normalizeToolId(item);
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
result.push(id);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizeManualOpenerToolCalls = (value: unknown): AnyRecord[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.filter((item) => item && typeof item === 'object')
|
||||
.map((item) => {
|
||||
const typed = item as AnyRecord;
|
||||
const toolName = normalizeToolId(typed.toolName || typed.tool_name || typed.name || '');
|
||||
if (!toolName) return null;
|
||||
return {
|
||||
...typed,
|
||||
toolName,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as AnyRecord[];
|
||||
};
|
||||
|
||||
const withLimit = (path: string, limit: number = DEFAULT_LIST_LIMIT): string =>
|
||||
`${path}${path.includes('?') ? '&' : '?'}limit=${limit}`;
|
||||
@@ -35,6 +73,7 @@ const mapAssistant = (raw: AnyRecord): Assistant => ({
|
||||
callCount: Number(readField(raw, ['callCount', 'call_count'], 0)),
|
||||
firstTurnMode: readField(raw, ['firstTurnMode', 'first_turn_mode'], 'bot_first') as 'bot_first' | 'user_first',
|
||||
opener: readField(raw, ['opener'], ''),
|
||||
manualOpenerToolCalls: normalizeManualOpenerToolCalls(readField(raw, ['manualOpenerToolCalls', 'manual_opener_tool_calls'], [])),
|
||||
generatedOpenerEnabled: Boolean(readField(raw, ['generatedOpenerEnabled', 'generated_opener_enabled'], false)),
|
||||
openerAudioEnabled: Boolean(readField(raw, ['openerAudioEnabled', 'opener_audio_enabled'], false)),
|
||||
openerAudioReady: Boolean(readField(raw, ['openerAudioReady', 'opener_audio_ready'], false)),
|
||||
@@ -47,7 +86,7 @@ const mapAssistant = (raw: AnyRecord): Assistant => ({
|
||||
voice: readField(raw, ['voice'], ''),
|
||||
speed: Number(readField(raw, ['speed'], 1)),
|
||||
hotwords: readField(raw, ['hotwords'], []),
|
||||
tools: readField(raw, ['tools'], []),
|
||||
tools: normalizeToolIdList(readField(raw, ['tools'], [])),
|
||||
botCannotBeInterrupted: Boolean(readField(raw, ['botCannotBeInterrupted', 'bot_cannot_be_interrupted'], false)),
|
||||
interruptionSensitivity: Number(readField(raw, ['interruptionSensitivity', 'interruption_sensitivity'], 500)),
|
||||
configMode: readField(raw, ['configMode', 'config_mode'], 'platform') as 'platform' | 'dify' | 'fastgpt' | 'none',
|
||||
@@ -114,7 +153,7 @@ const mapLLMModel = (raw: AnyRecord): LLMModel => ({
|
||||
});
|
||||
|
||||
const mapTool = (raw: AnyRecord): Tool => ({
|
||||
id: String(readField(raw, ['id'], '')),
|
||||
id: normalizeToolId(readField(raw, ['id'], '')),
|
||||
name: readField(raw, ['name'], ''),
|
||||
description: readField(raw, ['description'], ''),
|
||||
category: readField(raw, ['category'], 'system') as 'system' | 'query',
|
||||
@@ -234,6 +273,7 @@ export const createAssistant = async (data: Partial<Assistant>): Promise<Assista
|
||||
name: data.name || 'New Assistant',
|
||||
firstTurnMode: data.firstTurnMode || 'bot_first',
|
||||
opener: data.opener || '',
|
||||
manualOpenerToolCalls: normalizeManualOpenerToolCalls(data.manualOpenerToolCalls || []),
|
||||
generatedOpenerEnabled: data.generatedOpenerEnabled ?? false,
|
||||
openerAudioEnabled: data.openerAudioEnabled ?? false,
|
||||
prompt: data.prompt || '',
|
||||
@@ -243,7 +283,7 @@ export const createAssistant = async (data: Partial<Assistant>): Promise<Assista
|
||||
voice: data.voice || '',
|
||||
speed: data.speed ?? 1,
|
||||
hotwords: data.hotwords || [],
|
||||
tools: data.tools || [],
|
||||
tools: normalizeToolIdList(data.tools || []),
|
||||
botCannotBeInterrupted: data.botCannotBeInterrupted ?? false,
|
||||
interruptionSensitivity: data.interruptionSensitivity ?? 500,
|
||||
configMode: data.configMode || 'platform',
|
||||
@@ -263,6 +303,9 @@ export const updateAssistant = async (id: string, data: Partial<Assistant>): Pro
|
||||
name: data.name,
|
||||
firstTurnMode: data.firstTurnMode,
|
||||
opener: data.opener,
|
||||
manualOpenerToolCalls: data.manualOpenerToolCalls === undefined
|
||||
? undefined
|
||||
: normalizeManualOpenerToolCalls(data.manualOpenerToolCalls),
|
||||
generatedOpenerEnabled: data.generatedOpenerEnabled,
|
||||
openerAudioEnabled: data.openerAudioEnabled,
|
||||
prompt: data.prompt,
|
||||
@@ -272,7 +315,7 @@ export const updateAssistant = async (id: string, data: Partial<Assistant>): Pro
|
||||
voice: data.voice,
|
||||
speed: data.speed,
|
||||
hotwords: data.hotwords,
|
||||
tools: data.tools,
|
||||
tools: data.tools === undefined ? undefined : normalizeToolIdList(data.tools),
|
||||
botCannotBeInterrupted: data.botCannotBeInterrupted,
|
||||
interruptionSensitivity: data.interruptionSensitivity,
|
||||
configMode: data.configMode,
|
||||
@@ -556,12 +599,20 @@ export const previewLLMModel = async (
|
||||
export const fetchTools = async (): Promise<Tool[]> => {
|
||||
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/tools/resources'));
|
||||
const list = Array.isArray(response) ? response : (response.list || []);
|
||||
return list.map((item) => mapTool(item));
|
||||
const deduped = new Map<string, Tool>();
|
||||
for (const item of list) {
|
||||
const mapped = mapTool(item);
|
||||
if (!mapped.id) continue;
|
||||
if (!deduped.has(mapped.id)) {
|
||||
deduped.set(mapped.id, mapped);
|
||||
}
|
||||
}
|
||||
return Array.from(deduped.values());
|
||||
};
|
||||
|
||||
export const createTool = async (data: Partial<Tool>): Promise<Tool> => {
|
||||
const payload = {
|
||||
id: data.id || undefined,
|
||||
id: normalizeToolId(data.id || undefined) || undefined,
|
||||
name: data.name || 'New Tool',
|
||||
description: data.description || '',
|
||||
category: data.category || 'system',
|
||||
@@ -594,12 +645,12 @@ export const updateTool = async (id: string, data: Partial<Tool>): Promise<Tool>
|
||||
wait_for_response: data.waitForResponse,
|
||||
enabled: data.enabled,
|
||||
};
|
||||
const response = await apiRequest<AnyRecord>(`/tools/resources/${id}`, { method: 'PUT', body: payload });
|
||||
const response = await apiRequest<AnyRecord>(`/tools/resources/${normalizeToolId(id)}`, { method: 'PUT', body: payload });
|
||||
return mapTool(response);
|
||||
};
|
||||
|
||||
export const deleteTool = async (id: string): Promise<void> => {
|
||||
await apiRequest(`/tools/resources/${id}`, { method: 'DELETE' });
|
||||
await apiRequest(`/tools/resources/${normalizeToolId(id)}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
export const fetchWorkflows = async (): Promise<Workflow[]> => {
|
||||
|
||||
Reference in New Issue
Block a user