UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

851 lines 30.3 kB
/** * HTTP/WebSocket Client Library * * Core client for accessing the NeuroLink API with built-in authentication, * retry logic, and middleware support. Supports both browser and Node.js environments. * * @module @neurolink/client */ import { HttpError, ClientNetworkError, ClientTimeoutError } from "./errors.js"; import { logger } from "../utils/logger.js"; import { tracers } from "../telemetry/tracers.js"; import { withClientSpan } from "../telemetry/withSpan.js"; // ============================================================================= // Shared Utilities // ============================================================================= /** * Combine multiple AbortSignals into a single signal. * Aborts as soon as any input signal is aborted. */ export function combineSignals(...signals) { const controller = new AbortController(); for (const signal of signals) { if (signal.aborted) { controller.abort(); break; } signal.addEventListener("abort", () => controller.abort()); } return controller.signal; } /** * Promise-based delay utility. */ export function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } // ============================================================================= // Constants // ============================================================================= /** * Default request timeout in milliseconds (30 seconds) */ const DEFAULT_TIMEOUT = 30_000; /** * Default retry configuration */ const DEFAULT_RETRY_CONFIG = { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 10000, backoffMultiplier: 2, retryableStatusCodes: [408, 429, 500, 502, 503, 504], retryOnNetworkError: true, }; // Re-export error classes so existing consumers importing from httpClient still work export { HttpError as NeuroLinkApiError, ClientNetworkError, ClientTimeoutError, }; // ============================================================================= // HTTP Client Class // ============================================================================= /** * HTTP Client for NeuroLink API * * Provides type-safe access to all NeuroLink API endpoints with * built-in authentication, retry logic, and middleware support. * * @example Basic usage * ```typescript * import { createClient } from '@neurolink/client'; * * const client = createClient({ * baseUrl: 'https://api.neurolink.example.com', * apiKey: 'your-api-key', * }); * * const result = await client.generate({ * input: { text: 'Hello, world!' }, * provider: 'openai', * }); * ``` * * @example With middleware * ```typescript * const client = createClient({ baseUrl: 'https://api.example.com' }); * * client.use(async (request, next) => { * console.log('Request:', request.url); * const response = await next(); * console.log('Response:', response.status); * return response; * }); * ``` */ export class NeuroLinkClient { config; middlewares = []; wsConnection = null; wsState = "disconnected"; wsMessageHandlers = new Map(); constructor(config) { // Validate required config if (!config.baseUrl) { throw new Error("baseUrl is required in client configuration"); } this.config = { baseUrl: config.baseUrl.replace(/\/$/, ""), apiKey: config.apiKey ?? "", token: config.token ?? "", timeout: config.timeout ?? DEFAULT_TIMEOUT, headers: config.headers ?? {}, retry: { ...DEFAULT_RETRY_CONFIG, ...config.retry }, debug: config.debug ?? false, fetch: config.fetch ?? this.getDefaultFetch(), wsUrl: config.wsUrl ?? config.baseUrl.replace(/^http/, "ws").replace(/\/$/, ""), }; } /** * Get default fetch implementation */ getDefaultFetch() { if (typeof globalThis.fetch === "function") { return globalThis.fetch.bind(globalThis); } throw new Error("No fetch implementation available. Please provide a custom fetch function in the client configuration."); } // =========================================================================== // ClientMiddleware Methods // =========================================================================== /** * Add middleware to the client * * @param middleware - ClientMiddleware function * @returns Client instance for chaining */ use(middleware) { this.middlewares.push(middleware); return this; } /** * Clear all middleware */ clearMiddleware() { this.middlewares = []; return this; } // =========================================================================== // Internal Request Methods // =========================================================================== /** * Generate unique request ID */ generateRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Calculate delay for exponential backoff */ calculateDelay(attempt, config) { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt); const jitter = Math.random() * 0.1 * delay; return Math.min(delay + jitter, config.maxDelayMs); } /** * Delay utility — delegates to shared sleep() */ delay(ms) { return sleep(ms); } /** * Combine multiple abort signals — delegates to shared combineSignals() */ combineSignals(...signals) { return combineSignals(...signals); } /** * Debug logging via the unified NeuroLink logger. * Guarded by `logger.shouldLog("debug")` so expensive serialization is * skipped when debug output is disabled. */ log(message, data) { if (logger.shouldLog("debug")) { logger.debug(`[HttpClient] ${message}`, data); } } /** * Execute HTTP request with middleware and retry logic */ async request(method, path, body, options) { return withClientSpan({ name: "neurolink.client.http.request", tracer: tracers.http, attributes: { "http.method": method, "http.route": path, "http.url": `${this.config.baseUrl}${path}`, }, }, async () => this._doRequest(method, path, body, options)); } async _doRequest(method, path, body, options) { const url = `${this.config.baseUrl}${path}`; const requestId = this.generateRequestId(); const { retry } = this.config; const skipRetry = options?.skipRetry ?? false; const context = { startTime: Date.now(), requestId, retryCount: 0, }; const middlewareRequest = { url, method, headers: { "Content-Type": "application/json", Accept: "application/json", "X-Request-ID": requestId, ...(this.config.apiKey ? { "X-API-Key": this.config.apiKey } : {}), ...(this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}), ...this.config.headers, ...options?.headers, }, body, context, }; /** * Execute the actual HTTP request */ const executeRequest = async () => { const controller = new AbortController(); const timeoutMs = options?.timeout ?? this.config.timeout; const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const signal = options?.signal ? this.combineSignals(options.signal, controller.signal) : controller.signal; this.log(`${method} ${url}`); const response = await this.config.fetch(url, { method, headers: middlewareRequest.headers, body: body ? JSON.stringify(body) : undefined, signal, }); clearTimeout(timeoutId); let responseBody; const contentType = response.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) { responseBody = await response.json(); } else { responseBody = await response.text(); } const headers = {}; response.headers.forEach((value, key) => { headers[key] = value; }); this.log(`Response ${response.status}`, { duration: Date.now() - context.startTime, }); return { status: response.status, headers, body: responseBody, context, }; } catch (error) { clearTimeout(timeoutId); if (error.name === "AbortError") { throw new ClientTimeoutError(timeoutMs); } throw new ClientNetworkError(`Network request failed: ${error.message}`, { cause: error }); } }; /** * Execute request with retry logic */ const executeWithRetry = async () => { let lastError; for (let attempt = 0; attempt < retry.maxAttempts; attempt++) { try { const response = await executeRequest(); // Check if response status is retryable if (!skipRetry && retry.retryableStatusCodes?.includes(response.status)) { if (attempt < retry.maxAttempts - 1) { const delayMs = this.calculateDelay(attempt, retry); this.log(`Retrying after ${delayMs}ms (attempt ${attempt + 1})`); await this.delay(delayMs); context.retryCount = attempt + 1; continue; } } return response; } catch (error) { lastError = error; // Check if network error and should retry if (!skipRetry && retry.retryOnNetworkError && error instanceof ClientNetworkError && attempt < retry.maxAttempts - 1) { const delayMs = this.calculateDelay(attempt, retry); this.log(`Retrying after network error (attempt ${attempt + 1})`); await this.delay(delayMs); context.retryCount = attempt + 1; continue; } throw error; } } throw lastError ?? new Error("Max retries exceeded"); }; // Execute with middleware chain let index = 0; const next = async () => { if (index < this.middlewares.length) { const middleware = this.middlewares[index++]; return middleware(middlewareRequest, next); } return executeWithRetry(); }; const response = await next(); // Check for error responses if (response.status >= 400) { // Server may wrap errors as { error: { code, message }, metadata: {...} } const rawBody = response.body; const errorPayload = rawBody && typeof rawBody === "object" && "error" in rawBody ? rawBody.error : rawBody; throw new HttpError(errorPayload?.message ?? `HTTP ${response.status}`, response.status, { code: errorPayload?.code, details: errorPayload?.details, requestId, }); } // Unwrap server response envelope if present // The NeuroLink server wraps responses as { data: <payload>, metadata: {...} } const responseBody = response.body; const unwrapped = responseBody && typeof responseBody === "object" && "data" in responseBody && "metadata" in responseBody ? responseBody.data : response.body; return { data: unwrapped, status: response.status, headers: response.headers, duration: Date.now() - context.startTime, requestId, }; } // =========================================================================== // Generation API // =========================================================================== /** * Generate text using AI models * * @example * ```typescript * const response = await client.generate({ * input: { text: 'Write a poem about coding' }, * provider: 'openai', * model: 'gpt-4o', * temperature: 0.7, * }); * console.log(response.data.content); * ``` */ async generate(options, requestOptions) { return this.request("POST", "/api/agent/execute", options, requestOptions); } /** * Stream text generation * * @example * ```typescript * await client.stream( * { input: { text: 'Tell me a story' }, provider: 'openai' }, * { * onText: (text) => process.stdout.write(text), * onDone: (result) => console.log('\nDone!', result.usage), * } * ); * ``` */ async stream(options, callbacks, requestOptions) { const url = `${this.config.baseUrl}/api/agent/stream`; const requestId = this.generateRequestId(); const response = await this.config.fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "text/event-stream", "X-Request-ID": requestId, ...(this.config.apiKey ? { "X-API-Key": this.config.apiKey } : {}), ...(this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}), ...this.config.headers, ...requestOptions?.headers, }, body: JSON.stringify(options), signal: requestOptions?.signal, }); if (!response.ok) { const rawBody = (await response.json()); // Server may wrap errors as { error: { code, message }, metadata: {...} } const errorPayload = rawBody && typeof rawBody === "object" && "error" in rawBody ? rawBody.error : rawBody; throw new HttpError(errorPayload?.message ?? `HTTP ${response.status}`, response.status, { code: errorPayload?.code, details: errorPayload?.details, requestId, }); } const reader = response.body?.getReader(); if (!reader) { throw new Error("Response body is not readable"); } const decoder = new TextDecoder(); let buffer = ""; let fullContent = ""; const toolCalls = []; const toolResults = []; try { while (true) { const { done, value } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") { const result = { content: fullContent, toolCalls: toolCalls, toolResults: toolResults, }; callbacks?.onDone?.(result); return result; } try { const event = JSON.parse(data); this.handleStreamEvent(event, callbacks, { onText: (text) => { fullContent += text; }, onToolCall: (toolCall) => { toolCalls.push(toolCall); }, onToolResult: (toolResult) => { toolResults.push(toolResult); }, }); } catch { // Ignore parse errors for malformed events this.log("Failed to parse stream event", data); } } } } } finally { reader.releaseLock(); } return { content: fullContent, toolCalls: toolCalls, toolResults: toolResults, }; } /** * Handle individual stream events */ handleStreamEvent(event, callbacks, internalCallbacks) { switch (event.type) { case "text": if (event.content) { callbacks?.onText?.(event.content); internalCallbacks?.onText?.(event.content); } break; case "tool-call": if (event.toolCall) { callbacks?.onToolCall?.(event.toolCall); internalCallbacks?.onToolCall?.(event.toolCall); } break; case "tool-result": if (event.toolResult) { callbacks?.onToolResult?.(event.toolResult); internalCallbacks?.onToolResult?.(event.toolResult); } break; case "error": if (event.error) { callbacks?.onError?.(event.error); } break; case "metadata": if (event.metadata) { callbacks?.onMetadata?.(event.metadata); } break; case "audio": if (event.audio) { callbacks?.onAudio?.(event.audio); } break; case "thinking": if (event.thinking) { callbacks?.onThinking?.(event.thinking); } break; } } // =========================================================================== // Agent API // =========================================================================== /** * Execute an agent * * @example * ```typescript * const result = await client.executeAgent({ * agentId: 'customer-support', * input: 'I need help with my order', * sessionId: 'user-123', * }); * console.log(result.data.content); * ``` */ async executeAgent(options, requestOptions) { return this.request("POST", `/api/agents/${options.agentId}/execute`, options, requestOptions); } /** * Stream agent execution */ async streamAgent(options, callbacks, requestOptions) { return this.stream({ input: { text: options.input }, context: options.context, ...(options.tools ? { tools: options.tools.enabled } : {}), }, callbacks, requestOptions); } /** * List available agents */ async listAgents(requestOptions) { return this.request("GET", "/api/agents", undefined, requestOptions); } /** * Get agent details */ async getAgent(agentId, requestOptions) { return this.request("GET", `/api/agents/${agentId}`, undefined, requestOptions); } // =========================================================================== // Workflow API // =========================================================================== /** * Execute a workflow * * @example * ```typescript * const result = await client.executeWorkflow({ * workflowId: 'data-pipeline', * input: { data: [...] }, * }); * * if (result.data.status === 'running') { * // Poll for completion * const status = await client.getWorkflowStatus( * 'data-pipeline', * result.data.runId * ); * } * ``` */ async executeWorkflow(options, requestOptions) { return this.request("POST", `/api/workflows/${options.workflowId}/execute`, options, requestOptions); } /** * Resume a suspended workflow */ async resumeWorkflow(workflowId, resumeToken, resumeData, requestOptions) { return this.request("POST", `/api/workflows/${workflowId}/resume`, { resumeToken, resumeData }, requestOptions); } /** * Get workflow execution status */ async getWorkflowStatus(workflowId, runId, requestOptions) { return this.request("GET", `/api/workflows/${workflowId}/runs/${runId}`, undefined, requestOptions); } /** * Cancel workflow execution */ async cancelWorkflow(workflowId, runId, requestOptions) { return this.request("POST", `/api/workflows/${workflowId}/runs/${runId}/cancel`, undefined, requestOptions); } /** * List available workflows */ async listWorkflows(requestOptions) { return this.request("GET", "/api/workflows", undefined, requestOptions); } /** * Get workflow details */ async getWorkflow(workflowId, requestOptions) { return this.request("GET", `/api/workflows/${workflowId}`, undefined, requestOptions); } // =========================================================================== // Tools API // =========================================================================== /** * List available tools * * @example * ```typescript * const tools = await client.listTools({ category: 'data' }); * console.log(tools.data); * ``` */ async listTools(options, requestOptions) { const params = new URLSearchParams(); if (options?.category) { params.set("category", options.category); } if (options?.serverId) { params.set("serverId", options.serverId); } const query = params.toString(); return this.request("GET", `/api/tools${query ? `?${query}` : ""}`, undefined, requestOptions); } /** * Execute a tool */ async executeTool(toolName, params, requestOptions) { return this.request("POST", `/api/tools/${toolName}/execute`, { params }, requestOptions); } /** * Get tool details */ async getTool(toolName, requestOptions) { return this.request("GET", `/api/tools/${toolName}`, undefined, requestOptions); } // =========================================================================== // Provider API // =========================================================================== /** * List available providers */ async listProviders(requestOptions) { return this.request("GET", "/api/providers", undefined, requestOptions); } /** * Get provider status */ async getProviderStatus(providerName, requestOptions) { return this.request("GET", `/api/providers/${providerName}/status`, undefined, requestOptions); } // =========================================================================== // Health API // =========================================================================== /** * Health check */ async health(requestOptions) { return this.request("GET", "/api/health", undefined, requestOptions); } // =========================================================================== // WebSocket Methods // =========================================================================== /** * Connect to WebSocket for real-time communication * * @example * ```typescript * client.connectWebSocket({ * url: 'wss://api.example.com/ws', * autoReconnect: true, * }); * * client.onWebSocketMessage('chat', (data) => { * console.log('Chat message:', data); * }); * ``` */ connectWebSocket(options) { if (typeof WebSocket === "undefined") { throw new Error("WebSocket is not available in this environment. Please use a polyfill."); } const wsUrl = options?.url ?? `${this.config.wsUrl}/ws`; const protocols = options?.protocols; this.wsState = "connecting"; this.wsConnection = new WebSocket(wsUrl, protocols); this.wsConnection.onopen = () => { this.wsState = "connected"; this.log("WebSocket connected"); // Send auth if configured if (this.config.apiKey || this.config.token) { this.sendWebSocketMessage({ type: "auth", apiKey: this.config.apiKey, token: this.config.token, }); } }; this.wsConnection.onmessage = (event) => { try { const data = JSON.parse(event.data); const messageType = data.type ?? "message"; const handlers = this.wsMessageHandlers.get(messageType); if (handlers) { handlers.forEach((handler) => handler(data)); } // Also notify general message handlers const allHandlers = this.wsMessageHandlers.get("*"); if (allHandlers) { allHandlers.forEach((handler) => handler(data)); } } catch { this.log("Failed to parse WebSocket message", event.data); } }; this.wsConnection.onclose = () => { this.wsState = "disconnected"; this.log("WebSocket disconnected"); if (options?.autoReconnect) { const interval = options.reconnectInterval ?? 5000; setTimeout(() => { if (this.wsState === "disconnected") { this.connectWebSocket(options); } }, interval); } }; this.wsConnection.onerror = (error) => { this.log("WebSocket error", error); }; } /** * Disconnect WebSocket */ disconnectWebSocket() { if (this.wsConnection) { this.wsState = "disconnecting"; this.wsConnection.close(); this.wsConnection = null; } } /** * Send message over WebSocket */ sendWebSocketMessage(data) { if (this.wsConnection?.readyState === WebSocket.OPEN) { this.wsConnection.send(JSON.stringify(data)); } else { throw new Error("WebSocket is not connected"); } } /** * Register WebSocket message handler */ onWebSocketMessage(messageType, handler) { if (!this.wsMessageHandlers.has(messageType)) { this.wsMessageHandlers.set(messageType, new Set()); } const handlers = this.wsMessageHandlers.get(messageType); if (handlers) { handlers.add(handler); } // Return unsubscribe function return () => { this.wsMessageHandlers.get(messageType)?.delete(handler); }; } /** * Get WebSocket connection state */ getWebSocketState() { return this.wsState; } // =========================================================================== // Configuration Methods // =========================================================================== /** * Update client configuration */ updateConfig(config) { if (config.baseUrl) { this.config.baseUrl = config.baseUrl.replace(/\/$/, ""); } if (config.apiKey !== undefined) { this.config.apiKey = config.apiKey; } if (config.token !== undefined) { this.config.token = config.token; } if (config.timeout !== undefined) { this.config.timeout = config.timeout; } if (config.headers) { this.config.headers = { ...this.config.headers, ...config.headers }; } if (config.retry) { this.config.retry = { ...this.config.retry, ...config.retry }; } if (config.debug !== undefined) { this.config.debug = config.debug; } } /** * Get current configuration (readonly) */ getConfig() { return { ...this.config }; } } // ============================================================================= // Factory Function // ============================================================================= /** * Create a new NeuroLink client instance * * @example * ```typescript * import { createClient } from '@neurolink/client'; * * const client = createClient({ * baseUrl: 'https://api.neurolink.example.com', * apiKey: process.env.NEUROLINK_API_KEY, * debug: true, * }); * ``` */ export function createClient(config) { return new NeuroLinkClient(config); } //# sourceMappingURL=httpClient.js.map