import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios'; import { getValidAccessToken, forceReauth } from './auth.js'; import { ENV } from './env.js'; import type { ProjectMeta, Agent, Skill, FlowEvent, FlowState, AkbImportArticle, CustomerProfile, CustomerAttribute, CustomerAttributesResponse, UserPersonaResponse, UserPersona, ChatHistoryParams, ChatHistoryResponse, CreateAgentRequest, CreateAgentResponse, CreateFlowRequest, CreateFlowResponse, CreateSkillRequest, CreateSkillResponse, CreateFlowEventRequest, CreateFlowEventResponse, UpdateFlowEventRequest, CreateFlowStateRequest, CreateFlowStateResponse, UpdateFlowStateRequest, UpdateFlowRequest, CreateSkillParameterRequest, CreateSkillParameterResponse, CreateCustomerAttributeRequest, CreateCustomerAttributeResponse, CreatePersonaRequest, CreatePersonaResponse, CreateProjectRequest, CreateProjectResponse, PublishFlowRequest, PublishFlowResponse, Integration, Connector, CreateSandboxPersonaRequest, CreateSandboxPersonaResponse, CreateActorRequest, CreateActorResponse, SendChatMessageRequest, ConversationActsParams, ConversationActsResponse, ScriptAction, IntegrationSetting, CreateConnectorRequest, CreateConnectorResponse, UpdateConnectorRequest, OutgoingWebhook, IncomingWebhook, PersonaSearchResponse, AkbTopicsResponse, Registry, RegistryItem, AddProjectFromRegistryRequest, CreateNewoCustomerRequest, CreateNewoCustomerResponse, LogsQueryParams, LogsResponse } from './types.js'; // Per-request retry tracking to avoid shared state issues const RETRY_SYMBOL = Symbol('retried'); export async function makeClient(verbose: boolean = false, token?: string): Promise { let accessToken = token || await getValidAccessToken(); if (verbose) console.log('✓ Access token obtained'); const client = axios.create({ baseURL: ENV.NEWO_BASE_URL, headers: { accept: 'application/json' } }); client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${accessToken}`; if (verbose) { console.log(`→ ${config.method?.toUpperCase()} ${config.url}`); if (config.data) console.log(' Data:', JSON.stringify(config.data, null, 2)); if (config.params) console.log(' Params:', config.params); } return config; }); client.interceptors.response.use( (response: AxiosResponse) => { if (verbose) { console.log(`← ${response.status} ${response.config.method?.toUpperCase()} ${response.config.url}`); if (response.data && Object.keys(response.data).length < 20) { console.log(' Response:', JSON.stringify(response.data, null, 2)); } else if (response.data) { const itemCount = Array.isArray(response.data) ? response.data.length : Object.keys(response.data).length; console.log(` Response: [${typeof response.data}] ${Array.isArray(response.data) ? itemCount + ' items' : 'large object'}`); } } return response; }, async (error: AxiosError) => { const status = error?.response?.status; if (verbose) { console.log(`← ${status} ${error.config?.method?.toUpperCase()} ${error.config?.url} - ${error.message}`); if (error.response?.data) console.log(' Error data:', error.response.data); } // Use per-request retry tracking to avoid shared state issues const config = error.config as InternalAxiosRequestConfig & { [RETRY_SYMBOL]?: boolean }; if ((status === 401 || status === 403) && !config?.[RETRY_SYMBOL]) { if (config) { config[RETRY_SYMBOL] = true; if (verbose) console.log('🔄 Retrying with fresh token...'); accessToken = await forceReauth(); config.headers = config.headers || {}; config.headers.Authorization = `Bearer ${accessToken}`; return client.request(config); } } throw error; } ); return client; } export async function listProjects(client: AxiosInstance): Promise { const response = await client.get('/api/v1/designer/projects'); return response.data; } export async function listAgents(client: AxiosInstance, projectId: string): Promise { const response = await client.get('/api/v1/bff/agents/list', { params: { project_id: projectId } }); return response.data; } export async function getProjectMeta(client: AxiosInstance, projectId: string): Promise { const response = await client.get(`/api/v1/designer/projects/by-id/${projectId}`); return response.data; } export async function listFlowSkills(client: AxiosInstance, flowId: string): Promise { const response = await client.get(`/api/v1/designer/flows/${flowId}/skills`); return response.data; } /** * Fetch a single flow's top-level descriptor. * * Used by push to detect whether local metadata.yaml title/description * differ from the platform before patching. */ export interface FlowDescriptor { id: string; idn: string; title: string; description: string | null; agent_id: string; default_runner_type: string; default_model: { provider_idn: string; model_idn: string }; publication_datetime?: string; last_change_datetime?: string; creation_datetime?: string; } export async function getFlow(client: AxiosInstance, flowId: string): Promise { const response = await client.get(`/api/v1/designer/flows/${flowId}`); return response.data; } export async function getSkill(client: AxiosInstance, skillId: string): Promise { const response = await client.get(`/api/v1/designer/skills/${skillId}`); return response.data; } export async function updateSkill(client: AxiosInstance, skillObject: Skill): Promise { await client.put(`/api/v1/designer/flows/skills/${skillObject.id}`, skillObject); } export async function listFlowEvents(client: AxiosInstance, flowId: string): Promise { const response = await client.get(`/api/v1/designer/flows/${flowId}/events`); return response.data; } export async function listFlowStates(client: AxiosInstance, flowId: string): Promise { const response = await client.get(`/api/v1/designer/flows/${flowId}/states`); return response.data; } export async function importAkbArticle(client: AxiosInstance, articleData: AkbImportArticle): Promise { const response = await client.post('/api/v1/akb/append-manual', articleData); return response.data; } export async function getCustomerProfile(client: AxiosInstance): Promise { const response = await client.get('/api/v1/customer/profile'); return response.data; } export async function getCustomerAttributes(client: AxiosInstance, includeHidden: boolean = true): Promise { const response = await client.get('/api/v1/bff/customer/attributes', { params: { include_hidden: includeHidden } }); return response.data; } export async function updateCustomerAttribute(client: AxiosInstance, attribute: CustomerAttribute): Promise { if (!attribute.id) { throw new Error(`Attribute ${attribute.idn} is missing ID - cannot update`); } await client.put(`/api/v1/customer/attributes/${attribute.id}`, { idn: attribute.idn, value: attribute.value, title: attribute.title, description: attribute.description, group: attribute.group, is_hidden: attribute.is_hidden, possible_values: attribute.possible_values, value_type: attribute.value_type }); } export async function getProjectAttributes( client: AxiosInstance, projectId: string, includeHidden: boolean = false ): Promise { const response = await client.get(`/api/v1/bff/projects/${projectId}/attributes`, { params: { query: '', include_hidden: includeHidden } }); return response.data; } export async function updateProjectAttribute( client: AxiosInstance, projectId: string, attribute: CustomerAttribute ): Promise { if (!attribute.id) { throw new Error(`Project attribute ${attribute.idn} is missing ID - cannot update`); } await client.put(`/api/v1/designer/projects/${projectId}/attributes/${attribute.id}`, { idn: attribute.idn, value: attribute.value, title: attribute.title, description: attribute.description, group: attribute.group, is_hidden: attribute.is_hidden, possible_values: attribute.possible_values, value_type: attribute.value_type }); } export async function createProjectAttribute( client: AxiosInstance, projectId: string, attributeData: CreateCustomerAttributeRequest ): Promise { const response = await client.post( `/api/v1/designer/projects/${projectId}/attributes`, attributeData ); return response.data; } export async function deleteProjectAttribute( client: AxiosInstance, projectId: string, attributeId: string ): Promise { await client.delete(`/api/v1/designer/projects/${projectId}/attributes/${attributeId}`); } // Conversation API Functions export async function listUserPersonas(client: AxiosInstance, page: number = 1, per: number = 50): Promise { const response = await client.get('/api/v1/bff/conversations/user-personas', { params: { page, per } }); return response.data; } export async function getUserPersona(client: AxiosInstance, personaId: string): Promise { const response = await client.get(`/api/v1/bff/conversations/user-personas/${personaId}`); return response.data; } export async function getAccount(client: AxiosInstance): Promise<{ id: string; [key: string]: any }> { const response = await client.get<{ id: string; [key: string]: any }>('/api/v1/account'); return response.data; } export async function getChatHistory(client: AxiosInstance, params: ChatHistoryParams): Promise { const queryParams: Record = { user_actor_id: params.user_actor_id, page: params.page || 1, per: params.per || 50 }; // Only add agent_actor_id if provided if (params.agent_actor_id) { queryParams.agent_actor_id = params.agent_actor_id; } const response = await client.get('/api/v1/chat/history', { params: queryParams }); return response.data; } // Entity Creation/Deletion API Functions export async function createAgent(client: AxiosInstance, projectId: string, agentData: CreateAgentRequest): Promise { // Use project-specific v2 endpoint for proper project association const response = await client.post(`/api/v2/designer/${projectId}/agents`, agentData); return response.data; } export async function deleteAgent(client: AxiosInstance, agentId: string): Promise { await client.delete(`/api/v1/designer/agents/${agentId}`); } export async function createFlow(client: AxiosInstance, agentId: string, flowData: CreateFlowRequest): Promise { // Use the correct NEWO endpoint pattern for flow creation const response = await client.post(`/api/v1/designer/${agentId}/flows/empty`, flowData); // The NEWO flow creation API returns empty response body with 201 status // The flow is created successfully, but we need to get the ID through agent listing if (response.status === 201) { // Flow created successfully, but ID will be retrieved during pull operation return { id: 'pending-sync' }; } throw new Error(`Flow creation failed with status: ${response.status}`); } export async function deleteFlow(client: AxiosInstance, flowId: string): Promise { await client.delete(`/api/v1/designer/flows/${flowId}`); } export async function createSkill(client: AxiosInstance, flowId: string, skillData: CreateSkillRequest): Promise { const response = await client.post(`/api/v1/designer/flows/${flowId}/skills`, skillData); return response.data; } export async function deleteSkill(client: AxiosInstance, skillId: string): Promise { await client.delete(`/api/v1/designer/flows/skills/${skillId}`); } export async function deleteSkillById(client: AxiosInstance, skillId: string): Promise { console.log(`🗑️ Deleting skill from platform: ${skillId}`); await client.delete(`/api/v1/designer/flows/skills/${skillId}`); console.log(`✅ Skill deleted from platform: ${skillId}`); } export async function createFlowEvent(client: AxiosInstance, flowId: string, eventData: CreateFlowEventRequest): Promise { const response = await client.post(`/api/v1/designer/flows/${flowId}/events`, eventData); return response.data; } export async function updateFlowEvent( client: AxiosInstance, eventId: string, eventData: UpdateFlowEventRequest ): Promise { await client.patch(`/api/v1/designer/flows/events/${eventId}`, eventData); } export async function deleteFlowEvent(client: AxiosInstance, eventId: string): Promise { await client.delete(`/api/v1/designer/flows/events/${eventId}`); } export async function createFlowState(client: AxiosInstance, flowId: string, stateData: CreateFlowStateRequest): Promise { const response = await client.post(`/api/v1/designer/flows/${flowId}/states`, stateData); return response.data; } export async function updateFlowState( client: AxiosInstance, stateId: string, stateData: UpdateFlowStateRequest ): Promise { await client.put(`/api/v1/designer/flows/states/${stateId}`, stateData); } export async function deleteFlowState(client: AxiosInstance, stateId: string): Promise { await client.delete(`/api/v1/designer/flows/states/${stateId}`); } /** * Update flow metadata (title, description, runner type, default model). * * Uses PATCH /api/v1/designer/flows/{flowId}. The platform requires the * full flow descriptor; sending a partial body returns 500. */ export async function updateFlow( client: AxiosInstance, flowId: string, flowData: UpdateFlowRequest ): Promise { await client.patch(`/api/v1/designer/flows/${flowId}`, flowData); } export async function createSkillParameter(client: AxiosInstance, skillId: string, paramData: CreateSkillParameterRequest): Promise { // Debug the parameter creation request console.log('Creating parameter for skill:', skillId); console.log('Parameter data:', JSON.stringify(paramData, null, 2)); try { const response = await client.post(`/api/v1/designer/flows/skills/${skillId}/parameters`, paramData); return response.data; } catch (error: any) { console.error('Parameter creation error details:', error.response?.data); throw error; } } export async function createCustomerAttribute(client: AxiosInstance, attributeData: CreateCustomerAttributeRequest): Promise { const response = await client.post('/api/v1/customer/attributes', attributeData); return response.data; } export async function createProject(client: AxiosInstance, projectData: CreateProjectRequest): Promise { const response = await client.post('/api/v1/designer/projects', projectData); return response.data; } export async function deleteProject(client: AxiosInstance, projectId: string): Promise { await client.delete(`/api/v1/designer/projects/${projectId}`); } export async function createPersona(client: AxiosInstance, personaData: CreatePersonaRequest): Promise { const response = await client.post('/api/v1/customer/personas', personaData); return response.data; } export async function publishFlow(client: AxiosInstance, flowId: string, publishData: PublishFlowRequest): Promise { const response = await client.post(`/api/v1/designer/flows/${flowId}/publish`, publishData); return response.data; } // Sandbox Chat API Functions export async function listIntegrations(client: AxiosInstance): Promise { const response = await client.get('/api/v1/integrations'); return response.data; } export async function listConnectors(client: AxiosInstance, integrationId: string): Promise { const response = await client.get(`/api/v1/integrations/${integrationId}/connectors`); return response.data; } export async function createSandboxPersona(client: AxiosInstance, personaData: CreateSandboxPersonaRequest): Promise { const response = await client.post('/api/v1/customer/personas', personaData); return response.data; } export async function createActor(client: AxiosInstance, personaId: string, actorData: CreateActorRequest): Promise { const response = await client.post(`/api/v1/customer/personas/${personaId}/actors`, actorData); return response.data; } export async function sendChatMessage(client: AxiosInstance, actorId: string, messageData: SendChatMessageRequest): Promise { await client.post(`/api/v1/chat/user/${actorId}`, messageData); } export async function getConversationActs(client: AxiosInstance, params: ConversationActsParams): Promise { const queryParams: Record = { user_persona_id: params.user_persona_id, user_actor_id: params.user_actor_id, per: params.per || 100, page: params.page || 1 }; // Only add agent_persona_id if provided if (params.agent_persona_id) { queryParams.agent_persona_id = params.agent_persona_id; } const response = await client.get('/api/v1/bff/conversations/acts', { params: queryParams }); return response.data; } // Script Actions API Functions export async function getScriptActions(client: AxiosInstance): Promise { const response = await client.get('/api/v1/script/actions'); return response.data; } // Integration API Functions export async function getIntegrationSettings(client: AxiosInstance, integrationId: string): Promise { const response = await client.get(`/api/v1/integrations/${integrationId}/settings`); return response.data; } // Connector CRUD API Functions export async function createConnector(client: AxiosInstance, integrationId: string, connectorData: CreateConnectorRequest): Promise { const response = await client.post(`/api/v1/integrations/${integrationId}/connectors`, connectorData); return response.data; } export async function updateConnector(client: AxiosInstance, connectorId: string, updateData: UpdateConnectorRequest): Promise { await client.put(`/api/v1/integrations/connectors/${connectorId}`, updateData); } export async function deleteConnector(client: AxiosInstance, connectorId: string): Promise { await client.delete(`/api/v1/integrations/connectors/${connectorId}`); } // Webhook API Functions export async function listOutgoingWebhooks(client: AxiosInstance): Promise { const response = await client.get('/api/v1/webhooks'); return response.data; } export async function listIncomingWebhooks(client: AxiosInstance): Promise { const response = await client.get('/api/v1/webhooks/incoming'); return response.data; } // AKB (Knowledge Base) API Functions export async function searchPersonas( client: AxiosInstance, isLinkedToAgent: boolean = true, page: number = 1, per: number = 30 ): Promise { const response = await client.get('/api/v1/bff/personas/search', { params: { is_linked_to_agent: isLinkedToAgent, page, per } }); return response.data; } export async function getAkbTopics( client: AxiosInstance, personaId: string, page: number = 1, per: number = 100, orderBy: string = 'created_at' ): Promise { const response = await client.get('/api/v1/akb/topics', { params: { persona_id: personaId, page, per, order_by: orderBy } }); return response.data; } // Project update export async function updateProject( client: AxiosInstance, projectId: string, updateData: Partial<{ title: string; description: string; is_auto_update_enabled: boolean; registry_idn: string; registry_item_idn: string | null; registry_item_version: string | null; }> ): Promise { await client.put(`/api/v1/designer/projects/${projectId}`, updateData); } // Agent update export async function updateAgent( client: AxiosInstance, agentId: string, updateData: Partial<{ title: string; description: string; persona_id: string | null; }> ): Promise { await client.put(`/api/v1/designer/agents/${agentId}`, updateData); } // Webhook creation export async function createOutgoingWebhook( client: AxiosInstance, webhookData: { connector_idn: string; event_idn: string; url: string; method: string; headers?: Record; body_template?: string; } ): Promise<{ id: string }> { const response = await client.post('/api/v1/webhooks', webhookData); return response.data; } export async function createIncomingWebhook( client: AxiosInstance, webhookData: { connector_idn: string; event_idn: string; } ): Promise<{ id: string; url: string }> { const response = await client.post('/api/v1/webhooks/incoming', webhookData); return response.data; } // Registry API Functions export async function listRegistries(client: AxiosInstance): Promise { const response = await client.get('/api/v1/designer/registries'); return response.data; } export async function listRegistryItems(client: AxiosInstance, registryId: string): Promise { const response = await client.get(`/api/v1/designer/registries/${registryId}/items`); return response.data; } export async function addProjectFromRegistry( client: AxiosInstance, projectData: AddProjectFromRegistryRequest ): Promise { // Uses the same endpoint as createProject, but with registry fields populated const response = await client.post('/api/v1/designer/projects', projectData); return response.data; } // Create NEWO Customer (v3 API - creates a new customer account) export async function createNewoCustomer( client: AxiosInstance, customerData: CreateNewoCustomerRequest ): Promise { const response = await client.post('/api/v3/customer', customerData); return response.data; } // Analytics Logs API export async function getLogs( client: AxiosInstance, params: LogsQueryParams ): Promise { // Build query params, handling arrays for levels and log_types const queryParams = new URLSearchParams(); if (params.page !== undefined) queryParams.set('page', String(params.page)); if (params.per !== undefined) queryParams.set('per', String(params.per)); if (params.from_datetime) queryParams.set('from_datetime', params.from_datetime); if (params.to_datetime) queryParams.set('to_datetime', params.to_datetime); if (params.message) queryParams.set('message', params.message); if (params.project_idn) queryParams.set('project_idn', params.project_idn); if (params.flow_idn) queryParams.set('flow_idn', params.flow_idn); if (params.skill_idn) queryParams.set('skill_idn', params.skill_idn); if (params.external_event_id) queryParams.set('external_event_id', params.external_event_id); if (params.runtime_context_id) queryParams.set('runtime_context_id', params.runtime_context_id); if (params.user_persona_ids) queryParams.set('user_persona_ids', params.user_persona_ids); if (params.user_actor_ids) queryParams.set('user_actor_ids', params.user_actor_ids); if (params.agent_persona_ids) queryParams.set('agent_persona_ids', params.agent_persona_ids); // Handle levels (can be single or array) if (params.levels) { const levelsValue = Array.isArray(params.levels) ? params.levels.join(',') : params.levels; queryParams.set('levels', levelsValue); } // Handle log_types (can be single or array) if (params.log_types) { const typesValue = Array.isArray(params.log_types) ? params.log_types.join(',') : params.log_types; queryParams.set('log_types', typesValue); } const response = await client.get(`/api/v1/analytics/logs?${queryParams.toString()}`); return response.data; } // ── Libraries ── export interface LibraryResponse { id: string; idn: string; project_id: string; skills: Skill[]; } export async function listLibraries(client: AxiosInstance, projectId: string): Promise { const response = await client.get( `/api/v1/designer/projects/${projectId}/libraries` ); return response.data; } export async function listLibrarySkills(client: AxiosInstance, libraryId: string): Promise { const response = await client.get( `/api/v1/designer/libraries/${libraryId}/skills` ); return response.data; } export async function updateLibrarySkill( client: AxiosInstance, libraryId: string, skillId: string, data: { prompt_script: string; [key: string]: unknown } ): Promise { await client.patch( `/api/v1/designer/libraries/${libraryId}/skills/${skillId}`, data ); } // ── V2 Bulk Export/Import ── export interface V2ExportOptions { export_akb?: boolean; export_customer_attributes?: boolean; exclude_projects_idn?: string[]; } export async function exportCustomerV2( client: AxiosInstance, customerId: string, options: V2ExportOptions = {} ): Promise { const params = new URLSearchParams(); params.set('customer_id', customerId); if (options.export_akb !== undefined) params.set('export_akb', String(options.export_akb)); if (options.export_customer_attributes !== undefined) params.set('export_customer_attributes', String(options.export_customer_attributes)); if (options.exclude_projects_idn && options.exclude_projects_idn.length > 0) { for (const idn of options.exclude_projects_idn) { params.append('exclude_projects_idn', idn); } } const response = await client.post(`/api/v2/designer/customer/export?${params.toString()}`, null, { responseType: 'arraybuffer', }); return Buffer.from(response.data as ArrayBuffer); }