// Import core types and classes for the TrustGraph API import { Triple, Term } from "../models/Triple.js"; import { ServiceCallMulti } from "./service-call-multi.js"; import { ServiceCall } from "./service-call.js"; import { getWebSocketConstructor, getDefaultSocketUrl, getRandomValues, WS_CONNECTING, WS_OPEN, WS_CLOSED, type IsomorphicWebSocket, type WsMessageEvent, type WsCloseEvent, type WsEvent, } from "./websocket-adapter.js"; // Import all message types for different services import { AgentRequest, AgentResponse, ConfigRequest, ConfigResponse, DocumentMetadata, DocumentRagRequest, DocumentRagResponse, EmbeddingsRequest, EmbeddingsResponse, EntityMatch, FlowRequest, FlowResponse, GraphEmbeddingsQueryRequest, GraphEmbeddingsQueryResponse, GraphRagRequest, GraphRagResponse, // KnowledgeRequest, // KnowledgeResponse, LibraryRequest, LibraryResponse, LoadDocumentRequest, LoadDocumentResponse, LoadTextRequest, LoadTextResponse, NlpQueryRequest, NlpQueryResponse, RowsQueryRequest, RowsQueryResponse, RowEmbeddingsQueryRequest, RowEmbeddingsQueryResponse, RowEmbeddingsMatch, PromptRequest, PromptResponse, // ProcessingMetadata, RequestMessage, StructuredQueryRequest, StructuredQueryResponse, TextCompletionRequest, TextCompletionResponse, TriplesQueryRequest, TriplesQueryResponse, // Chunked upload types ChunkedUploadDocumentMetadata, BeginUploadRequest, BeginUploadResponse, UploadChunkRequest, UploadChunkResponse, CompleteUploadRequest, CompleteUploadResponse, GetUploadStatusRequest, GetUploadStatusResponse, AbortUploadRequest, AbortUploadResponse, ListUploadsRequest, ListUploadsResponse, UploadSession, StreamDocumentRequest, StreamDocumentResponse, // EntityEmbeddings, // Error, // GraphEmbedding, // Metadata, // Request, // Response, } from "../models/messages.js"; // GraphRAG options interface for configurable parameters export interface GraphRagOptions { entityLimit?: number; tripleLimit?: number; maxSubgraphSize?: number; pathLength?: number; } // Metadata included in final streaming message export interface StreamingMetadata { in_token?: number; out_token?: number; model?: string; } // Explainability event data export interface ExplainEvent { explainId: string; explainGraph: string; // Named graph where explain data is stored (e.g., urn:graph:retrieval) } // Configuration constants const SOCKET_RECONNECTION_TIMEOUT = 2000; // 2 seconds between reconnection // attempts const SOCKET_URL = getDefaultSocketUrl(); // WebSocket endpoint path (isomorphic) /** * Socket interface defining all available operations for the TrustGraph API * This provides a unified interface for various AI/ML and knowledge graph * operations */ export interface Socket { close: () => void; // Text completion using AI models textCompletion: (system: string, text: string) => Promise; // Graph-based Retrieval Augmented Generation graphRag: (text: string, options?: GraphRagOptions) => Promise; // Agent interaction with streaming callbacks for different phases // BREAKING CHANGE: Callbacks now receive (chunk, complete, metadata?) instead of full messages agent: ( question: string, think: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, observe: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, answer: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, error: (e: string) => void, onExplain?: (event: ExplainEvent) => void, ) => void; // Streaming variants for RAG and completion services graphRagStreaming: ( text: string, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, options?: GraphRagOptions, collection?: string, ) => void; documentRagStreaming: ( text: string, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, docLimit?: number, collection?: string, onExplain?: (event: ExplainEvent) => void, ) => void; textCompletionStreaming: ( system: string, text: string, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, ) => void; promptStreaming: ( id: string, terms: Record, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, ) => void; // Generate embeddings for texts (batch) embeddings: (texts: string[]) => Promise; // Query graph using embedding vector graphEmbeddingsQuery: (vec: number[], limit: number) => Promise; // Query knowledge graph triples (subject-predicate-object) triplesQuery: ( s?: Term, // Subject (optional) p?: Term, // Predicate (optional) o?: Term, // Object (optional) limit?: number, collection?: string, graph?: string, // Named graph URI filter ) => Promise; // Load a document into the system loadDocument: ( document: string, // Base64-encoded document id?: string, // Optional document ID metadata?: Triple[], // Optional metadata as triples ) => Promise; // Load plain text into the system loadText: (text: string, id?: string, metadata?: Triple[]) => Promise; // Load a document into the library with full metadata loadLibraryDocument: ( document: string, mimeType: string, id?: string, metadata?: Triple[], ) => Promise; } /** * Generates a random message ID using cryptographically secure random values * @param length - Number of random characters to generate * @returns Random string of specified length */ function makeid(length: number) { const array = new Uint32Array(length); getRandomValues(array); const characters = "abcdefghijklmnopqrstuvwxyz1234567890"; return array.reduce( (acc, current) => acc + characters[current % characters.length], "", ); } /** * BaseApi - Core WebSocket client for TrustGraph API * Manages connection lifecycle, message routing, and provides base request * functionality */ // Connection state interface for UI consumption export interface ConnectionState { status: | "connecting" | "connected" | "reconnecting" | "failed" | "authenticated" | "unauthenticated"; hasApiKey: boolean; reconnectAttempt?: number; maxAttempts?: number; nextRetryIn?: number; lastError?: string; } export class BaseApi { ws?: IsomorphicWebSocket; // WebSocket connection instance tag: string; // Unique client identifier id: number; // Counter for generating unique message IDs token?: string; // Optional authentication token user: string; // User identifier for API requests socketUrl: string; // WebSocket URL inflight: { [key: string]: ServiceCall } = {}; // Track active requests by // message ID reconnectAttempts: number = 0; // Track reconnection attempts maxReconnectAttempts: number = 10; // Maximum reconnection attempts reconnectTimer?: number; // Timer for reconnection attempts reconnectionState: "idle" | "reconnecting" | "failed" = "idle"; // Connection state // Connection state tracking for UI private connectionStateListeners: ((state: ConnectionState) => void)[] = []; private lastError?: string; constructor(user: string, token?: string, socketUrl?: string) { this.tag = makeid(16); // Generate unique client tag this.id = 1; // Start message ID counter this.token = token; // Store authentication token this.user = user; // Store user identifier this.socketUrl = socketUrl || SOCKET_URL; // Use provided URL or default console.log( "SOCKET: opening socket...", token ? "with auth" : "without auth", "user:", user, ); this.openSocket(); // Establish WebSocket connection console.log("SOCKET: socket opened"); } /** * Subscribe to connection state changes for UI updates */ onConnectionStateChange(listener: (state: ConnectionState) => void) { this.connectionStateListeners.push(listener); // Immediately send current state listener(this.getConnectionState()); // Return unsubscribe function return () => { const index = this.connectionStateListeners.indexOf(listener); if (index > -1) { this.connectionStateListeners.splice(index, 1); } }; } /** * Get current connection state */ private getConnectionState(): ConnectionState { const hasApiKey = !!this.token; // Determine status based on WebSocket state and reconnection state let status: ConnectionState["status"]; if (!this.ws || this.ws.readyState === WS_CLOSED) { if (this.reconnectionState === "failed") { status = "failed"; } else if (this.reconnectionState === "reconnecting") { status = "reconnecting"; } else { status = "connecting"; } } else if (this.ws.readyState === WS_CONNECTING) { status = "connecting"; } else if (this.ws.readyState === WS_OPEN) { status = hasApiKey ? "authenticated" : "unauthenticated"; } else { status = "connecting"; } const state: ConnectionState = { status, hasApiKey, lastError: this.lastError, }; // Add reconnection details if applicable if (status === "reconnecting") { state.reconnectAttempt = this.reconnectAttempts; state.maxAttempts = this.maxReconnectAttempts; } return state; } /** * Notify all listeners of connection state changes */ private notifyStateChange() { const state = this.getConnectionState(); this.connectionStateListeners.forEach((listener) => { try { listener(state); } catch (error) { console.error("Error in connection state listener:", error); } }); } /** * Establishes WebSocket connection and sets up event handlers */ openSocket() { // Don't create multiple connections if ( this.ws && (this.ws.readyState === WS_CONNECTING || this.ws.readyState === WS_OPEN) ) { return; } // Clean up old socket if exists if (this.ws) { this.ws.removeEventListener("message", this.onMessage); this.ws.removeEventListener("close", this.onClose); this.ws.removeEventListener("open", this.onOpen); this.ws.removeEventListener("error", this.onError); this.ws = undefined; } try { // Build WebSocket URL with optional token parameter const wsUrl = this.token ? `${this.socketUrl}?token=${this.token}` : this.socketUrl; console.log( "SOCKET: connecting to", wsUrl.replace(/token=[^&]*/, "token=***"), ); const WS = getWebSocketConstructor(); this.ws = new WS(wsUrl); } catch (e) { console.error("[socket creation error]", e); this.scheduleReconnect(); return; } // Bind event handlers to maintain proper 'this' context this.onMessage = this.onMessage.bind(this); this.onClose = this.onClose.bind(this); this.onOpen = this.onOpen.bind(this); this.onError = this.onError.bind(this); // Attach event listeners this.ws.addEventListener("message", this.onMessage); this.ws.addEventListener("close", this.onClose); this.ws.addEventListener("open", this.onOpen); this.ws.addEventListener("error", this.onError); } // Handle incoming messages from server onMessage(message: WsMessageEvent) { if (!message.data) return; try { const obj = JSON.parse(String(message.data)); // Skip messages without ID (can't route them) if (!obj.id) return; // Route response to the corresponding inflight request if (this.inflight[obj.id]) { // Pass the whole message object so receiver can access 'complete' flag this.inflight[obj.id].onReceived(obj); } } catch (e) { console.error("[socket message parse error]", e); } } // Handle connection closure - automatically attempt reconnection onClose(event: WsCloseEvent) { console.log("[socket close]", event.code, event.reason); this.lastError = `Connection closed: ${event.reason || "Unknown reason"}`; this.ws = undefined; this.notifyStateChange(); this.scheduleReconnect(); } // Handle successful connection onOpen(_event: WsEvent) { console.log("[socket open]"); this.reconnectAttempts = 0; // Reset reconnection attempts on success this.reconnectionState = "idle"; // Reset connection state this.lastError = undefined; // Clear any previous errors // Clear any pending reconnect timer if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = undefined; } // Notify UI of successful connection this.notifyStateChange(); // Immediately retry any pending requests that were waiting for connection for (const mid in this.inflight) { this.inflight[mid].retryNow(); } } // Handle socket errors onError(event: WsEvent) { console.error("[socket error]", event); this.lastError = "Connection error occurred"; this.notifyStateChange(); } /** * Schedules a reconnection attempt with exponential backoff */ scheduleReconnect() { // Prevent concurrent reconnection attempts if (this.reconnectionState === "reconnecting") { console.log("[socket] Reconnection already in progress, skipping"); return; } // Don't schedule if already scheduled if (this.reconnectTimer) return; this.reconnectionState = "reconnecting"; this.reconnectAttempts++; this.notifyStateChange(); // Notify UI of reconnection attempt if (this.reconnectAttempts > this.maxReconnectAttempts) { console.error("[socket] Max reconnection attempts reached"); this.reconnectionState = "failed"; this.lastError = "Max reconnection attempts exceeded"; this.notifyStateChange(); // Notify all pending requests of the failure for (const mid in this.inflight) { this.inflight[mid].error(new Error("WebSocket connection failed")); } return; } // Calculate exponential backoff with jitter const backoffDelay = Math.min( SOCKET_RECONNECTION_TIMEOUT * Math.pow(2, this.reconnectAttempts - 1) + Math.random() * 1000, 30000, // Max 30 seconds ); console.log( `[socket] Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`, ); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = undefined; this.reopen(); }, backoffDelay) as unknown as number; } /** * Reopens the WebSocket connection (used after connection failures) */ reopen() { console.log("[socket reopen]"); // Check if we're already connected or connecting if ( this.ws && (this.ws.readyState === WS_OPEN || this.ws.readyState === WS_CONNECTING) ) { return; } this.openSocket(); } /** * Closes the WebSocket connection and cleans up */ close() { // Clear reconnection timer if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = undefined; } // Clean up WebSocket if (this.ws) { // Remove event listeners to prevent memory leaks this.ws.removeEventListener("message", this.onMessage); this.ws.removeEventListener("close", this.onClose); this.ws.removeEventListener("open", this.onOpen); this.ws.removeEventListener("error", this.onError); this.ws.close(); this.ws = undefined; } // Clear any remaining inflight requests for (const mid in this.inflight) { this.inflight[mid].error(new Error("Socket closed")); } this.inflight = {}; } /** * Generates the next unique message ID for requests * Format: {clientTag}-{incrementingNumber} */ getNextId() { const mid = this.tag + "-" + this.id.toString(); this.id++; return mid; } /** * Core method for making service requests over WebSocket * @param service - Name of the service to call * @param request - Request payload * @param timeout - Request timeout in milliseconds (default: 10000) * @param retries - Number of retry attempts (default: 3) * @param flow - Optional flow identifier * @returns Promise resolving to the service response */ makeRequest( service: string, request: RequestType, timeout?: number, retries?: number, flow?: string, ) { const mid = this.getNextId(); // Set default values if (timeout == undefined) timeout = 10000; if (retries == undefined) retries = 3; // Construct the request message const msg: RequestMessage = { id: mid, service: service, request: request, }; // Add flow identifier if provided if (flow) msg.flow = flow; // Return a Promise that will be resolved/rejected by the ServiceCall return new Promise((resolve, reject) => { const call = new ServiceCall( mid, msg, resolve as (resp: unknown) => void, reject as (err: object | string) => void, timeout, retries, this, ); call.start(); // Commented out debug logging: console.log("-->", msg); }).then((obj) => { // Commented out success logging: console.log("Success for", mid); return obj as ResponseType; }); } /** * Makes a request that can receive multiple responses (streaming) * Used for operations that return data in chunks */ makeRequestMulti( service: string, request: RequestType, receiver: (resp: unknown) => boolean, // Callback to handle each response chunk timeout?: number, retries?: number, flow?: string, ) { const mid = this.getNextId(); // Set defaults if (timeout == undefined) timeout = 10000; if (retries == undefined) retries = 3; // Construct request message const msg: RequestMessage = { id: mid, service: service, request: request, }; if (flow) msg.flow = flow; return new Promise((resolve, reject) => { const call = new ServiceCallMulti( mid, msg, resolve as (resp: unknown) => void, reject as (err: object | string) => void, timeout, retries, this as any, // eslint-disable-line @typescript-eslint/no-explicit-any receiver, ); call.start(); }).then((obj) => { return obj as ResponseType; }); } /** * Convenience method for making flow-specific requests * Defaults to "default" flow if none specified */ makeFlowRequest( service: string, request: RequestType, timeout?: number, retries?: number, flow?: string, ) { if (!flow) flow = "default"; return this.makeRequest( service, request, timeout, retries, flow, ); } // Factory methods for creating specialized API instances librarian() { return new LibrarianApi(this); } flows() { return new FlowsApi(this); } flow(id: string) { return new FlowApi(this, id); } knowledge() { return new KnowledgeApi(this); } config() { return new ConfigApi(this); } collectionManagement() { return new CollectionManagementApi(this); } } /** * LibrarianApi - Manages document storage and retrieval * Handles document lifecycle including upload, processing, and removal */ export class LibrarianApi { api: BaseApi; constructor(api: BaseApi) { this.api = api; } /** * Retrieves list of all documents in the system */ getDocuments() { return this.api .makeRequest( "librarian", { operation: "list-documents", user: this.api.user, }, 60000, // 60 second timeout for potentially large lists ) .then((r) => r["document-metadatas"] || []); } /** * Retrieves list of documents currently being processed */ getProcessing() { return this.api .makeRequest( "librarian", { operation: "list-processing", user: this.api.user, }, 60000, ) .then((r) => r["processing-metadata"] || []); } /** * Retrieves metadata for a single document by ID * @param documentId - Document URI/ID to fetch * @returns Document metadata including title, comments, tags, and RDF metadata */ getDocumentMetadata(documentId: string): Promise { return this.api .makeRequest( "librarian", { operation: "get-document-metadata", "document-id": documentId, user: this.api.user, }, 30000, ) .then((r) => r["document-metadata"] || null); } /** * Uploads a document to the library with full metadata * @param document - Base64-encoded document content * @param id - Optional document identifier * @param metadata - Optional metadata as triples * @param mimeType - Document MIME type * @param title - Document title * @param comments - Additional comments * @param tags - Document tags for categorization */ loadDocument( document: string, // base64-encoded doc mimeType: string, title: string, comments: string, tags: string[], id?: string, metadata?: Triple[], ) { return this.api.makeRequest( "librarian", { operation: "add-document", "document-metadata": { id: id, time: Math.floor(Date.now() / 1000), // Unix timestamp kind: mimeType, title: title, comments: comments, metadata: metadata, user: this.api.user, tags: tags, }, content: document, }, 30000, // 30 second timeout for document upload ); } /** * Removes a document from the library */ removeDocument(id: string, collection?: string) { return this.api.makeRequest( "librarian", { operation: "remove-document", "document-id": id, user: this.api.user, collection: collection || "default", }, 30000, ); } /** * Adds a document to the processing queue * @param id - Processing job identifier * @param doc_id - Document to process * @param flow - Processing flow to use * @param collection - Collection to add processed data to * @param tags - Tags for the processing job */ addProcessing( id: string, doc_id: string, flow: string, collection?: string, tags?: string[], ) { return this.api.makeRequest( "librarian", { operation: "add-processing", "processing-metadata": { id: id, "document-id": doc_id, time: Math.floor(Date.now() / 1000), flow: flow, user: this.api.user, collection: collection ? collection : "default", tags: tags ? tags : [], }, }, 30000, ); } // ========== Chunked Upload API ========== /** * Initialize a chunked upload session for large documents (>2MB) * @param metadata - Document metadata including id, title, kind (MIME type), etc. * @param totalSize - Total size of the document in bytes * @param chunkSize - Optional chunk size (default: 5MB) * @returns Upload session info including upload-id and total-chunks */ beginUpload( metadata: ChunkedUploadDocumentMetadata, totalSize: number, chunkSize?: number, ): Promise { return this.api .makeRequest( "librarian", { operation: "begin-upload", "document-metadata": metadata, "total-size": totalSize, "chunk-size": chunkSize, }, 30000, ) .then((r) => { if (r.error) { throw new Error(r.error.message); } return r; }); } /** * Upload a single chunk of a document * Chunks can be uploaded in any order and in parallel * @param uploadId - Upload session ID from beginUpload * @param chunkIndex - Zero-based chunk index * @param content - Base64-encoded chunk content * @returns Progress info including chunks-received and bytes-received */ uploadChunk( uploadId: string, chunkIndex: number, content: string, ): Promise { return this.api .makeRequest( "librarian", { operation: "upload-chunk", "upload-id": uploadId, "chunk-index": chunkIndex, content: content, user: this.api.user, }, 60000, // Longer timeout for chunk uploads ) .then((r) => { if (r.error) { throw new Error(r.error.message); } return r; }); } /** * Finalize a chunked upload after all chunks are received * Triggers document processing * @param uploadId - Upload session ID from beginUpload * @returns Document ID and object ID */ completeUpload(uploadId: string): Promise { return this.api .makeRequest( "librarian", { operation: "complete-upload", "upload-id": uploadId, user: this.api.user, }, 30000, ) .then((r) => { if (r.error) { throw new Error(r.error.message); } return r; }); } /** * Check upload progress (useful for resuming interrupted uploads) * @param uploadId - Upload session ID * @returns Status including received/missing chunks */ getUploadStatus(uploadId: string): Promise { return this.api .makeRequest( "librarian", { operation: "get-upload-status", "upload-id": uploadId, user: this.api.user, }, 30000, ) .then((r) => { if (r.error) { throw new Error(r.error.message); } return r; }); } /** * Cancel an in-progress upload and clean up * @param uploadId - Upload session ID to abort */ abortUpload(uploadId: string): Promise { return this.api .makeRequest( "librarian", { operation: "abort-upload", "upload-id": uploadId, user: this.api.user, }, 30000, ) .then((r) => { if (r.error) { throw new Error(r.error.message); } }); } /** * List pending upload sessions for the current user * @returns Array of upload sessions with metadata and progress */ listUploads(): Promise { return this.api .makeRequest( "librarian", { operation: "list-uploads", user: this.api.user, }, 30000, ) .then((r) => { if (r.error) { throw new Error(r.error.message); } return r["upload-sessions"] || []; }); } /** * Stream a document in chunks for retrieval (streaming response) * Sends one request, receives multiple chunk responses via callback * @param documentId - Document ID to retrieve * @param onChunk - Callback for each chunk: (content, chunkIndex, totalChunks, complete) => void * @param onError - Callback for errors * @param chunkSize - Optional chunk size (default: 1MB) */ streamDocument( documentId: string, onChunk: (content: string, chunkIndex: number, totalChunks: number, complete: boolean) => void, onError: (error: string) => void, chunkSize?: number, ): void { const receiver = (message: unknown): boolean => { const msg = message as { response?: StreamDocumentResponse; complete?: boolean; error?: string }; // Check for top-level error if (msg.error) { onError(msg.error); return true; } const resp = msg.response; if (!resp) { return !!msg.complete; } // Check for response-level error if (resp.error) { onError(resp.error.message); return true; } const complete = !!msg.complete; onChunk(resp.content, resp["chunk-index"], resp["total-chunks"], complete); return complete; }; this.api.makeRequestMulti( "librarian", { operation: "stream-document", "document-id": documentId, "chunk-size": chunkSize, user: this.api.user, }, receiver, 300000, // 5 minute timeout for full document stream ); } } /** * FlowsApi - Manages processing flows and configuration * Flows define how documents and data are processed through the system */ export class FlowsApi { api: BaseApi; constructor(api: BaseApi) { this.api = api; } /** * Retrieves list of available flows */ getFlows() { return this.api .makeRequest( "flow", { operation: "list-flows", }, 60000, ) .then((r) => r["flow-ids"] || []); } /** * Retrieves definition of a specific flow */ getFlow(id: string) { return this.api .makeRequest( "flow", { operation: "get-flow", "flow-id": id, }, 60000, ) .then((r) => JSON.parse(r.flow || "{}")); // Parse JSON flow definition } // Configuration management methods /** * Retrieves all configuration settings */ getConfigAll() { return this.api.makeRequest( "config", { operation: "config", }, 60000, ); } /** * Retrieves specific configuration values by key */ getConfig(keys: { type: string; key: string }[]) { return this.api.makeRequest( "config", { operation: "get", keys: keys, }, 60000, ); } /** * Updates configuration values */ putConfig(values: { type: string; key: string; value: string }[]) { return this.api.makeRequest( "config", { operation: "put", values: values, }, 60000, ); } /** * Deletes configuration entries */ deleteConfig(keys: { type: string; key: string }) { return this.api.makeRequest( "config", { operation: "delete", keys: keys, }, 30000, ); } // Prompt management - specialized config operations for AI prompts /** * Retrieves list of available prompt templates */ getPrompts() { return this.getConfigAll().then((r) => { const config = r as Record< string, Record> >; const raw = config.config?.prompt?.["template-index"]; return raw ? JSON.parse(raw) : []; }); } /** * Retrieves a specific prompt template */ getPrompt(id: string) { return this.getConfigAll().then((r) => { const config = r as Record< string, Record> >; const raw = config.config?.prompt?.[`template.${id}`]; return raw ? JSON.parse(raw) : null; }); } /** * Retrieves the system prompt configuration */ getSystemPrompt() { return this.getConfigAll().then((r) => { const config = r as Record< string, Record> >; const raw = config.config?.prompt?.system; return raw ? JSON.parse(raw) : ""; }); } // Flow blueprint management - templates for creating flows /** * Retrieves list of available flow blueprints (templates) */ getFlowBlueprints() { return this.api .makeRequest( "flow", { operation: "list-blueprints", }, 60000, ) .then((r) => r["blueprint-names"]); } /** * Retrieves definition of a specific flow blueprint */ getFlowBlueprint(name: string) { return this.api .makeRequest( "flow", { operation: "get-blueprint", "blueprint-name": name, }, 60000, ) .then((r) => JSON.parse(r["blueprint-definition"] || "{}")); } /** * Deletes a flow blueprint */ deleteFlowBlueprint(name: string) { return this.api.makeRequest( "flow", { operation: "delete-blueprint", "blueprint-name": name, }, 30000, ); } // Flow lifecycle management /** * Starts a new flow instance */ startFlow( id: string, blueprint_name: string, description: string, parameters?: Record, ) { const request: FlowRequest = { operation: "start-flow", "flow-id": id, "blueprint-name": blueprint_name, description: description, }; // Only include parameters if provided and not empty if (parameters && Object.keys(parameters).length > 0) { request.parameters = parameters; } return this.api .makeRequest("flow", request, 30000) .then((response) => { if (response.error) { let errorMessage = "Flow start failed"; if ( typeof response.error === "object" && response.error && "message" in response.error ) { errorMessage = (response.error as { message?: string }).message || errorMessage; } else if (typeof response.error === "string") { errorMessage = response.error; } throw new Error(errorMessage); } return response; }); } /** * Stops a running flow instance */ stopFlow(id: string) { return this.api.makeRequest( "flow", { operation: "stop-flow", "flow-id": id, }, 30000, ); } } /** * FlowApi - Interface for interacting with a specific flow instance * Provides flow-specific versions of core AI/ML operations */ export class FlowApi { api: BaseApi; flowId: string; constructor(api: BaseApi, flowId: string) { this.api = api; this.flowId = flowId; // All requests will be routed through this flow } /** * Performs text completion using AI models within this flow */ textCompletion(system: string, text: string): Promise { return this.api .makeRequest( "text-completion", { system: system, // System prompt/instructions prompt: text, // User prompt }, 30000, undefined, // Use default retries this.flowId, // Route through this flow ) .then((r) => r.response); } /** * Performs Graph RAG (Retrieval Augmented Generation) query */ graphRag(text: string, options?: GraphRagOptions, collection?: string) { return this.api .makeRequest( "graph-rag", { query: text, user: this.api.user, collection: collection || "default", "entity-limit": options?.entityLimit, "triple-limit": options?.tripleLimit, "max-subgraph-size": options?.maxSubgraphSize, "max-path-length": options?.pathLength, }, 60000, // Longer timeout for complex graph operations undefined, this.flowId, ) .then((r) => r.response); } /** * Performs Document RAG (Retrieval Augmented Generation) query */ documentRag(text: string, docLimit?: number, collection?: string) { return this.api .makeRequest( "document-rag", { query: text, user: this.api.user, collection: collection || "default", "doc-limit": docLimit || 20, }, 60000, // Longer timeout for document operations undefined, this.flowId, ) .then((r) => r.response); } /** * Interacts with an AI agent that provides streaming responses * BREAKING CHANGE: Callbacks now receive (chunk, complete, metadata?) instead of full messages */ agent( question: string, think: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, observe: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, answer: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, error: (s: string) => void, onExplain?: (event: ExplainEvent) => void, ) { const receiver = (message: unknown) => { const msg = message as { response?: AgentResponse; complete?: boolean; error?: string }; // Check for top-level error if (msg.error) { error(msg.error); return true; } const resp = msg.response || {}; // Check for errors in response if (resp.chunk_type === "error" || resp.error) { error(resp.error?.message || "Unknown agent error"); return true; // End streaming on error } // Handle explainability events (agent uses chunk_type="explain") if ((resp.chunk_type === "explain" || resp.message_type === "explain") && resp.explain_id && resp.explain_graph) { onExplain?.({ explainId: resp.explain_id, explainGraph: resp.explain_graph, }); return false; } // Handle streaming chunks by chunk_type const content = resp.content || ""; const messageComplete = !!resp.end_of_message; const dialogComplete = !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = dialogComplete && (resp.in_token || resp.out_token || resp.model) ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model } : undefined; switch (resp.chunk_type) { case "thought": think(content, messageComplete, metadata); break; case "observation": observe(content, messageComplete, metadata); break; case "answer": case "final-answer": answer(content, messageComplete, metadata); break; case "action": // Actions are typically not streamed incrementally, just logged console.log("Agent action:", content); break; } return dialogComplete; // End when backend signals complete }; return this.api .makeRequestMulti( "agent", { question: question, user: this.api.user, streaming: true, // Always use streaming mode }, receiver, 120000, 2, this.flowId, ) .catch((err) => { const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error"; error(`Agent request failed: ${errorMessage}`); }); } /** * Performs Graph RAG query with streaming response * @param text - Query text * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk * @param onError - Called on error * @param options - Graph RAG options (including explainable flag) * @param collection - Collection name * @param onExplain - Optional callback for explainability events */ graphRagStreaming( text: string, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, options?: GraphRagOptions, collection?: string, onExplain?: (event: ExplainEvent) => void, ): void { const recv = (message: unknown): boolean => { const msg = message as { response?: GraphRagResponse; complete?: boolean; error?: string }; // Check for top-level error if (msg.error) { onError(msg.error); return true; } const resp = (msg.response || {}) as GraphRagResponse; // Check for response-level error if (resp.error) { onError(resp.error.message); return true; } // Handle explainability events if (resp.message_type === "explain" && resp.explain_id && resp.explain_graph) { onExplain?.({ explainId: resp.explain_id, explainGraph: resp.explain_graph, }); // Don't return true - more messages may follow return false; } // Handle chunk messages (default behavior) const chunk = resp.response || resp.chunk || ""; const complete = !!resp.end_of_session || !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model) ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model } : undefined; receiver(chunk, complete, metadata); return complete; }; this.api.makeRequestMulti( "graph-rag", { query: text, user: this.api.user, collection: collection || "default", "entity-limit": options?.entityLimit, "triple-limit": options?.tripleLimit, "max-subgraph-size": options?.maxSubgraphSize, "max-path-length": options?.pathLength, streaming: true, }, recv, 60000, undefined, this.flowId, ).catch((err) => { const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error"; onError(`Graph RAG request failed: ${errorMessage}`); }); } /** * Performs Document RAG query with streaming response * @param text - Query text * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk * @param onError - Called on error * @param docLimit - Maximum documents to retrieve * @param collection - Collection name */ documentRagStreaming( text: string, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, docLimit?: number, collection?: string, onExplain?: (event: ExplainEvent) => void, ): void { const recv = (message: unknown): boolean => { const msg = message as { response?: DocumentRagResponse; complete?: boolean; error?: string }; // Check for top-level error if (msg.error) { onError(msg.error); return true; } const resp = (msg.response || {}) as DocumentRagResponse; // Check for response-level error if (resp.error) { onError(resp.error.message); return true; } // Handle explainability events if (resp.message_type === "explain" && resp.explain_id && resp.explain_graph) { onExplain?.({ explainId: resp.explain_id, explainGraph: resp.explain_graph, }); return false; } const chunk = resp.response || resp.chunk || ""; const complete = !!resp.end_of_session || !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model) ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model } : undefined; receiver(chunk, complete, metadata); return complete; }; this.api.makeRequestMulti( "document-rag", { query: text, user: this.api.user, collection: collection || "default", "doc-limit": docLimit, streaming: true, }, recv, 60000, undefined, this.flowId, ).catch((err) => { const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error"; onError(`Document RAG request failed: ${errorMessage}`); }); } /** * Performs text completion with streaming response * @param system - System prompt * @param text - User prompt * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk * @param onError - Called on error */ textCompletionStreaming( system: string, text: string, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, ): void { const recv = (message: unknown): boolean => { const msg = message as { response?: TextCompletionResponse; complete?: boolean; error?: string }; // Check for top-level error if (msg.error) { onError(msg.error); return true; } const resp = (msg.response || {}) as TextCompletionResponse; // Check for response-level error if (resp.error) { onError(resp.error.message); return true; } // Text completion uses 'response' field for chunks const chunk = resp.response || ""; const complete = !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model) ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model } : undefined; receiver(chunk, complete, metadata); return complete; }; this.api.makeRequestMulti( "text-completion", { system: system, prompt: text, streaming: true, }, recv, 30000, undefined, this.flowId, ); } /** * Executes a prompt template with streaming response * @param id - Prompt template ID * @param terms - Template variables * @param receiver - Called for each chunk with (chunk, complete) where complete=true on final chunk * @param onError - Called on error */ promptStreaming( id: string, terms: Record, receiver: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, onError: (error: string) => void, ): void { const recv = (message: unknown): boolean => { const msg = message as { response?: PromptResponse; complete?: boolean; error?: string }; // Check for top-level error if (msg.error) { onError(msg.error); return true; } const resp = (msg.response || {}) as PromptResponse; // Check for response-level error if (resp.error) { onError(resp.error.message); return true; } // Prompt service uses 'text' field for chunks const chunk = resp.text || ""; const complete = !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model) ? { in_token: resp.in_token, out_token: resp.out_token, model: resp.model } : undefined; receiver(chunk, complete, metadata); return complete; }; this.api.makeRequestMulti( "prompt", { id: id, terms: terms, streaming: true, }, recv, 30000, undefined, this.flowId, ); } /** * Generates embeddings for multiple texts within this flow. * Returns vectors[text_index][dimension_index] - one vector per input text. */ embeddings(texts: string[]) { return this.api .makeRequest( "embeddings", { texts: texts, }, 30000, undefined, this.flowId, ) .then((r) => r.vectors); } /** * Queries the knowledge graph using a single embedding vector */ graphEmbeddingsQuery( vec: number[], limit: number | undefined, collection?: string, ) { return this.api .makeRequest( "graph-embeddings", { vector: vec, limit: limit ? limit : 20, // Default to 20 results user: this.api.user, collection: collection || "default", }, 30000, undefined, this.flowId, ) .then((r) => r.entities); } /** * Queries knowledge graph triples (subject-predicate-object relationships) * All parameters are optional - omitted parameters act as wildcards */ triplesQuery( s?: Term, p?: Term, o?: Term, limit?: number, collection?: string, graph?: string, ) { return this.api .makeRequest( "triples", { s: s, // Subject p: p, // Predicate o: o, // Object g: graph, // Named graph URI filter limit: limit ? limit : 20, user: this.api.user, collection: collection || "default", }, 30000, undefined, this.flowId, ) .then((r) => r.response); } /** * Loads a document into this flow for processing */ loadDocument( document: string, // base64-encoded document id?: string, metadata?: Triple[], ) { return this.api.makeRequest( "document-load", { id: id, metadata: metadata, data: document, }, 30000, undefined, this.flowId, ); } /** * Loads plain text into this flow for processing */ loadText( text: string, // Text content id?: string, metadata?: Triple[], charset?: string, // Character encoding ) { return this.api.makeRequest( "text-load", { id: id, metadata: metadata, text: text, charset: charset, }, 30000, undefined, this.flowId, ); } /** * Executes a GraphQL query against structured row data */ rowsQuery( query: string, collection?: string, variables?: Record, operationName?: string, ) { return this.api .makeRequest( "rows", { query: query, user: this.api.user, collection: collection || "default", variables: variables, operation_name: operationName, }, 30000, undefined, this.flowId, ) .then((r) => { // Return the GraphQL response structure directly const result: Record = {}; if (r.data !== undefined) result.data = r.data; if (r.errors) result.errors = r.errors; if (r.extensions) result.extensions = r.extensions; return result; }); } /** * Converts a natural language question to a GraphQL query */ nlpQuery(question: string, maxResults?: number) { return this.api .makeRequest( "nlp-query", { question: question, max_results: maxResults || 100, }, 30000, undefined, this.flowId, ) .then((r) => r); } /** * Executes a natural language question against structured data * Combines NLP query conversion and GraphQL execution */ structuredQuery(question: string, collection?: string) { return this.api .makeRequest( "structured-query", { question: question, user: this.api.user, collection: collection || "default", }, 30000, undefined, this.flowId, ) .then((r) => { // Return the response structure directly const result: Record = {}; if (r.data !== undefined) result.data = r.data; if (r.errors) result.errors = r.errors; return result; }); } /** * Performs semantic search on structured data indexes using embedding vectors * @param vectors - Embedding vectors to search for * @param schemaName - Name of the schema to search * @param collection - Optional collection name * @param indexName - Optional index name to filter results * @param limit - Maximum number of results to return (default: 10) */ rowEmbeddingsQuery( vector: number[], schemaName: string, collection?: string, indexName?: string, limit?: number, ): Promise { const request: RowEmbeddingsQueryRequest = { vector: vector, schema_name: schemaName, user: this.api.user, collection: collection || "default", limit: limit || 10, }; if (indexName) { request.index_name = indexName; } return this.api .makeRequest( "row-embeddings", request, 30000, undefined, this.flowId, ) .then((r) => { if (r.error) { throw new Error(r.error.message); } return r.matches || []; }); } } /** * ConfigApi - Dedicated configuration management interface * Handles system configuration, prompts, and token cost tracking */ export class ConfigApi { api: BaseApi; constructor(api: BaseApi) { this.api = api; } /** * Retrieves complete configuration */ getConfigAll() { return this.api.makeRequest( "config", { operation: "config", }, 60000, ); } /** * Retrieves specific configuration entries */ getConfig(keys: { type: string; key: string }[]) { return this.api.makeRequest( "config", { operation: "get", keys: keys, }, 60000, ); } /** * Updates configuration values */ putConfig(values: { type: string; key: string; value: string }[]) { return this.api.makeRequest( "config", { operation: "put", values: values, }, 60000, ); } /** * Deletes configuration entries */ deleteConfig(keys: { type: string; key: string }) { return this.api.makeRequest( "config", { operation: "delete", keys: keys, }, 30000, ); } // Specialized prompt management methods /** * Retrieves available prompt templates */ getPrompts() { return this.getConfigAll().then((r) => { const config = r as Record< string, Record> >; const raw = config.config?.prompt?.["template-index"]; return raw ? JSON.parse(raw) : []; }); } /** * Retrieves a specific prompt template */ getPrompt(id: string) { return this.getConfigAll().then((r) => { const config = r as Record< string, Record> >; const raw = config.config?.prompt?.[`template.${id}`]; return raw ? JSON.parse(raw) : null; }); } /** * Retrieves system prompt configuration */ getSystemPrompt() { return this.getConfigAll().then((r) => { const config = r as Record< string, Record> >; const raw = config.config?.prompt?.system; return raw ? JSON.parse(raw) : ""; }); } /** * Lists available configuration types */ list(type: string) { return this.api .makeRequest( "config", { operation: "list", type: type, }, 60000, ) .then((r) => r); } /** * Retrieves all key/values for a specific type */ getValues(type: string) { return this.api .makeRequest( "config", { operation: "getvalues", type: type, }, 60000, ) .then((r) => (r as RowsQueryResponse).values); } /** * Retrieves token cost information for different AI models * Useful for cost tracking and optimization */ getTokenCosts() { return this.api .makeRequest( "config", { operation: "getvalues", type: "token-cost", }, 60000, ) .then((r) => { // Parse JSON values and restructure data const response = r as RowsQueryResponse; return (response.values || []).map((x: unknown) => { const item = x as Record; return { key: item.key, value: JSON.parse(item.value) }; }); }) .then((r) => // Transform to more usable format r.map((x: unknown) => { const item = x as Record; const value = item.value as Record; return { model: item.key, input_price: value.input_price, // Cost per input token output_price: value.output_price, // Cost per output token }; }), ); } } /** * KnowledgeApi - Manages knowledge graph cores and data * Knowledge cores appear to be collections of processed knowledge graph data */ export class KnowledgeApi { api: BaseApi; constructor(api: BaseApi) { this.api = api; } /** * Retrieves list of available knowledge graph cores */ getKnowledgeCores() { return this.api .makeRequest( "knowledge", { operation: "list-kg-cores", user: this.api.user, }, 60000, ) .then((r) => r.ids || []); } /** * Deletes a knowledge graph core */ deleteKgCore(id: string, collection?: string) { return this.api.makeRequest( "knowledge", { operation: "delete-kg-core", id: id, user: this.api.user, collection: collection || "default", }, 30000, ); } /** * Deletes a knowledge graph core */ loadKgCore(id: string, flow: string, collection?: string) { return this.api.makeRequest( "knowledge", { operation: "load-kg-core", id: id, flow: flow, user: this.api.user, collection: collection || "default", }, 30000, ); } /** * Retrieves a knowledge graph core with streaming data * Uses multi-request pattern for large datasets * @param receiver - Callback function to handle streaming data chunks */ getKgCore( id: string, collection: string | undefined, receiver: (msg: unknown, eos: boolean) => void, ) { // Wrapper to handle end-of-stream detection const recv = (msg: unknown) => { const response = msg as Record; if (response.eos) { // End of stream - notify receiver and signal completion receiver(msg, true); return true; } else { // Regular message - continue streaming receiver(msg, false); return false; } }; return this.api.makeRequestMulti( "knowledge", { operation: "get-kg-core", id: id, user: this.api.user, collection: collection || "default", }, recv, // Stream handler 30000, ); } } /** * CollectionManagementApi - Manages collections for organizing documents * Provides operations for listing, creating, updating, and deleting collections */ export class CollectionManagementApi { api: BaseApi; constructor(api: BaseApi) { this.api = api; } /** * Lists all collections for the current user with optional tag filtering * @param tagFilter - Optional array of tags to filter collections * @returns Promise resolving to array of collection metadata */ listCollections(tagFilter?: string[]) { const request: Record = { operation: "list-collections", user: this.api.user, }; if (tagFilter && tagFilter.length > 0) { request.tag_filter = tagFilter; } return this.api .makeRequest< Record, Record >("collection-management", request, 30000) .then((r) => r.collections || []); } /** * Creates or updates a collection for the current user * @param collection - Collection ID (unique identifier) * @param name - Display name for the collection * @param description - Description of the collection * @param tags - Array of tags for categorization * @returns Promise resolving to updated collection metadata */ updateCollection( collection: string, name?: string, description?: string, tags?: string[], ) { const request: Record = { operation: "update-collection", user: this.api.user, collection, }; if (name !== undefined) { request.name = name; } if (description !== undefined) { request.description = description; } if (tags !== undefined) { request.tags = tags; } return this.api .makeRequest< Record, Record >("collection-management", request, 30000) .then((r) => { if ( r.collections && Array.isArray(r.collections) && r.collections.length > 0 ) { return r.collections[0]; } throw new Error("Failed to update collection"); }); } /** * Deletes a collection and all its data for the current user * @param collection - Collection ID to delete * @returns Promise resolving when deletion is complete */ deleteCollection(collection: string) { return this.api.makeRequest< Record, Record >( "collection-management", { operation: "delete-collection", user: this.api.user, collection, }, 30000, ); } } /** * Factory function to create a new TrustGraph WebSocket connection * This is the main entry point for using the TrustGraph API * @param user - User identifier for API requests * @param token - Optional authentication token for secure connections * @param socketUrl - Optional WebSocket URL (defaults to /api/socket for browser, provide full URL for Node.js) */ export const createTrustGraphSocket = ( user: string, token?: string, socketUrl?: string, ): BaseApi => { return new BaseApi(user, token, socketUrl); };