diff options
| author | nexxeln <[email protected]> | 2025-11-19 18:57:55 +0000 |
|---|---|---|
| committer | nexxeln <[email protected]> | 2025-11-19 18:57:56 +0000 |
| commit | 5e24eb66c3ca7d2224d0d1f7837cda17015f5fcb (patch) | |
| tree | 60336fd37b41e3597065729d098877483eba73b6 /packages/memory-graph/src/lib | |
| parent | Fix: Prevent multiple prompts while AI response is generated (fixes #538) (#583) (diff) | |
| download | supermemory-5e24eb66c3ca7d2224d0d1f7837cda17015f5fcb.tar.xz supermemory-5e24eb66c3ca7d2224d0d1f7837cda17015f5fcb.zip | |
package the graph (#563)shoubhit/eng-358-packaging-graph-component
includes:
- a package that contains a MemoryGraph component which handles fetching data and rendering the graph
- a playground to test the package
problems:
- the bundle size is huge
- the styles are kinda broken? we are using [https://www.npmjs.com/package/vite-plugin-libgi-inject-css](https://www.npmjs.com/package/vite-plugin-lib-inject-css) to inject the styles

Diffstat (limited to 'packages/memory-graph/src/lib')
| -rw-r--r-- | packages/memory-graph/src/lib/api-client.ts | 213 | ||||
| -rw-r--r-- | packages/memory-graph/src/lib/similarity.ts | 115 |
2 files changed, 328 insertions, 0 deletions
diff --git a/packages/memory-graph/src/lib/api-client.ts b/packages/memory-graph/src/lib/api-client.ts new file mode 100644 index 00000000..faef4d06 --- /dev/null +++ b/packages/memory-graph/src/lib/api-client.ts @@ -0,0 +1,213 @@ +import type { DocumentsResponse } from "@/api-types"; + +export interface FetchDocumentsOptions { + apiKey: string; + baseUrl?: string; + page?: number; + limit?: number; + sort?: "createdAt" | "updatedAt"; + order?: "asc" | "desc"; + containerTags?: string[]; + signal?: AbortSignal; +} + +export interface ApiClientError extends Error { + status?: number; + statusText?: string; + response?: unknown; +} + +/** + * Creates an API client error with additional context + */ +function createApiError( + message: string, + status?: number, + statusText?: string, + response?: unknown, +): ApiClientError { + const error = new Error(message) as ApiClientError; + error.name = "ApiClientError"; + error.status = status; + error.statusText = statusText; + error.response = response; + return error; +} + +/** + * Fetches documents with their memory entries from the Supermemory API + * + * @param options - Configuration options for the API request + * @returns Promise resolving to the documents response + * @throws ApiClientError if the request fails + */ +export async function fetchDocuments( + options: FetchDocumentsOptions, +): Promise<DocumentsResponse> { + const { + apiKey, + baseUrl = "https://api.supermemory.ai", + page = 1, + limit = 50, + sort = "createdAt", + order = "desc", + containerTags, + signal, + } = options; + + // Validate required parameters + if (!apiKey) { + throw createApiError("API key is required"); + } + + // Construct the full URL + const url = `${baseUrl}/v3/documents/documents`; + + // Build request body + const body: { + page: number; + limit: number; + sort: string; + order: string; + containerTags?: string[]; + } = { + page, + limit, + sort, + order, + }; + + if (containerTags && containerTags.length > 0) { + body.containerTags = containerTags; + } + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + signal, + }); + + // Handle non-OK responses + if (!response.ok) { + let errorMessage = `Failed to fetch documents: ${response.status} ${response.statusText}`; + let errorResponse: unknown; + + try { + errorResponse = await response.json(); + if ( + errorResponse && + typeof errorResponse === "object" && + "message" in errorResponse + ) { + errorMessage = `API Error: ${(errorResponse as { message: string }).message}`; + } + } catch { + // If response is not JSON, use default error message + } + + throw createApiError( + errorMessage, + response.status, + response.statusText, + errorResponse, + ); + } + + // Parse and validate response + const data = await response.json(); + + // Basic validation of response structure + if (!data || typeof data !== "object") { + throw createApiError("Invalid response format: expected an object"); + } + + if (!("documents" in data) || !Array.isArray(data.documents)) { + throw createApiError( + "Invalid response format: missing documents array", + ); + } + + if (!("pagination" in data) || typeof data.pagination !== "object") { + throw createApiError( + "Invalid response format: missing pagination object", + ); + } + + return data as DocumentsResponse; + } catch (error) { + // Re-throw ApiClientError as-is + if ((error as ApiClientError).name === "ApiClientError") { + throw error; + } + + // Handle network errors + if (error instanceof TypeError && error.message.includes("fetch")) { + throw createApiError( + `Network error: Unable to connect to ${baseUrl}. Please check your internet connection.`, + ); + } + + // Handle abort errors + if (error instanceof Error && error.name === "AbortError") { + throw createApiError("Request was aborted"); + } + + // Handle other errors + throw createApiError( + `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } +} + +/** + * Fetches a single page of documents (convenience wrapper) + */ +export async function fetchDocumentsPage( + apiKey: string, + page: number, + baseUrl?: string, + signal?: AbortSignal, +): Promise<DocumentsResponse> { + return fetchDocuments({ + apiKey, + baseUrl, + page, + limit: 50, + signal, + }); +} + +/** + * Validates an API key by making a test request + * + * @param apiKey - The API key to validate + * @param baseUrl - Optional base URL for the API + * @returns Promise resolving to true if valid, false otherwise + */ +export async function validateApiKey( + apiKey: string, + baseUrl?: string, +): Promise<boolean> { + try { + await fetchDocuments({ + apiKey, + baseUrl, + page: 1, + limit: 1, + }); + return true; + } catch (error) { + // Check if it's an authentication error + if ((error as ApiClientError).status === 401) { + return false; + } + // Other errors might indicate valid key but other issues + // We'll return true in those cases to not block the user + return true; + } +} diff --git a/packages/memory-graph/src/lib/similarity.ts b/packages/memory-graph/src/lib/similarity.ts new file mode 100644 index 00000000..09d3a2cc --- /dev/null +++ b/packages/memory-graph/src/lib/similarity.ts @@ -0,0 +1,115 @@ +// Utility functions for calculating semantic similarity between documents and memories + +/** + * Calculate cosine similarity between two normalized vectors (unit vectors) + * Since all embeddings in this system are normalized using normalizeEmbeddingFast, + * cosine similarity equals dot product for unit vectors. + */ +export const cosineSimilarity = ( + vectorA: number[], + vectorB: number[], +): number => { + if (vectorA.length !== vectorB.length) { + throw new Error("Vectors must have the same length") + } + + let dotProduct = 0 + + for (let i = 0; i < vectorA.length; i++) { + const vectorAi = vectorA[i] + const vectorBi = vectorB[i] + if ( + typeof vectorAi !== "number" || + typeof vectorBi !== "number" || + isNaN(vectorAi) || + isNaN(vectorBi) + ) { + throw new Error("Vectors must contain only numbers") + } + dotProduct += vectorAi * vectorBi + } + + return dotProduct +} + +/** + * Calculate semantic similarity between two documents + * Returns a value between 0 and 1, where 1 is most similar + */ +export const calculateSemanticSimilarity = ( + document1Embedding: number[] | null, + document2Embedding: number[] | null, +): number => { + // If we have both embeddings, use cosine similarity + if ( + document1Embedding && + document2Embedding && + document1Embedding.length > 0 && + document2Embedding.length > 0 + ) { + const similarity = cosineSimilarity(document1Embedding, document2Embedding) + // Convert from [-1, 1] to [0, 1] range + return similarity >= 0 ? similarity : 0 + } + + return 0 +} + +/** + * Calculate semantic similarity between a document and memory entry + * Returns a value between 0 and 1, where 1 is most similar + */ +export const calculateDocumentMemorySimilarity = ( + documentEmbedding: number[] | null, + memoryEmbedding: number[] | null, + relevanceScore?: number | null, +): number => { + // If we have both embeddings, use cosine similarity + if ( + documentEmbedding && + memoryEmbedding && + documentEmbedding.length > 0 && + memoryEmbedding.length > 0 + ) { + const similarity = cosineSimilarity(documentEmbedding, memoryEmbedding) + // Convert from [-1, 1] to [0, 1] range + return similarity >= 0 ? similarity : 0 + } + + // Fall back to relevance score from database (0-100 scale) + if (relevanceScore !== null && relevanceScore !== undefined) { + return Math.max(0, Math.min(1, relevanceScore / 100)) + } + + // Default similarity for connections without embeddings or relevance scores + return 0.5 +} + +/** + * Get visual properties for connection based on similarity + */ +export const getConnectionVisualProps = (similarity: number) => { + // Ensure similarity is between 0 and 1 + const normalizedSimilarity = Math.max(0, Math.min(1, similarity)) + + return { + opacity: Math.max(0, normalizedSimilarity), // 0 to 1 range + thickness: Math.max(1, normalizedSimilarity * 4), // 1 to 4 pixels + glow: normalizedSimilarity * 0.6, // Glow intensity + pulseDuration: 2000 + (1 - normalizedSimilarity) * 3000, // Faster pulse for higher similarity + } +} + +/** + * Generate magical color based on similarity and connection type + */ +export const getMagicalConnectionColor = ( + similarity: number, + hue = 220, +): string => { + const normalizedSimilarity = Math.max(0, Math.min(1, similarity)) + const saturation = 60 + normalizedSimilarity * 40 // 60% to 100% + const lightness = 40 + normalizedSimilarity * 30 // 40% to 70% + + return `hsl(${hue}, ${saturation}%, ${lightness}%)` +} |