import { ASRModel, Assistant, CallLog, InteractionDetail, KnowledgeBase, KnowledgeDocument, LLMModel, Tool, Voice, Workflow, WorkflowEdge, WorkflowNode } from '../types'; import { apiRequest, getApiBaseUrl } from './apiClient'; type AnyRecord = Record; const DEFAULT_LIST_LIMIT = 1000; const withLimit = (path: string, limit: number = DEFAULT_LIST_LIMIT): string => `${path}${path.includes('?') ? '&' : '?'}limit=${limit}`; const readField = (obj: AnyRecord, keys: string[], fallback: T): T => { for (const key of keys) { if (obj[key] !== undefined && obj[key] !== null) { return obj[key] as T; } } return fallback; }; const normalizeDateLabel = (value: string): string => { if (!value) return ''; return value.includes('T') ? value.replace('T', ' ').slice(0, 16) : value.slice(0, 16); }; const formatDuration = (seconds?: number | null): string => { if (!seconds || seconds <= 0) return '0s'; const m = Math.floor(seconds / 60); const s = seconds % 60; if (m === 0) return `${s}s`; return `${m}m ${s}s`; }; const mapAssistant = (raw: AnyRecord): Assistant => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), 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'], ''), 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)), openerAudioDurationMs: Number(readField(raw, ['openerAudioDurationMs', 'opener_audio_duration_ms'], 0)), openerAudioUpdatedAt: readField(raw, ['openerAudioUpdatedAt', 'opener_audio_updated_at'], ''), prompt: readField(raw, ['prompt'], ''), knowledgeBaseId: readField(raw, ['knowledgeBaseId', 'knowledge_base_id'], ''), language: readField(raw, ['language'], 'zh') as 'zh' | 'en', voiceOutputEnabled: Boolean(readField(raw, ['voiceOutputEnabled', 'voice_output_enabled'], true)), voice: readField(raw, ['voice'], ''), speed: Number(readField(raw, ['speed'], 1)), hotwords: readField(raw, ['hotwords'], []), tools: 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', apiUrl: readField(raw, ['apiUrl', 'api_url'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), llmModelId: readField(raw, ['llmModelId', 'llm_model_id'], ''), asrModelId: readField(raw, ['asrModelId', 'asr_model_id'], ''), embeddingModelId: readField(raw, ['embeddingModelId', 'embedding_model_id'], ''), rerankModelId: readField(raw, ['rerankModelId', 'rerank_model_id'], ''), }); const mapVoice = (raw: AnyRecord): Voice => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), vendor: ((): string => { const vendor = String(readField(raw, ['vendor'], '')).trim().toLowerCase(); if (vendor === 'dashscope') { return 'DashScope'; } if (vendor === 'siliconflow' || vendor === '硅基流动' || vendor === 'openai-compatible') { return 'OpenAI Compatible'; } return String(readField(raw, ['vendor'], 'OpenAI Compatible')) || 'OpenAI Compatible'; })(), gender: readField(raw, ['gender'], ''), language: readField(raw, ['language'], ''), description: readField(raw, ['description'], ''), model: readField(raw, ['model'], ''), voiceKey: readField(raw, ['voiceKey', 'voice_key'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), baseUrl: readField(raw, ['baseUrl', 'base_url'], ''), speed: Number(readField(raw, ['speed'], 1)), gain: Number(readField(raw, ['gain'], 0)), pitch: Number(readField(raw, ['pitch'], 0)), enabled: Boolean(readField(raw, ['enabled'], true)), isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)), }); const mapASRModel = (raw: AnyRecord): ASRModel => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), vendor: readField(raw, ['vendor'], 'OpenAI Compatible'), language: readField(raw, ['language'], 'zh'), baseUrl: readField(raw, ['baseUrl', 'base_url'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), modelName: readField(raw, ['modelName', 'model_name'], ''), hotwords: readField(raw, ['hotwords'], []), enablePunctuation: Boolean(readField(raw, ['enablePunctuation', 'enable_punctuation'], true)), enableNormalization: Boolean(readField(raw, ['enableNormalization', 'enable_normalization'], true)), enabled: Boolean(readField(raw, ['enabled'], true)), }); const mapLLMModel = (raw: AnyRecord): LLMModel => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), vendor: readField(raw, ['vendor'], 'OpenAI Compatible'), type: readField(raw, ['type'], 'text'), baseUrl: readField(raw, ['baseUrl', 'base_url'], ''), apiKey: readField(raw, ['apiKey', 'api_key'], ''), modelName: readField(raw, ['modelName', 'model_name'], ''), temperature: Number(readField(raw, ['temperature'], 0.7)), contextLength: Number(readField(raw, ['contextLength', 'context_length'], 0)), enabled: Boolean(readField(raw, ['enabled'], true)), }); const mapTool = (raw: AnyRecord): Tool => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), description: readField(raw, ['description'], ''), category: readField(raw, ['category'], 'system') as 'system' | 'query', icon: readField(raw, ['icon'], 'Wrench'), httpMethod: readField(raw, ['httpMethod', 'http_method'], 'GET') as Tool['httpMethod'], httpUrl: readField(raw, ['httpUrl', 'http_url'], ''), httpHeaders: readField(raw, ['httpHeaders', 'http_headers'], {}), httpTimeoutMs: Number(readField(raw, ['httpTimeoutMs', 'http_timeout_ms'], 10000)), isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)), enabled: Boolean(readField(raw, ['enabled'], true)), isCustom: !Boolean(readField(raw, ['isSystem', 'is_system'], false)), }); const mapWorkflowNode = (raw: AnyRecord): WorkflowNode => ({ id: readField(raw, ['id'], ''), name: readField(raw, ['name'], String(readField(raw, ['id'], ''))), type: readField(raw, ['type'], 'assistant') as WorkflowNode['type'], isStart: readField(raw, ['isStart', 'is_start'], undefined), metadata: (() => { const metadata = readField(raw, ['metadata'], null); if (metadata && typeof metadata === 'object') return metadata; const position = readField(raw, ['position'], null); if (position && typeof position === 'object') return { position }; return { position: { x: 200, y: 200 } }; })(), assistantId: readField(raw, ['assistantId', 'assistant_id'], undefined), assistant: readField(raw, ['assistant'], undefined), prompt: readField(raw, ['prompt'], ''), messagePlan: readField(raw, ['messagePlan', 'message_plan'], undefined), variableExtractionPlan: readField(raw, ['variableExtractionPlan', 'variable_extraction_plan'], undefined), tool: readField(raw, ['tool'], undefined), globalNodePlan: readField(raw, ['globalNodePlan', 'global_node_plan'], undefined), }); const mapWorkflowEdge = (raw: AnyRecord): WorkflowEdge => ({ id: readField(raw, ['id'], undefined), fromNodeId: readField(raw, ['fromNodeId', 'from', 'from_', 'source'], ''), toNodeId: readField(raw, ['toNodeId', 'to', 'target'], ''), from: readField(raw, ['fromNodeId', 'from', 'from_', 'source'], ''), to: readField(raw, ['toNodeId', 'to', 'target'], ''), label: readField(raw, ['label'], undefined), condition: readField(raw, ['condition'], undefined), priority: Number(readField(raw, ['priority'], 100)), }); const mapWorkflow = (raw: AnyRecord): Workflow => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), nodeCount: Number(readField(raw, ['nodeCount', 'node_count'], Array.isArray(raw.nodes) ? raw.nodes.length : 0)), createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')), updatedAt: normalizeDateLabel(readField(raw, ['updatedAt', 'updated_at'], '')), nodes: readField(raw, ['nodes'], []).map((node: AnyRecord) => mapWorkflowNode(node)), edges: readField(raw, ['edges'], []).map((edge: AnyRecord) => mapWorkflowEdge(edge)), globalPrompt: readField(raw, ['globalPrompt', 'global_prompt'], ''), }); const mapKnowledgeDocument = (raw: AnyRecord): KnowledgeDocument => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), size: readField(raw, ['size'], ''), uploadDate: normalizeDateLabel(readField(raw, ['uploadDate', 'upload_date', 'created_at'], '')), status: readField(raw, ['status'], 'pending'), chunkCount: Number(readField(raw, ['chunkCount', 'chunk_count'], 0)), }); const mapKnowledgeBase = (raw: AnyRecord): KnowledgeBase => ({ id: String(readField(raw, ['id'], '')), name: readField(raw, ['name'], ''), creator: 'Admin', createdAt: normalizeDateLabel(readField(raw, ['createdAt', 'created_at'], '')), description: readField(raw, ['description'], ''), embeddingModel: readField(raw, ['embeddingModel', 'embedding_model'], ''), chunkSize: Number(readField(raw, ['chunkSize', 'chunk_size'], 500)), chunkOverlap: Number(readField(raw, ['chunkOverlap', 'chunk_overlap'], 50)), status: readField(raw, ['status'], 'active'), documents: readField(raw, ['documents'], []).map((doc: AnyRecord) => mapKnowledgeDocument(doc)), }); const toHistoryRow = (raw: AnyRecord, assistantNameMap: Map): CallLog => { const assistantId = readField(raw, ['assistant_id', 'assistantId'], ''); const startTime = normalizeDateLabel(readField(raw, ['started_at', 'startTime'], '')); const rawType = String(readField(raw, ['type'], 'text')).toLowerCase(); const type: CallLog['type'] = rawType === 'audio' || rawType === 'video' ? rawType : 'text'; return { id: String(readField(raw, ['id'], '')), source: readField(raw, ['source'], 'debug') as 'debug' | 'external', status: readField(raw, ['status'], 'connected') as 'connected' | 'missed', startTime, duration: formatDuration(readField(raw, ['duration_seconds', 'durationSeconds'], 0)), agentName: assistantNameMap.get(String(assistantId)) || String(assistantId || 'Unknown Assistant'), type, details: [], }; }; const toHistoryDetails = (raw: AnyRecord): InteractionDetail[] => { const transcripts = readField(raw, ['transcripts'], []); return transcripts.map((t: AnyRecord) => ({ role: readField(t, ['speaker'], 'human') === 'human' ? 'user' : 'assistant', content: readField(t, ['content'], ''), timestamp: `${Math.floor(Number(readField(t, ['startMs', 'start_ms'], 0)) / 1000)}s`, audioUrl: readField(t, ['audioUrl', 'audio_url'], undefined), })); }; export const fetchAssistants = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/assistants')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapAssistant(item)); }; export const createAssistant = async (data: Partial): Promise => { const payload = { name: data.name || 'New Assistant', firstTurnMode: data.firstTurnMode || 'bot_first', opener: data.opener || '', generatedOpenerEnabled: data.generatedOpenerEnabled ?? false, openerAudioEnabled: data.openerAudioEnabled ?? false, prompt: data.prompt || '', knowledgeBaseId: data.knowledgeBaseId || '', language: data.language || 'zh', voiceOutputEnabled: data.voiceOutputEnabled ?? true, voice: data.voice || '', speed: data.speed ?? 1, hotwords: data.hotwords || [], tools: data.tools || [], botCannotBeInterrupted: data.botCannotBeInterrupted ?? false, interruptionSensitivity: data.interruptionSensitivity ?? 500, configMode: data.configMode || 'platform', apiUrl: data.apiUrl || '', apiKey: data.apiKey || '', llmModelId: data.llmModelId || '', asrModelId: data.asrModelId || '', embeddingModelId: data.embeddingModelId || '', rerankModelId: data.rerankModelId || '', }; const response = await apiRequest('/assistants', { method: 'POST', body: payload }); return mapAssistant(response); }; export const updateAssistant = async (id: string, data: Partial): Promise => { const payload = { name: data.name, firstTurnMode: data.firstTurnMode, opener: data.opener, generatedOpenerEnabled: data.generatedOpenerEnabled, openerAudioEnabled: data.openerAudioEnabled, prompt: data.prompt, knowledgeBaseId: data.knowledgeBaseId, language: data.language, voiceOutputEnabled: data.voiceOutputEnabled, voice: data.voice, speed: data.speed, hotwords: data.hotwords, tools: data.tools, botCannotBeInterrupted: data.botCannotBeInterrupted, interruptionSensitivity: data.interruptionSensitivity, configMode: data.configMode, apiUrl: data.apiUrl, apiKey: data.apiKey, llmModelId: data.llmModelId, asrModelId: data.asrModelId, embeddingModelId: data.embeddingModelId, rerankModelId: data.rerankModelId, }; const response = await apiRequest(`/assistants/${id}`, { method: 'PUT', body: payload }); return mapAssistant(response); }; export const deleteAssistant = async (id: string): Promise => { await apiRequest(`/assistants/${id}`, { method: 'DELETE' }); }; export interface AssistantRuntimeConfigResponse { assistantId: string; configVersionId?: string; assistant?: Record; sessionStartMetadata: Record; sources?: { llmModelId?: string; asrModelId?: string; voiceId?: string; }; warnings?: string[]; } export interface AssistantOpenerAudioStatus { enabled: boolean; ready: boolean; encoding: string; sample_rate_hz: number; channels: number; duration_ms: number; updated_at?: string | null; text_hash?: string | null; tts_fingerprint?: string | null; } export const fetchAssistantRuntimeConfig = async (assistantId: string): Promise => { return apiRequest(`/assistants/${assistantId}/config`); }; export const fetchAssistantOpenerAudioStatus = async (assistantId: string): Promise => { return apiRequest(`/assistants/${assistantId}/opener-audio`); }; export const generateAssistantOpenerAudio = async ( assistantId: string, payload?: { text?: string } ): Promise => { return apiRequest(`/assistants/${assistantId}/opener-audio/generate`, { method: 'POST', body: payload || {}, }); }; export const fetchVoices = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/voices')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapVoice(item)); }; export const createVoice = async (data: Partial): Promise => { const payload = { name: data.name || 'New Voice', vendor: data.vendor || 'OpenAI Compatible', gender: data.gender || 'Female', language: data.language || 'zh', description: data.description || '', model: data.model || undefined, voice_key: data.voiceKey || undefined, api_key: data.apiKey || undefined, base_url: data.baseUrl || undefined, speed: data.speed ?? 1, gain: data.gain ?? 0, pitch: data.pitch ?? 0, enabled: data.enabled ?? true, }; const response = await apiRequest('/voices', { method: 'POST', body: payload }); return mapVoice(response); }; export const updateVoice = async (id: string, data: Partial): Promise => { const payload = { name: data.name, vendor: data.vendor, gender: data.gender, language: data.language, description: data.description, model: data.model, voice_key: data.voiceKey, api_key: data.apiKey, base_url: data.baseUrl, speed: data.speed, gain: data.gain, pitch: data.pitch, enabled: data.enabled, }; const response = await apiRequest(`/voices/${id}`, { method: 'PUT', body: payload }); return mapVoice(response); }; export const deleteVoice = async (id: string): Promise => { await apiRequest(`/voices/${id}`, { method: 'DELETE' }); }; export const previewVoice = async (id: string, text: string, speed?: number, apiKey?: string): Promise => { const response = await apiRequest<{ success: boolean; audio_url?: string; error?: string }>(`/voices/${id}/preview`, { method: 'POST', body: { text, speed, api_key: apiKey }, }); if (!response.success || !response.audio_url) { throw new Error(response.error || 'Preview failed'); } return response.audio_url; }; export const fetchASRModels = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/asr')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapASRModel(item)); }; export const createASRModel = async (data: Partial): Promise => { const payload = { name: data.name || 'New ASR Model', vendor: data.vendor || 'OpenAI Compatible', language: data.language || 'zh', base_url: data.baseUrl || 'https://api.siliconflow.cn/v1', api_key: data.apiKey || '', model_name: data.modelName || undefined, hotwords: data.hotwords || [], enable_punctuation: data.enablePunctuation ?? true, enable_normalization: data.enableNormalization ?? true, enabled: data.enabled ?? true, }; const response = await apiRequest('/asr', { method: 'POST', body: payload }); return mapASRModel(response); }; export const updateASRModel = async (id: string, data: Partial): Promise => { const payload = { name: data.name, vendor: data.vendor, language: data.language, base_url: data.baseUrl, api_key: data.apiKey, model_name: data.modelName, hotwords: data.hotwords, enable_punctuation: data.enablePunctuation, enable_normalization: data.enableNormalization, enabled: data.enabled, }; const response = await apiRequest(`/asr/${id}`, { method: 'PUT', body: payload }); return mapASRModel(response); }; export const deleteASRModel = async (id: string): Promise => { await apiRequest(`/asr/${id}`, { method: 'DELETE' }); }; export type ASRPreviewResult = { success: boolean; transcript?: string; language?: string; confidence?: number; latency_ms?: number; message?: string; error?: string; }; export const previewASRModel = async ( id: string, file: File, options?: { language?: string; apiKey?: string } ): Promise => { const formData = new FormData(); formData.append('file', file); if (options?.language) { formData.append('language', options.language); } if (options?.apiKey) { formData.append('api_key', options.apiKey); } const url = `${getApiBaseUrl()}/asr/${id}/preview`; const response = await fetch(url, { method: 'POST', body: formData, }); let data: ASRPreviewResult | null = null; try { data = await response.json(); } catch { data = null; } if (!response.ok) { const detail = (data as AnyRecord | null)?.error || (data as AnyRecord | null)?.detail || `Request failed: ${response.status}`; throw new Error(typeof detail === 'string' ? detail : `Request failed: ${response.status}`); } return data || { success: false, error: 'Invalid preview response' }; }; export const fetchLLMModels = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/llm')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapLLMModel(item)); }; export const createLLMModel = async (data: Partial): Promise => { const payload = { name: data.name || 'New LLM Model', vendor: data.vendor || 'OpenAI Compatible', type: data.type || 'text', base_url: data.baseUrl || '', api_key: data.apiKey || '', model_name: data.modelName || undefined, temperature: data.temperature, context_length: data.contextLength, enabled: data.enabled ?? true, }; const response = await apiRequest('/llm', { method: 'POST', body: payload }); return mapLLMModel(response); }; export const updateLLMModel = async (id: string, data: Partial): Promise => { const payload = { name: data.name, vendor: data.vendor, type: data.type, base_url: data.baseUrl, api_key: data.apiKey, model_name: data.modelName, temperature: data.temperature, context_length: data.contextLength, enabled: data.enabled, }; const response = await apiRequest(`/llm/${id}`, { method: 'PUT', body: payload }); return mapLLMModel(response); }; export const deleteLLMModel = async (id: string): Promise => { await apiRequest(`/llm/${id}`, { method: 'DELETE' }); }; export type LLMPreviewResult = { success: boolean; reply?: string; usage?: Record; latency_ms?: number; error?: string; }; export const previewLLMModel = async ( id: string, payload: { message: string; system_prompt?: string; max_tokens?: number; temperature?: number; api_key?: string } ): Promise => { return apiRequest(`/llm/${id}/preview`, { method: 'POST', body: payload, }); }; export const fetchTools = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/tools/resources')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapTool(item)); }; export const createTool = async (data: Partial): Promise => { const payload = { id: data.id || undefined, name: data.name || 'New Tool', description: data.description || '', category: data.category || 'system', icon: data.icon || (data.category === 'query' ? 'Globe' : 'Terminal'), http_method: data.httpMethod || 'GET', http_url: data.httpUrl || null, http_headers: data.httpHeaders || {}, http_timeout_ms: data.httpTimeoutMs ?? 10000, enabled: data.enabled ?? true, }; const response = await apiRequest('/tools/resources', { method: 'POST', body: payload }); return mapTool(response); }; export const updateTool = async (id: string, data: Partial): Promise => { const payload = { name: data.name, description: data.description, category: data.category, icon: data.icon, http_method: data.httpMethod, http_url: data.httpUrl, http_headers: data.httpHeaders, http_timeout_ms: data.httpTimeoutMs, enabled: data.enabled, }; const response = await apiRequest(`/tools/resources/${id}`, { method: 'PUT', body: payload }); return mapTool(response); }; export const deleteTool = async (id: string): Promise => { await apiRequest(`/tools/resources/${id}`, { method: 'DELETE' }); }; export const fetchWorkflows = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/workflows')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapWorkflow(item)); }; export const fetchWorkflowById = async (id: string): Promise => { const response = await apiRequest(`/workflows/${id}`); return mapWorkflow(response); }; export const createWorkflow = async (data: Partial): Promise => { const payload = { name: data.name || '新工作流', nodeCount: data.nodeCount ?? data.nodes?.length ?? 0, createdAt: data.createdAt || '', updatedAt: data.updatedAt || '', globalPrompt: data.globalPrompt || '', nodes: data.nodes || [], edges: data.edges || [], }; const response = await apiRequest('/workflows', { method: 'POST', body: payload }); return mapWorkflow(response); }; export const updateWorkflow = async (id: string, data: Partial): Promise => { const payload = { name: data.name, nodeCount: data.nodeCount ?? data.nodes?.length, nodes: data.nodes, edges: data.edges, globalPrompt: data.globalPrompt, }; const response = await apiRequest(`/workflows/${id}`, { method: 'PUT', body: payload }); return mapWorkflow(response); }; export const deleteWorkflow = async (id: string): Promise => { await apiRequest(`/workflows/${id}`, { method: 'DELETE' }); }; export const fetchKnowledgeBases = async (): Promise => { const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/knowledge/bases')); const list = Array.isArray(response) ? response : (response.list || []); return list.map((item) => mapKnowledgeBase(item)); }; export const fetchKnowledgeBaseById = async (kbId: string): Promise => { const response = await apiRequest(`/knowledge/bases/${kbId}`); return mapKnowledgeBase(response); }; export const createKnowledgeBase = async (data: { name: string; description?: string; embeddingModel?: string; chunkSize?: number; chunkOverlap?: number; }): Promise => { const payload = { name: data.name, description: data.description || '', embeddingModel: data.embeddingModel || 'text-embedding-3-small', chunkSize: data.chunkSize ?? 500, chunkOverlap: data.chunkOverlap ?? 50, }; const response = await apiRequest('/knowledge/bases', { method: 'POST', body: payload }); return mapKnowledgeBase(response); }; export const updateKnowledgeBase = async (kbId: string, data: Partial): Promise => { const payload = { name: data.name, description: data.description, embeddingModel: data.embeddingModel, chunkSize: data.chunkSize, chunkOverlap: data.chunkOverlap, status: data.status, }; const response = await apiRequest(`/knowledge/bases/${kbId}`, { method: 'PUT', body: payload }); return mapKnowledgeBase(response); }; export const deleteKnowledgeBase = async (kbId: string): Promise => { await apiRequest(`/knowledge/bases/${kbId}`, { method: 'DELETE' }); }; export const uploadKnowledgeDocument = async (kbId: string, file: File): Promise => { const formData = new FormData(); formData.append('file', file); formData.append('name', file.name); formData.append('size', `${file.size} bytes`); formData.append('file_type', file.type || 'application/octet-stream'); const url = `${getApiBaseUrl()}/knowledge/bases/${kbId}/documents`; const response = await fetch(url, { method: 'POST', body: formData, }); if (!response.ok) { let message = `Upload failed: ${response.status}`; try { const data = await response.json(); if (data?.detail) { message = typeof data.detail === 'string' ? data.detail : message; } } catch { // ignore parse error } throw new Error(message); } }; export const deleteKnowledgeDocument = async (kbId: string, docId: string): Promise => { await apiRequest(`/knowledge/bases/${kbId}/documents/${docId}`, { method: 'DELETE' }); }; export const indexKnowledgeDocument = async ( kbId: string, docId: string, content: string, ): Promise<{ message: string; chunkCount: number }> => { return apiRequest<{ message: string; chunkCount: number }>( `/knowledge/bases/${kbId}/documents/${docId}/index`, { method: 'POST', body: { document_id: docId, content, }, } ); }; export type KnowledgeSearchResultItem = { content: string; metadata?: { document_id?: string; chunk_index?: number; kb_id?: string; [key: string]: any; }; distance?: number; }; export type KnowledgeSearchResponse = { query: string; results: KnowledgeSearchResultItem[]; }; export const searchKnowledgeBase = async ( kbId: string, query: string, nResults: number = 5 ): Promise => { return apiRequest('/knowledge/search', { method: 'POST', body: { kb_id: kbId, query, nResults, }, }); }; export const fetchHistory = async (): Promise => { const [historyResp, assistantsResp] = await Promise.all([ apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/history')), apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>(withLimit('/assistants')), ]); const assistantList = Array.isArray(assistantsResp) ? assistantsResp : (assistantsResp.list || []); const assistantNameMap = new Map(assistantList.map((a: AnyRecord) => [String(readField(a, ['id'], '')), readField(a, ['name'], '')])); const historyList = Array.isArray(historyResp) ? historyResp : (historyResp.list || []); return historyList.map((item) => toHistoryRow(item, assistantNameMap)); }; export const fetchHistoryDetail = async (id: string, base: CallLog): Promise => { const response = await apiRequest(`/history/${id}`); const details = toHistoryDetails(response); const duration = formatDuration(readField(response, ['duration_seconds', 'durationSeconds'], 0)); const startTime = normalizeDateLabel(readField(response, ['started_at'], base.startTime)); return { ...base, startTime, duration, details, }; };