Initial commit of Brainy vector database v0.1.0

This commit is contained in:
David Snelling 2025-05-23 10:55:20 -07:00
commit 49480e694c
29 changed files with 10871 additions and 0 deletions

549
src/brainyData.ts Normal file
View file

@ -0,0 +1,549 @@
/**
* BrainyData
* Main class that provides the vector database functionality
*/
import { v4 as uuidv4 } from 'uuid'
import { HNSWIndex } from './hnsw/hnswIndex.js'
import { createStorage } from './storage/opfsStorage.js'
import { DistanceFunction, Edge, EmbeddingFunction, HNSWConfig, SearchResult, StorageAdapter, Vector, VectorDocument } from './coreTypes.js'
import { cosineDistance, defaultEmbeddingFunction, euclideanDistance } from './utils/index.js'
export interface BrainyDataConfig {
/**
* HNSW index configuration
*/
hnsw?: Partial<HNSWConfig>
/**
* Distance function to use for similarity calculations
*/
distanceFunction?: DistanceFunction
/**
* Custom storage adapter (if not provided, will use OPFS or memory storage)
*/
storageAdapter?: StorageAdapter
/**
* Embedding function to convert data to vectors
*/
embeddingFunction?: EmbeddingFunction
}
export class BrainyData<T = any> {
private index: HNSWIndex
private storage: StorageAdapter | null = null
private isInitialized = false
private embeddingFunction: EmbeddingFunction
/**
* Create a new vector database
*/
constructor(config: BrainyDataConfig = {}) {
// Initialize HNSW index
this.index = new HNSWIndex(
config.hnsw,
config.distanceFunction || cosineDistance
)
// Set storage if provided, otherwise it will be initialized in init()
this.storage = config.storageAdapter || null
// Set embedding function if provided, otherwise use default
this.embeddingFunction = config.embeddingFunction || defaultEmbeddingFunction
}
/**
* Initialize the database
* Loads existing data from storage if available
*/
public async init(): Promise<void> {
if (this.isInitialized) {
return
}
try {
// Initialize storage if not provided in constructor
if (!this.storage) {
this.storage = await createStorage()
}
// Initialize storage
await this.storage!.init()
// Load all nodes from storage
const nodes = await this.storage!.getAllNodes()
// Clear the index and add all nodes
this.index.clear()
for (const node of nodes) {
// Add to index
this.index.addItem({
id: node.id,
vector: node.vector
})
}
this.isInitialized = true
} catch (error) {
console.error('Failed to initialize vector database:', error)
throw new Error(`Failed to initialize vector database: ${error}`)
}
}
/**
* Add a vector or data to the database
* If the input is not a vector, it will be converted using the embedding function
* @param vectorOrData Vector or data to add
* @param metadata Optional metadata to associate with the vector
* @param options Additional options
* @returns The ID of the added vector
*/
public async add(
vectorOrData: Vector | any,
metadata?: T,
options: {
forceEmbed?: boolean // Force using the embedding function even if input is a vector
} = {}
): Promise<string> {
await this.ensureInitialized()
try {
let vector: Vector
// Check if input is already a vector
if (
Array.isArray(vectorOrData) &&
vectorOrData.every((item) => typeof item === 'number') &&
!options.forceEmbed
) {
// Input is already a vector
vector = vectorOrData
} else {
// Input needs to be vectorized
try {
vector = await this.embeddingFunction(vectorOrData)
} catch (embedError) {
throw new Error(`Failed to vectorize data: ${embedError}`)
}
}
// Check if vector is defined
if (!vector) {
throw new Error('Vector is undefined or null')
}
// Generate ID if isn't provided
const id = uuidv4()
// Add to index
this.index.addItem({ id, vector })
// Get the node from the index
const node = this.index.getNodes().get(id)
if (!node) {
throw new Error(`Failed to retrieve newly created node with ID ${id}`)
}
// Save node to storage
await this.storage!.saveNode(node)
// Save metadata if provided
if (metadata !== undefined) {
await this.storage!.saveMetadata(id, metadata)
}
return id
} catch (error) {
console.error('Failed to add vector:', error)
throw new Error(`Failed to add vector: ${error}`)
}
}
/**
* Add multiple vectors or data items to the database
* @param items Array of items to add
* @param options Additional options
* @returns Array of IDs for the added items
*/
public async addBatch(
items: Array<{
vectorOrData: Vector | any;
metadata?: T
}>,
options: {
forceEmbed?: boolean // Force using the embedding function even if input is a vector
} = {}
): Promise<string[]> {
await this.ensureInitialized()
const ids: string[] = []
try {
for (const item of items) {
const id = await this.add(item.vectorOrData, item.metadata, options)
ids.push(id)
}
return ids
} catch (error) {
console.error('Failed to add batch of items:', error)
throw new Error(`Failed to add batch of items: ${error}`)
}
}
/**
* Search for similar vectors
* @param queryVectorOrData Query vector or data to search for
* @param k Number of results to return
* @param options Additional options
* @returns Array of search results
*/
public async search(
queryVectorOrData: Vector | any,
k: number = 10,
options: {
forceEmbed?: boolean // Force using the embedding function even if input is a vector
} = {}
): Promise<SearchResult<T>[]> {
await this.ensureInitialized()
try {
let queryVector: Vector
// Check if input is already a vector
if (
Array.isArray(queryVectorOrData) &&
queryVectorOrData.every((item) => typeof item === 'number') &&
!options.forceEmbed
) {
// Input is already a vector
queryVector = queryVectorOrData
} else {
// Input needs to be vectorized
try {
queryVector = await this.embeddingFunction(queryVectorOrData)
} catch (embedError) {
throw new Error(`Failed to vectorize query data: ${embedError}`)
}
}
// Check if query vector is defined
if (!queryVector) {
throw new Error('Query vector is undefined or null')
}
// Search in the index
const results = this.index.search(queryVector, k)
// Get metadata for each result
const searchResults: SearchResult<T>[] = []
for (const [id, score] of results) {
const node = this.index.getNodes().get(id)
if (!node) {
continue
}
const metadata = await this.storage!.getMetadata(id)
searchResults.push({
id,
score,
vector: node.vector,
metadata
})
}
return searchResults
} catch (error) {
console.error('Failed to search vectors:', error)
throw new Error(`Failed to search vectors: ${error}`)
}
}
/**
* Get a vector by ID
*/
public async get(id: string): Promise<VectorDocument<T> | null> {
await this.ensureInitialized()
try {
// Get node from index
const node = this.index.getNodes().get(id)
if (!node) {
return null
}
// Get metadata
const metadata = await this.storage!.getMetadata(id)
return {
id,
vector: node.vector,
metadata
}
} catch (error) {
console.error(`Failed to get vector ${id}:`, error)
throw new Error(`Failed to get vector ${id}: ${error}`)
}
}
/**
* Delete a vector by ID
*/
public async delete(id: string): Promise<boolean> {
await this.ensureInitialized()
try {
// Remove from index
const removed = this.index.removeItem(id)
if (!removed) {
return false
}
// Remove from storage
await this.storage!.deleteNode(id)
// Try to remove metadata (ignore errors)
try {
await this.storage!.saveMetadata(id, null)
} catch (error) {
// Ignore
}
return true
} catch (error) {
console.error(`Failed to delete vector ${id}:`, error)
throw new Error(`Failed to delete vector ${id}: ${error}`)
}
}
/**
* Update metadata for a vector
*/
public async updateMetadata(id: string, metadata: T): Promise<boolean> {
await this.ensureInitialized()
try {
// Check if a vector exists
const node = this.index.getNodes().get(id)
if (!node) {
return false
}
// Update metadata
await this.storage!.saveMetadata(id, metadata)
return true
} catch (error) {
console.error(`Failed to update metadata for vector ${id}:`, error)
throw new Error(`Failed to update metadata for vector ${id}: ${error}`)
}
}
/**
* Add an edge between two nodes
*/
public async addEdge(
sourceId: string,
targetId: string,
vector?: Vector,
options: {
type?: string
weight?: number
metadata?: any
} = {}
): Promise<string> {
await this.ensureInitialized()
try {
// Check if source and target nodes exist
const sourceNode = this.index.getNodes().get(sourceId)
const targetNode = this.index.getNodes().get(targetId)
if (!sourceNode) {
throw new Error(`Source node with ID ${sourceId} not found`)
}
if (!targetNode) {
throw new Error(`Target node with ID ${targetId} not found`)
}
// Generate ID for the edge
const id = uuidv4()
// Use a provided vector or average of source and target vectors
const edgeVector =
vector ||
sourceNode.vector.map((val, i) => (val + targetNode.vector[i]) / 2)
// Create edge
const edge: Edge = {
id,
vector: edgeVector,
connections: new Map(),
sourceId,
targetId,
type: options.type,
weight: options.weight,
metadata: options.metadata
}
// Add to index
this.index.addItem({ id, vector: edgeVector })
// Get the node from the index
const indexNode = this.index.getNodes().get(id)
if (!indexNode) {
throw new Error(
`Failed to retrieve newly created edge node with ID ${id}`
)
}
// Update edge connections from index
edge.connections = indexNode.connections
// Save edge to storage
await this.storage!.saveEdge(edge)
return id
} catch (error) {
console.error('Failed to add edge:', error)
throw new Error(`Failed to add edge: ${error}`)
}
}
/**
* Get an edge by ID
*/
public async getEdge(id: string): Promise<Edge | null> {
await this.ensureInitialized()
try {
return await this.storage!.getEdge(id)
} catch (error) {
console.error(`Failed to get edge ${id}:`, error)
throw new Error(`Failed to get edge ${id}: ${error}`)
}
}
/**
* Get all edges
*/
public async getAllEdges(): Promise<Edge[]> {
await this.ensureInitialized()
try {
return await this.storage!.getAllEdges()
} catch (error) {
console.error('Failed to get all edges:', error)
throw new Error(`Failed to get all edges: ${error}`)
}
}
/**
* Get edges by source node ID
*/
public async getEdgesBySource(sourceId: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
return await this.storage!.getEdgesBySource(sourceId)
} catch (error) {
console.error(`Failed to get edges by source ${sourceId}:`, error)
throw new Error(`Failed to get edges by source ${sourceId}: ${error}`)
}
}
/**
* Get edges by target node ID
*/
public async getEdgesByTarget(targetId: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
return await this.storage!.getEdgesByTarget(targetId)
} catch (error) {
console.error(`Failed to get edges by target ${targetId}:`, error)
throw new Error(`Failed to get edges by target ${targetId}: ${error}`)
}
}
/**
* Get edges by type
*/
public async getEdgesByType(type: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
return await this.storage!.getEdgesByType(type)
} catch (error) {
console.error(`Failed to get edges by type ${type}:`, error)
throw new Error(`Failed to get edges by type ${type}: ${error}`)
}
}
/**
* Delete an edge
*/
public async deleteEdge(id: string): Promise<boolean> {
await this.ensureInitialized()
try {
// Remove from index
const removed = this.index.removeItem(id)
if (!removed) {
return false
}
// Remove from storage
await this.storage!.deleteEdge(id)
return true
} catch (error) {
console.error(`Failed to delete edge ${id}:`, error)
throw new Error(`Failed to delete edge ${id}: ${error}`)
}
}
/**
* Clear the database
*/
public async clear(): Promise<void> {
await this.ensureInitialized()
try {
// Clear index
this.index.clear()
// Clear storage
await this.storage!.clear()
} catch (error) {
console.error('Failed to clear vector database:', error)
throw new Error(`Failed to clear vector database: ${error}`)
}
}
/**
* Get the number of vectors in the database
*/
public size(): number {
return this.index.size()
}
/**
* Ensure the database is initialized
*/
private async ensureInitialized(): Promise<void> {
if (!this.isInitialized) {
await this.init()
}
}
}
// Export distance functions for convenience
export { euclideanDistance, cosineDistance, manhattanDistance, dotProductDistance } from './utils/index.js'

123
src/coreTypes.ts Normal file
View file

@ -0,0 +1,123 @@
/**
* Type definitions for the Soulcraft Brainy
*/
/**
* Vector representation - an array of numbers
*/
export type Vector = number[];
/**
* A document with a vector embedding and optional metadata
*/
export interface VectorDocument<T = any> {
id: string;
vector: Vector;
metadata?: T;
}
/**
* Search result with similarity score
*/
export interface SearchResult<T = any> {
id: string;
score: number;
vector: Vector;
metadata?: T;
}
/**
* Distance function for comparing vectors
*/
export type DistanceFunction = (a: Vector, b: Vector) => number;
/**
* Embedding function for converting data to vectors
*/
export type EmbeddingFunction = (data: any) => Promise<Vector>;
/**
* Embedding model interface
*/
export interface EmbeddingModel {
/**
* Initialize the embedding model
*/
init(): Promise<void>;
/**
* Embed data into a vector
*/
embed(data: any): Promise<Vector>;
/**
* Dispose of the model resources
*/
dispose(): Promise<void>;
}
/**
* HNSW graph node
*/
export interface HNSWNode {
id: string;
vector: Vector;
connections: Map<number, Set<string>>; // level -> set of connected node ids
}
/**
* Edge representing a relationship between nodes
* Extends HNSWNode to allow edges to be first-class entities in the data model
*/
export interface Edge extends HNSWNode {
sourceId: string; // ID of the source node
targetId: string; // ID of the target node
type?: string; // Optional type of the relationship
weight?: number; // Optional weight of the relationship
metadata?: any; // Optional metadata for the edge
}
/**
* HNSW index configuration
*/
export interface HNSWConfig {
M: number; // Maximum number of connections per node
efConstruction: number; // Size of the dynamic candidate list during construction
efSearch: number; // Size of the dynamic candidate list during search
ml: number; // Maximum level
}
/**
* Storage interface for persistence
*/
export interface StorageAdapter {
init(): Promise<void>;
saveNode(node: HNSWNode): Promise<void>;
getNode(id: string): Promise<HNSWNode | null>;
getAllNodes(): Promise<HNSWNode[]>;
deleteNode(id: string): Promise<void>;
saveEdge(edge: Edge): Promise<void>;
getEdge(id: string): Promise<Edge | null>;
getAllEdges(): Promise<Edge[]>;
getEdgesBySource(sourceId: string): Promise<Edge[]>;
getEdgesByTarget(targetId: string): Promise<Edge[]>;
getEdgesByType(type: string): Promise<Edge[]>;
deleteEdge(id: string): Promise<void>;
saveMetadata(id: string, metadata: any): Promise<void>;
getMetadata(id: string): Promise<any | null>;
clear(): Promise<void>;
}

163
src/examples/basicUsage.ts Normal file
View file

@ -0,0 +1,163 @@
/**
* Basic usage example for the Soulcraft Brainy database
*/
import { BrainyData } from '../brainyData.js'
// Example data - word embeddings
const wordEmbeddings = {
cat: [0.2, 0.3, 0.4, 0.1],
dog: [0.3, 0.2, 0.4, 0.2],
fish: [0.1, 0.1, 0.8, 0.2],
bird: [0.1, 0.4, 0.2, 0.5],
tiger: [0.3, 0.4, 0.3, 0.1],
lion: [0.4, 0.3, 0.2, 0.1],
shark: [0.2, 0.1, 0.7, 0.3],
eagle: [0.2, 0.5, 0.1, 0.4]
}
// Example metadata
const metadata = {
cat: { type: 'mammal', domesticated: true },
dog: { type: 'mammal', domesticated: true },
fish: { type: 'fish', domesticated: false },
bird: { type: 'bird', domesticated: false },
tiger: { type: 'mammal', domesticated: false },
lion: { type: 'mammal', domesticated: false },
shark: { type: 'fish', domesticated: false },
eagle: { type: 'bird', domesticated: false }
}
/**
* Run the example
*/
async function runExample() {
console.log('Initializing vector database...')
// Create a new vector database
const db = new BrainyData()
await db.init()
console.log('Adding vectors to the database...')
// Add vectors to the database
const ids: Record<string, string> = {}
const metadata: Record<string, { type: string; domesticated: boolean }> = {
cat: { type: 'mammal', domesticated: true },
dog: { type: 'mammal', domesticated: true },
fish: { type: 'fish', domesticated: false },
bird: { type: 'bird', domesticated: false },
tiger: { type: 'mammal', domesticated: false },
lion: { type: 'mammal', domesticated: false },
shark: { type: 'fish', domesticated: false },
eagle: { type: 'bird', domesticated: false }
}
for (const [word, vector] of Object.entries(wordEmbeddings)) {
ids[word] = await db.add(vector, metadata[word])
console.log(`Added "${word}" with ID: ${ids[word]}`)
}
console.log('\nDatabase size:', db.size())
// Search for similar vectors
console.log('\nSearching for vectors similar to "cat"...')
const catResults = await db.search(wordEmbeddings['cat'], 3)
console.log('Results:')
for (const result of catResults) {
const word =
Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown'
console.log(
`- ${word} (score: ${result.score.toFixed(4)}, metadata:`,
result.metadata,
')'
)
}
// Search for similar vectors
console.log('\nSearching for vectors similar to "fish"...')
const fishResults = await db.search(wordEmbeddings['fish'], 3)
console.log('Results:')
for (const result of fishResults) {
const word =
Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown'
console.log(
`- ${word} (score: ${result.score.toFixed(4)}, metadata:`,
result.metadata,
')'
)
}
// Update metadata
console.log('\nUpdating metadata for "bird"...')
await db.updateMetadata(ids['bird'], {
...metadata['bird'],
notes: 'Can fly'
})
// Get the updated document
const birdDoc = await db.get(ids['bird'])
console.log('Updated bird document:', birdDoc)
// Delete a vector
console.log('\nDeleting "shark"...')
await db.delete(ids['shark'])
console.log('Database size after deletion:', db.size())
// Search again to verify shark is gone
console.log('\nSearching for vectors similar to "fish" after deletion...')
const fishResultsAfterDeletion = await db.search(wordEmbeddings['fish'], 3)
console.log('Results:')
for (const result of fishResultsAfterDeletion) {
const word =
Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown'
console.log(
`- ${word} (score: ${result.score.toFixed(4)}, metadata:`,
result.metadata,
')'
)
}
console.log('\nExample completed successfully!')
}
// Check if we're in a browser or Node.js environment
if (typeof window !== 'undefined') {
// Browser environment
document.addEventListener('DOMContentLoaded', () => {
const button = document.createElement('button')
button.textContent = 'Run BrainyData Example'
button.addEventListener('click', async () => {
const output = document.createElement('pre')
document.body.appendChild(output)
// Redirect console.log to the output element
const originalLog = console.log
console.log = (...args) => {
originalLog(...args)
output.textContent +=
args
.map((arg) =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg
)
.join(' ') + '\n'
}
try {
await runExample()
} catch (error) {
console.error('Error running example:', error)
}
// Restore console.log
console.log = originalLog
})
document.body.appendChild(button)
})
} else {
// Node.js environment
runExample().catch((error) => {
console.error('Error running example:', error)
})
}

View file

@ -0,0 +1,76 @@
/**
* Example demonstrating how to use the FileSystemStorage adapter with a custom directory
*/
import { BrainyData } from '../brainyData.js'
import { FileSystemStorage } from '../storage/fileSystemStorage.js'
import path from 'path'
// Example data - word embeddings
const wordEmbeddings = {
'cat': [0.2, 0.3, 0.4, 0.1],
'dog': [0.3, 0.2, 0.4, 0.2],
'fish': [0.1, 0.1, 0.8, 0.2]
}
// Example metadata
const metadata: {
[key: string]: { type: string; [key]: boolean } | undefined | null
} = {
'cat': { type: 'mammal', domesticated: true },
'dog': { type: 'mammal', domesticated: true },
'fish': { type: 'fish', domesticated: false }
}
/**
* Run the example
*/
async function runExample() {
console.log('Initializing vector database with custom storage location...')
// Create a custom storage adapter with a specific directory
// This will store data in ./custom-data directory relative to the current working directory
const customStoragePath = path.join(process.cwd(), 'custom-data')
const storageAdapter = new FileSystemStorage(customStoragePath)
// Create a new vector database with the custom storage adapter
const db = new BrainyData({
storageAdapter
})
await db.init()
console.log(`Using custom storage location: ${customStoragePath}`)
console.log('Adding vectors to the database...')
// Add vectors to the database
const ids: Record<string, string> = {}
for (const [word, vector] of Object.entries(wordEmbeddings)) {
ids[word] = await db.add(vector, metadata[word])
console.log(`Added "${word}" with ID: ${ids[word]}`)
}
console.log('\nDatabase size:', db.size())
// Search for similar vectors
console.log('\nSearching for vectors similar to "cat"...')
const catResults = await db.search(wordEmbeddings['cat'], 2)
console.log('Results:')
for (const result of catResults) {
const word = Object.entries(ids).find(([_, id]) => id === result.id)?.[0] || 'unknown'
console.log(`- ${word} (score: ${result.score.toFixed(4)}, metadata:`, result.metadata, ')')
}
console.log('\nExample completed successfully!')
console.log(`Data has been stored in: ${customStoragePath}`)
console.log('You can restart this example to verify that data persists between runs.')
}
// Only run in Node.js environment
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
runExample().catch(error => {
console.error('Error running example:', error)
})
} else {
console.error('This example is designed to run in Node.js environments only.')
}

527
src/hnsw/hnswIndex.ts Normal file
View file

@ -0,0 +1,527 @@
/**
* HNSW (Hierarchical Navigable Small World) Index implementation
* Based on the paper: "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs"
*/
import { DistanceFunction, HNSWConfig, HNSWNode, Vector, VectorDocument } from '../coreTypes.js'
import { euclideanDistance } from '../utils/index.js'
// Default HNSW parameters
const DEFAULT_CONFIG: HNSWConfig = {
M: 16, // Max number of connections per node
efConstruction: 200, // Size of a dynamic candidate list during construction
efSearch: 50, // Size of a dynamic candidate list during search
ml: 16 // Max level
}
export class HNSWIndex {
private nodes: Map<string, HNSWNode> = new Map()
private entryPointId: string | null = null
private maxLevel = 0
private config: HNSWConfig
private distanceFunction: DistanceFunction
private dimension: number | null = null
constructor(
config: Partial<HNSWConfig> = {},
distanceFunction: DistanceFunction = euclideanDistance
) {
this.config = { ...DEFAULT_CONFIG, ...config }
this.distanceFunction = distanceFunction
}
/**
* Add a vector to the index
*/
public addItem(item: VectorDocument): string {
// Check if item is defined
if (!item) {
throw new Error('Item is undefined or null')
}
const { id, vector } = item
// Check if vector is defined
if (!vector) {
throw new Error('Vector is undefined or null')
}
// Set dimension on first insert
if (this.dimension === null) {
this.dimension = vector.length
} else if (vector.length !== this.dimension) {
throw new Error(
`Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}`
)
}
// Generate random level for this node
const nodeLevel = this.getRandomLevel()
// Create new node
const node: HNSWNode = {
id,
vector,
connections: new Map()
}
// Initialize empty connection sets for each level
for (let level = 0; level <= nodeLevel; level++) {
node.connections.set(level, new Set<string>())
}
// If this is the first node, make it the entry point
if (this.nodes.size === 0) {
this.entryPointId = id
this.maxLevel = nodeLevel
this.nodes.set(id, node)
return id
}
// Find entry point
if (!this.entryPointId) {
console.error('Entry point ID is null')
// If there's no entry point, this is the first node, so we should have returned earlier
// This is a safety check
this.entryPointId = id
this.maxLevel = nodeLevel
this.nodes.set(id, node)
return id
}
const entryPoint = this.nodes.get(this.entryPointId)
if (!entryPoint) {
console.error(`Entry point with ID ${this.entryPointId} not found`)
// If the entry point doesn't exist, treat this as the first node
this.entryPointId = id
this.maxLevel = nodeLevel
this.nodes.set(id, node)
return id
}
let currObj = entryPoint
let currDist = this.distanceFunction(vector, entryPoint.vector)
// Traverse the graph from top to bottom to find the closest node
for (let level = this.maxLevel; level > nodeLevel; level--) {
let changed = true
while (changed) {
changed = false
// Check all neighbors at current level
const connections = currObj.connections.get(level) || new Set<string>()
for (const neighborId of connections) {
const neighbor = this.nodes.get(neighborId)
if (!neighbor) {
console.error(`Neighbor with ID ${neighborId} not found in addItem traversal`)
continue
}
const distToNeighbor = this.distanceFunction(vector, neighbor.vector)
if (distToNeighbor < currDist) {
currDist = distToNeighbor
currObj = neighbor
changed = true
}
}
}
}
// For each level from nodeLevel down to 0
for (let level = Math.min(nodeLevel, this.maxLevel); level >= 0; level--) {
// Find ef nearest elements using greedy search
const nearestNodes = this.searchLayer(
vector,
currObj,
this.config.efConstruction,
level
)
// Select M nearest neighbors
const neighbors = this.selectNeighbors(
vector,
nearestNodes,
this.config.M
)
// Add bidirectional connections
for (const [neighborId, _] of neighbors) {
const neighbor = this.nodes.get(neighborId)
if (!neighbor) {
console.error(`Neighbor with ID ${neighborId} not found`)
continue
}
node.connections.get(level)!.add(neighborId)
// Add reverse connection
if (!neighbor.connections.has(level)) {
neighbor.connections.set(level, new Set<string>())
}
neighbor.connections.get(level)!.add(id)
// Ensure neighbor doesn't have too many connections
if (neighbor.connections.get(level)!.size > this.config.M) {
this.pruneConnections(neighbor, level)
}
}
// Update entry point for the next level
if (nearestNodes.size > 0) {
const [nearestId, nearestDist] = [...nearestNodes][0]
if (nearestDist < currDist) {
currDist = nearestDist
const nearestNode = this.nodes.get(nearestId)
if (!nearestNode) {
console.error(`Nearest node with ID ${nearestId} not found in addItem`)
// Keep the current object as is
} else {
currObj = nearestNode
}
}
}
}
// Update max level and entry point if needed
if (nodeLevel > this.maxLevel) {
this.maxLevel = nodeLevel
this.entryPointId = id
}
// Add node to the index
this.nodes.set(id, node)
return id
}
/**
* Search for nearest neighbors
*/
public search(queryVector: Vector, k: number = 10): Array<[string, number]> {
if (this.nodes.size === 0) {
return []
}
// Check if query vector is defined
if (!queryVector) {
throw new Error('Query vector is undefined or null')
}
if (this.dimension !== null && queryVector.length !== this.dimension) {
throw new Error(
`Query vector dimension mismatch: expected ${this.dimension}, got ${queryVector.length}`
)
}
// Start from the entry point
if (!this.entryPointId) {
console.error('Entry point ID is null')
return []
}
const entryPoint = this.nodes.get(this.entryPointId)
if (!entryPoint) {
console.error(`Entry point with ID ${this.entryPointId} not found`)
return []
}
let currObj = entryPoint
let currDist = this.distanceFunction(queryVector, currObj.vector)
// Traverse the graph from top to bottom to find the closest node
for (let level = this.maxLevel; level > 0; level--) {
let changed = true
while (changed) {
changed = false
// Check all neighbors at current level
const connections = currObj.connections.get(level) || new Set<string>()
for (const neighborId of connections) {
const neighbor = this.nodes.get(neighborId)
if (!neighbor) {
console.error(`Neighbor with ID ${neighborId} not found in search`)
continue
}
const distToNeighbor = this.distanceFunction(
queryVector,
neighbor.vector
)
if (distToNeighbor < currDist) {
currDist = distToNeighbor
currObj = neighbor
changed = true
}
}
}
}
// Search at level 0 with ef = k
const nearestNodes = this.searchLayer(
queryVector,
currObj,
Math.max(this.config.efSearch, k),
0
)
// Convert to array and sort by distance
return [...nearestNodes].slice(0, k)
}
/**
* Remove an item from the index
*/
public removeItem(id: string): boolean {
if (!this.nodes.has(id)) {
return false
}
const node = this.nodes.get(id)!
// Remove connections to this node from all neighbors
for (const [level, connections] of node.connections.entries()) {
for (const neighborId of connections) {
const neighbor = this.nodes.get(neighborId)
if (!neighbor) {
console.error(`Neighbor with ID ${neighborId} not found in removeItem`)
continue
}
if (neighbor.connections.has(level)) {
neighbor.connections.get(level)!.delete(id)
// Prune connections after removing this node to ensure consistency
this.pruneConnections(neighbor, level)
}
}
}
// Also check all other nodes for references to this node and remove them
for (const [nodeId, otherNode] of this.nodes.entries()) {
if (nodeId === id) continue // Skip the node being removed
for (const [level, connections] of otherNode.connections.entries()) {
if (connections.has(id)) {
connections.delete(id)
// Prune connections after removing this reference
this.pruneConnections(otherNode, level)
}
}
}
// Remove the node
this.nodes.delete(id)
// If we removed the entry point, find a new one
if (this.entryPointId === id) {
if (this.nodes.size === 0) {
this.entryPointId = null
this.maxLevel = 0
} else {
// Find the node with the highest level
let maxLevel = 0
let newEntryPointId = null
for (const [nodeId, node] of this.nodes.entries()) {
if (node.connections.size === 0) continue // Skip nodes with no connections
const nodeLevel = Math.max(...node.connections.keys())
if (nodeLevel >= maxLevel) {
maxLevel = nodeLevel
newEntryPointId = nodeId
}
}
this.entryPointId = newEntryPointId
this.maxLevel = maxLevel
}
}
return true
}
/**
* Get all nodes in the index
*/
public getNodes(): Map<string, HNSWNode> {
return new Map(this.nodes)
}
/**
* Clear the index
*/
public clear(): void {
this.nodes.clear()
this.entryPointId = null
this.maxLevel = 0
}
/**
* Get the size of the index
*/
public size(): number {
return this.nodes.size
}
/**
* Search within a specific layer
* Returns a map of node IDs to distances, sorted by distance
*/
private searchLayer(
queryVector: Vector,
entryPoint: HNSWNode,
ef: number,
level: number
): Map<string, number> {
// Set of visited nodes
const visited = new Set<string>([entryPoint.id])
// Priority queue of candidates (closest first)
const candidates = new Map<string, number>()
candidates.set(
entryPoint.id,
this.distanceFunction(queryVector, entryPoint.vector)
)
// Priority queue of nearest neighbors found so far (closest first)
const nearest = new Map<string, number>()
nearest.set(
entryPoint.id,
this.distanceFunction(queryVector, entryPoint.vector)
)
// While there are candidates to explore
while (candidates.size > 0) {
// Get closest candidate
const [closestId, closestDist] = [...candidates][0]
candidates.delete(closestId)
// If this candidate is farther than the farthest in our result set, we're done
const farthestInNearest = [...nearest][nearest.size - 1]
if (nearest.size >= ef && closestDist > farthestInNearest[1]) {
break
}
// Explore neighbors of the closest candidate
const node = this.nodes.get(closestId)
if (!node) {
console.error(`Node with ID ${closestId} not found in searchLayer`)
continue
}
const connections = node.connections.get(level) || new Set<string>()
for (const neighborId of connections) {
if (!visited.has(neighborId)) {
visited.add(neighborId)
const neighbor = this.nodes.get(neighborId)
if (!neighbor) {
console.error(`Neighbor with ID ${neighborId} not found in searchLayer`)
continue
}
const distToNeighbor = this.distanceFunction(
queryVector,
neighbor.vector
)
// If we haven't found ef nearest neighbors yet, or this neighbor is closer than the farthest one we've found
if (nearest.size < ef || distToNeighbor < farthestInNearest[1]) {
candidates.set(neighborId, distToNeighbor)
nearest.set(neighborId, distToNeighbor)
// If we have more than ef neighbors, remove the farthest one
if (nearest.size > ef) {
const sortedNearest = [...nearest].sort((a, b) => a[1] - b[1])
nearest.clear()
for (let i = 0; i < ef; i++) {
nearest.set(sortedNearest[i][0], sortedNearest[i][1])
}
}
}
}
}
}
// Sort nearest by distance
return new Map([...nearest].sort((a, b) => a[1] - b[1]))
}
/**
* Select M nearest neighbors from the candidate set
*/
private selectNeighbors(
queryVector: Vector,
candidates: Map<string, number>,
M: number
): Map<string, number> {
if (candidates.size <= M) {
return candidates
}
// Simple heuristic: just take the M closest
const sortedCandidates = [...candidates].sort((a, b) => a[1] - b[1])
const result = new Map<string, number>()
for (let i = 0; i < Math.min(M, sortedCandidates.length); i++) {
result.set(sortedCandidates[i][0], sortedCandidates[i][1])
}
return result
}
/**
* Ensure a node doesn't have too many connections at a given level
*/
private pruneConnections(node: HNSWNode, level: number): void {
const connections = node.connections.get(level)!
if (connections.size <= this.config.M) {
return
}
// Calculate distances to all neighbors
const distances = new Map<string, number>()
const validNeighborIds = new Set<string>()
for (const neighborId of connections) {
const neighbor = this.nodes.get(neighborId)
if (!neighbor) {
console.error(`Neighbor with ID ${neighborId} not found in pruneConnections`)
continue
}
// Only add valid neighbors to the distances map
distances.set(
neighborId,
this.distanceFunction(node.vector, neighbor.vector)
)
validNeighborIds.add(neighborId)
}
// Only proceed if we have valid neighbors
if (distances.size === 0) {
// If no valid neighbors, clear connections at this level
node.connections.set(level, new Set())
return
}
// Select M closest neighbors from valid ones
const selectedNeighbors = this.selectNeighbors(
node.vector,
distances,
this.config.M
)
// Update connections with only valid neighbors
node.connections.set(level, new Set(selectedNeighbors.keys()))
}
/**
* Generate a random level for a new node
* Uses the same distribution as in the original HNSW paper
*/
private getRandomLevel(): number {
const r = Math.random()
return Math.floor(-Math.log(r) * (1.0 / Math.log(this.config.M)))
}
}

61
src/index.ts Normal file
View file

@ -0,0 +1,61 @@
/**
* OPFS BrainyData
* A vector database using HNSW indexing with Origin Private File System storage
*/
// Export main BrainyData class and related types
import { BrainyData, BrainyDataConfig } from './brainyData.js'
export { BrainyData }
export type { BrainyDataConfig }
// Export distance functions for convenience
import {
euclideanDistance,
cosineDistance,
manhattanDistance,
dotProductDistance
} from './utils/index.js'
export {
euclideanDistance,
cosineDistance,
manhattanDistance,
dotProductDistance
}
// Export storage adapters
import {
OPFSStorage,
MemoryStorage,
createStorage
} from './storage/opfsStorage.js'
export {
OPFSStorage,
MemoryStorage,
createStorage
}
// Export types
import type {
Vector,
VectorDocument,
SearchResult,
DistanceFunction,
EmbeddingFunction,
EmbeddingModel,
HNSWNode,
Edge,
HNSWConfig,
StorageAdapter
} from './coreTypes.js'
export type {
Vector,
VectorDocument,
SearchResult,
DistanceFunction,
EmbeddingFunction,
EmbeddingModel,
HNSWNode,
Edge,
HNSWConfig,
StorageAdapter
}

View file

@ -0,0 +1,432 @@
import fs from 'fs'
import path from 'path'
import { Edge, HNSWNode, StorageAdapter } from '../coreTypes.js'
// Constants for directory and file names
const ROOT_DIR = 'brainy-data'
const NODES_DIR = 'nodes'
const EDGES_DIR = 'edges'
const METADATA_DIR = 'metadata'
/**
* File system storage adapter for Node.js environments
*/
export class FileSystemStorage implements StorageAdapter {
private rootDir: string
private nodesDir: string
private edgesDir: string
private metadataDir: string
private isInitialized = false
constructor(rootDirectory?: string) {
// Use the provided root directory or default to the current working directory
this.rootDir = rootDirectory ? path.resolve(rootDirectory, ROOT_DIR) : path.resolve(process.cwd(), ROOT_DIR)
this.nodesDir = path.join(this.rootDir, NODES_DIR)
this.edgesDir = path.join(this.rootDir, EDGES_DIR)
this.metadataDir = path.join(this.rootDir, METADATA_DIR)
}
/**
* Initialize the storage adapter
*/
public async init(): Promise<void> {
if (this.isInitialized) {
return
}
try {
// Create directories if they don't exist
await this.ensureDirectoryExists(this.rootDir)
await this.ensureDirectoryExists(this.nodesDir)
await this.ensureDirectoryExists(this.edgesDir)
await this.ensureDirectoryExists(this.metadataDir)
this.isInitialized = true
} catch (error) {
console.error('Failed to initialize file system storage:', error)
throw new Error(`Failed to initialize file system storage: ${error}`)
}
}
/**
* Save a node to storage
*/
public async saveNode(node: HNSWNode): Promise<void> {
await this.ensureInitialized()
try {
// Convert connections Map to a serializable format
const serializableNode = {
...node,
connections: this.mapToObject(node.connections, (set) => Array.from(set))
}
const filePath = path.join(this.nodesDir, `${node.id}.json`)
await fs.promises.writeFile(
filePath,
JSON.stringify(serializableNode, null, 2),
'utf8'
)
} catch (error) {
console.error(`Failed to save node ${node.id}:`, error)
throw new Error(`Failed to save node ${node.id}: ${error}`)
}
}
/**
* Get a node from storage
*/
public async getNode(id: string): Promise<HNSWNode | null> {
await this.ensureInitialized()
try {
const filePath = path.join(this.nodesDir, `${id}.json`)
// Check if a file exists
try {
await fs.promises.access(filePath)
} catch {
return null // File doesn't exist
}
const data = await fs.promises.readFile(filePath, 'utf8')
const parsedNode = JSON.parse(data)
// Convert serialized connections back to Map<number, Set<string>>
const connections = new Map<number, Set<string>>()
for (const [level, nodeIds] of Object.entries(parsedNode.connections)) {
connections.set(Number(level), new Set(nodeIds as string[]))
}
return {
id: parsedNode.id,
vector: parsedNode.vector,
connections
}
} catch (error) {
console.error(`Failed to get node ${id}:`, error)
return null
}
}
/**
* Get all nodes from storage
*/
public async getAllNodes(): Promise<HNSWNode[]> {
await this.ensureInitialized()
try {
const files = await fs.promises.readdir(this.nodesDir)
const nodePromises = files
.filter(file => file.endsWith('.json'))
.map(file => {
const id = path.basename(file, '.json')
return this.getNode(id)
})
const nodes = await Promise.all(nodePromises)
return nodes.filter((node): node is HNSWNode => node !== null)
} catch (error) {
console.error('Failed to get all nodes:', error)
throw new Error(`Failed to get all nodes: ${error}`)
}
}
/**
* Delete a node from storage
*/
public async deleteNode(id: string): Promise<void> {
await this.ensureInitialized()
try {
const filePath = path.join(this.nodesDir, `${id}.json`)
// Check if a file exists before attempting to delete
try {
await fs.promises.access(filePath)
} catch {
return // File doesn't exist, nothing to delete
}
await fs.promises.unlink(filePath)
} catch (error) {
console.error(`Failed to delete node ${id}:`, error)
throw new Error(`Failed to delete node ${id}: ${error}`)
}
}
/**
* Save an edge to storage
*/
public async saveEdge(edge: Edge): Promise<void> {
await this.ensureInitialized()
try {
// Convert connections Map to a serializable format
const serializableEdge = {
...edge,
connections: this.mapToObject(edge.connections, (set) => Array.from(set))
}
const filePath = path.join(this.edgesDir, `${edge.id}.json`)
await fs.promises.writeFile(
filePath,
JSON.stringify(serializableEdge, null, 2),
'utf8'
)
} catch (error) {
console.error(`Failed to save edge ${edge.id}:`, error)
throw new Error(`Failed to save edge ${edge.id}: ${error}`)
}
}
/**
* Get an edge from storage
*/
public async getEdge(id: string): Promise<Edge | null> {
await this.ensureInitialized()
try {
const filePath = path.join(this.edgesDir, `${id}.json`)
// Check if a file exists
try {
await fs.promises.access(filePath)
} catch {
return null // File doesn't exist
}
const data = await fs.promises.readFile(filePath, 'utf8')
const parsedEdge = JSON.parse(data)
// Convert serialized connections back to Map<number, Set<string>>
const connections = new Map<number, Set<string>>()
for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) {
connections.set(Number(level), new Set(nodeIds as string[]))
}
return {
id: parsedEdge.id,
vector: parsedEdge.vector,
connections,
sourceId: parsedEdge.sourceId,
targetId: parsedEdge.targetId,
type: parsedEdge.type,
weight: parsedEdge.weight,
metadata: parsedEdge.metadata
}
} catch (error) {
console.error(`Failed to get edge ${id}:`, error)
return null
}
}
/**
* Get all edges from storage
*/
public async getAllEdges(): Promise<Edge[]> {
await this.ensureInitialized()
try {
const files = await fs.promises.readdir(this.edgesDir)
const edgePromises = files
.filter(file => file.endsWith('.json'))
.map(file => {
const id = path.basename(file, '.json')
return this.getEdge(id)
})
const edges = await Promise.all(edgePromises)
return edges.filter((edge): edge is Edge => edge !== null)
} catch (error) {
console.error('Failed to get all edges:', error)
throw new Error(`Failed to get all edges: ${error}`)
}
}
/**
* Get edges by source node ID
*/
public async getEdgesBySource(sourceId: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
const allEdges = await this.getAllEdges()
return allEdges.filter(edge => edge.sourceId === sourceId)
} catch (error) {
console.error(`Failed to get edges by source ${sourceId}:`, error)
throw new Error(`Failed to get edges by source ${sourceId}: ${error}`)
}
}
/**
* Get edges by target node ID
*/
public async getEdgesByTarget(targetId: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
const allEdges = await this.getAllEdges()
return allEdges.filter(edge => edge.targetId === targetId)
} catch (error) {
console.error(`Failed to get edges by target ${targetId}:`, error)
throw new Error(`Failed to get edges by target ${targetId}: ${error}`)
}
}
/**
* Get edges by type
*/
public async getEdgesByType(type: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
const allEdges = await this.getAllEdges()
return allEdges.filter(edge => edge.type === type)
} catch (error) {
console.error(`Failed to get edges by type ${type}:`, error)
throw new Error(`Failed to get edges by type ${type}: ${error}`)
}
}
/**
* Delete an edge from storage
*/
public async deleteEdge(id: string): Promise<void> {
await this.ensureInitialized()
try {
const filePath = path.join(this.edgesDir, `${id}.json`)
// Check if a file exists before attempting to delete
try {
await fs.promises.access(filePath)
} catch {
return // File doesn't exist, nothing to delete
}
await fs.promises.unlink(filePath)
} catch (error) {
console.error(`Failed to delete edge ${id}:`, error)
throw new Error(`Failed to delete edge ${id}: ${error}`)
}
}
/**
* Save metadata to storage
*/
public async saveMetadata(id: string, metadata: any): Promise<void> {
await this.ensureInitialized()
try {
const filePath = path.join(this.metadataDir, `${id}.json`)
await fs.promises.writeFile(
filePath,
JSON.stringify(metadata, null, 2),
'utf8'
)
} catch (error) {
console.error(`Failed to save metadata for ${id}:`, error)
throw new Error(`Failed to save metadata for ${id}: ${error}`)
}
}
/**
* Get metadata from storage
*/
public async getMetadata(id: string): Promise<any | null> {
await this.ensureInitialized()
try {
const filePath = path.join(this.metadataDir, `${id}.json`)
// Check if a file exists
try {
await fs.promises.access(filePath)
} catch {
return null // File doesn't exist
}
const data = await fs.promises.readFile(filePath, 'utf8')
return JSON.parse(data)
} catch (error) {
console.error(`Failed to get metadata for ${id}:`, error)
return null
}
}
/**
* Clear all data from storage
*/
public async clear(): Promise<void> {
await this.ensureInitialized()
try {
// Delete and recreate the nodes, edges, and metadata directories
await this.deleteDirectory(this.nodesDir)
await this.deleteDirectory(this.edgesDir)
await this.deleteDirectory(this.metadataDir)
await this.ensureDirectoryExists(this.nodesDir)
await this.ensureDirectoryExists(this.edgesDir)
await this.ensureDirectoryExists(this.metadataDir)
} catch (error) {
console.error('Failed to clear storage:', error)
throw new Error(`Failed to clear storage: ${error}`)
}
}
/**
* Ensure the storage adapter is initialized
*/
private async ensureInitialized(): Promise<void> {
if (!this.isInitialized) {
await this.init()
}
}
/**
* Ensure a directory exists, creating it if necessary
*/
private async ensureDirectoryExists(dirPath: string): Promise<void> {
try {
await fs.promises.access(dirPath)
} catch {
// Directory doesn't exist, create it
await fs.promises.mkdir(dirPath, { recursive: true })
}
}
/**
* Delete a directory and all its contents
*/
private async deleteDirectory(dirPath: string): Promise<void> {
try {
const files = await fs.promises.readdir(dirPath)
for (const file of files) {
const filePath = path.join(dirPath, file)
await fs.promises.unlink(filePath)
}
} catch (error) {
// If the directory doesn't exist, that's fine
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
}
}
/**
* Convert a Map to a plain object for serialization
*/
private mapToObject<K extends string | number, V>(
map: Map<K, V>,
valueTransformer: (value: V) => any = (v) => v
): Record<string, any> {
const obj: Record<string, any> = {}
for (const [key, value] of map.entries()) {
obj[key.toString()] = valueTransformer(value)
}
return obj
}
}

680
src/storage/opfsStorage.ts Normal file
View file

@ -0,0 +1,680 @@
/**
* OPFS (Origin Private File System) Storage Adapter
* Provides persistent storage for the vector database using the Origin Private File System API
*/
import { Edge, HNSWNode, StorageAdapter } from '../coreTypes.js'
// Directory and file names
const ROOT_DIR = 'opfs-vector-db'
const NODES_DIR = 'nodes'
const EDGES_DIR = 'edges'
const METADATA_DIR = 'metadata'
const DB_INFO_FILE = 'db-info.json'
export class OPFSStorage implements StorageAdapter {
private rootDir: FileSystemDirectoryHandle | null = null
private nodesDir: FileSystemDirectoryHandle | null = null
private edgesDir: FileSystemDirectoryHandle | null = null
private metadataDir: FileSystemDirectoryHandle | null = null
private isInitialized = false
private isAvailable = false
constructor() {
// Check if OPFS is available
this.isAvailable =
typeof navigator !== 'undefined' &&
'storage' in navigator &&
'getDirectory' in navigator.storage
}
/**
* Initialize the storage adapter
*/
public async init(): Promise<void> {
if (this.isInitialized) {
return
}
if (!this.isAvailable) {
throw new Error(
'Origin Private File System is not available in this environment'
)
}
try {
// Get the root directory
const root = await navigator.storage.getDirectory()
// Create or get our app's root directory
this.rootDir = await root.getDirectoryHandle(ROOT_DIR, { create: true })
// Create or get nodes directory
this.nodesDir = await this.rootDir.getDirectoryHandle(NODES_DIR, {
create: true
})
// Create or get edges directory
this.edgesDir = await this.rootDir.getDirectoryHandle(EDGES_DIR, {
create: true
})
// Create or get metadata directory
this.metadataDir = await this.rootDir.getDirectoryHandle(METADATA_DIR, {
create: true
})
this.isInitialized = true
} catch (error) {
console.error('Failed to initialize OPFS storage:', error)
throw new Error(`Failed to initialize OPFS storage: ${error}`)
}
}
/**
* Check if OPFS is available in the current environment
*/
public isOPFSAvailable(): boolean {
return this.isAvailable
}
/**
* Save a node to storage
*/
public async saveNode(node: HNSWNode): Promise<void> {
await this.ensureInitialized()
try {
// Convert connections Map to a serializable format
const serializableNode = {
...node,
connections: this.mapToObject(node.connections, (set) =>
Array.from(set)
)
}
// Create or get the file for this node
const fileHandle = await this.nodesDir!.getFileHandle(node.id, {
create: true
})
// Write the node data to the file
const writable = await fileHandle.createWritable()
await writable.write(JSON.stringify(serializableNode))
await writable.close()
} catch (error) {
console.error(`Failed to save node ${node.id}:`, error)
throw new Error(`Failed to save node ${node.id}: ${error}`)
}
}
/**
* Get a node from storage
*/
public async getNode(id: string): Promise<HNSWNode | null> {
await this.ensureInitialized()
try {
// Get the file handle for this node
const fileHandle = await this.nodesDir!.getFileHandle(id)
// Read the node data from the file
const file = await fileHandle.getFile()
const text = await file.text()
const data = JSON.parse(text)
// Convert serialized connections back to Map<number, Set<string>>
const connections = new Map<number, Set<string>>()
for (const [level, nodeIds] of Object.entries(data.connections)) {
connections.set(Number(level), new Set(nodeIds as string[]))
}
return {
id: data.id,
vector: data.vector,
connections
}
} catch (error) {
// If the file doesn't exist, return null
if ((error as any).name === 'NotFoundError') {
return null
}
console.error(`Failed to get node ${id}:`, error)
throw new Error(`Failed to get node ${id}: ${error}`)
}
}
/**
* Get all nodes from storage
*/
public async getAllNodes(): Promise<HNSWNode[]> {
await this.ensureInitialized()
try {
const nodes: HNSWNode[] = []
// Get all keys (filenames) in the nodes directory
// @ts-ignore - TypeScript doesn't recognize FileSystemDirectoryHandle.keys() properly
const keys = this.nodesDir!.keys()
// Iterate through all keys and get the corresponding nodes
for await (const name of keys) {
const node = await this.getNode(name)
if (node) {
nodes.push(node)
}
}
return nodes
} catch (error) {
console.error('Failed to get all nodes:', error)
throw new Error(`Failed to get all nodes: ${error}`)
}
}
/**
* Delete a node from storage
*/
public async deleteNode(id: string): Promise<void> {
await this.ensureInitialized()
try {
await this.nodesDir!.removeEntry(id)
} catch (error) {
// Ignore if the file doesn't exist
if ((error as any).name !== 'NotFoundError') {
console.error(`Failed to delete node ${id}:`, error)
throw new Error(`Failed to delete node ${id}: ${error}`)
}
}
}
/**
* Save an edge to storage
*/
public async saveEdge(edge: Edge): Promise<void> {
await this.ensureInitialized()
try {
// Convert connections Map to a serializable format
const serializableEdge = {
...edge,
connections: this.mapToObject(edge.connections, (set) =>
Array.from(set)
)
}
// Create or get the file for this edge
const fileHandle = await this.edgesDir!.getFileHandle(edge.id, {
create: true
})
// Write the edge data to the file
const writable = await fileHandle.createWritable()
await writable.write(JSON.stringify(serializableEdge))
await writable.close()
} catch (error) {
console.error(`Failed to save edge ${edge.id}:`, error)
throw new Error(`Failed to save edge ${edge.id}: ${error}`)
}
}
/**
* Get an edge from storage
*/
public async getEdge(id: string): Promise<Edge | null> {
await this.ensureInitialized()
try {
// Get the file handle for this edge
const fileHandle = await this.edgesDir!.getFileHandle(id)
// Read the edge data from the file
const file = await fileHandle.getFile()
const text = await file.text()
const data = JSON.parse(text)
// Convert serialized connections back to Map<number, Set<string>>
const connections = new Map<number, Set<string>>()
for (const [level, nodeIds] of Object.entries(data.connections)) {
connections.set(Number(level), new Set(nodeIds as string[]))
}
return {
id: data.id,
vector: data.vector,
connections,
sourceId: data.sourceId,
targetId: data.targetId,
type: data.type,
weight: data.weight,
metadata: data.metadata
}
} catch (error) {
// If the file doesn't exist, return null
if ((error as any).name === 'NotFoundError') {
return null
}
console.error(`Failed to get edge ${id}:`, error)
throw new Error(`Failed to get edge ${id}: ${error}`)
}
}
/**
* Get all edges from storage
*/
public async getAllEdges(): Promise<Edge[]> {
await this.ensureInitialized()
try {
const edges: Edge[] = []
// Get all keys (filenames) in the edges directory
// @ts-ignore - TypeScript doesn't recognize FileSystemDirectoryHandle.keys() properly
const keys = this.edgesDir!.keys()
// Iterate through all keys and get the corresponding edges
for await (const name of keys) {
const edge = await this.getEdge(name)
if (edge) {
edges.push(edge)
}
}
return edges
} catch (error) {
console.error('Failed to get all edges:', error)
throw new Error(`Failed to get all edges: ${error}`)
}
}
/**
* Delete an edge from storage
*/
public async deleteEdge(id: string): Promise<void> {
await this.ensureInitialized()
try {
await this.edgesDir!.removeEntry(id)
} catch (error) {
// Ignore if the file doesn't exist
if ((error as any).name !== 'NotFoundError') {
console.error(`Failed to delete edge ${id}:`, error)
throw new Error(`Failed to delete edge ${id}: ${error}`)
}
}
}
/**
* Get edges by source node ID
*/
public async getEdgesBySource(sourceId: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
const allEdges = await this.getAllEdges()
return allEdges.filter(edge => edge.sourceId === sourceId)
} catch (error) {
console.error(`Failed to get edges by source ${sourceId}:`, error)
throw new Error(`Failed to get edges by source ${sourceId}: ${error}`)
}
}
/**
* Get edges by target node ID
*/
public async getEdgesByTarget(targetId: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
const allEdges = await this.getAllEdges()
return allEdges.filter(edge => edge.targetId === targetId)
} catch (error) {
console.error(`Failed to get edges by target ${targetId}:`, error)
throw new Error(`Failed to get edges by target ${targetId}: ${error}`)
}
}
/**
* Get edges by type
*/
public async getEdgesByType(type: string): Promise<Edge[]> {
await this.ensureInitialized()
try {
const allEdges = await this.getAllEdges()
return allEdges.filter(edge => edge.type === type)
} catch (error) {
console.error(`Failed to get edges by type ${type}:`, error)
throw new Error(`Failed to get edges by type ${type}: ${error}`)
}
}
/**
* Save metadata for a node
*/
public async saveMetadata(id: string, metadata: any): Promise<void> {
await this.ensureInitialized()
try {
// Create or get the file for this metadata
const fileHandle = await this.metadataDir!.getFileHandle(id, {
create: true
})
// Write the metadata to the file
const writable = await fileHandle.createWritable()
await writable.write(JSON.stringify(metadata))
await writable.close()
} catch (error) {
console.error(`Failed to save metadata for ${id}:`, error)
throw new Error(`Failed to save metadata for ${id}: ${error}`)
}
}
/**
* Get metadata for a node
*/
public async getMetadata(id: string): Promise<any | null> {
await this.ensureInitialized()
try {
// Get the file handle for this metadata
const fileHandle = await this.metadataDir!.getFileHandle(id)
// Read the metadata from the file
const file = await fileHandle.getFile()
const text = await file.text()
return JSON.parse(text)
} catch (error) {
// If the file doesn't exist, return null
if ((error as any).name === 'NotFoundError') {
return null
}
console.error(`Failed to get metadata for ${id}:`, error)
throw new Error(`Failed to get metadata for ${id}: ${error}`)
}
}
/**
* Clear all data from storage
*/
public async clear(): Promise<void> {
await this.ensureInitialized()
try {
// Delete and recreate the nodes directory
await this.rootDir!.removeEntry(NODES_DIR, { recursive: true })
this.nodesDir = await this.rootDir!.getDirectoryHandle(NODES_DIR, {
create: true
})
// Delete and recreate the edges directory
await this.rootDir!.removeEntry(EDGES_DIR, { recursive: true })
this.edgesDir = await this.rootDir!.getDirectoryHandle(EDGES_DIR, {
create: true
})
// Delete and recreate the metadata directory
await this.rootDir!.removeEntry(METADATA_DIR, { recursive: true })
this.metadataDir = await this.rootDir!.getDirectoryHandle(METADATA_DIR, {
create: true
})
} catch (error) {
console.error('Failed to clear storage:', error)
throw new Error(`Failed to clear storage: ${error}`)
}
}
/**
* Ensure the storage adapter is initialized
*/
private async ensureInitialized(): Promise<void> {
if (!this.isInitialized) {
await this.init()
}
}
/**
* Convert a Map to a plain object for serialization
*/
private mapToObject<K extends string | number, V>(
map: Map<K, V>,
valueTransformer: (value: V) => any = (v) => v
): Record<string, any> {
const obj: Record<string, any> = {}
for (const [key, value] of map.entries()) {
obj[key.toString()] = valueTransformer(value)
}
return obj
}
}
/**
* In-memory storage adapter for environments where OPFS is not available
*/
export class MemoryStorage implements StorageAdapter {
private nodes: Map<string, HNSWNode> = new Map()
private edges: Map<string, Edge> = new Map()
private metadata: Map<string, any> = new Map()
public async init(): Promise<void> {
// Nothing to initialize for in-memory storage
}
public async saveNode(node: HNSWNode): Promise<void> {
// Create a deep copy to avoid reference issues
const nodeCopy: HNSWNode = {
id: node.id,
vector: [...node.vector],
connections: new Map()
}
// Copy connections
for (const [level, connections] of node.connections.entries()) {
nodeCopy.connections.set(level, new Set(connections))
}
this.nodes.set(node.id, nodeCopy)
}
public async getNode(id: string): Promise<HNSWNode | null> {
const node = this.nodes.get(id)
if (!node) {
return null
}
// Return a deep copy to avoid reference issues
const nodeCopy: HNSWNode = {
id: node.id,
vector: [...node.vector],
connections: new Map()
}
// Copy connections
for (const [level, connections] of node.connections.entries()) {
nodeCopy.connections.set(level, new Set(connections))
}
return nodeCopy
}
public async getAllNodes(): Promise<HNSWNode[]> {
const nodes: HNSWNode[] = []
for (const nodeId of this.nodes.keys()) {
const node = await this.getNode(nodeId)
if (node) {
nodes.push(node)
}
}
return nodes
}
public async deleteNode(id: string): Promise<void> {
this.nodes.delete(id)
}
public async saveMetadata(id: string, metadata: any): Promise<void> {
this.metadata.set(id, JSON.parse(JSON.stringify(metadata)))
}
public async getMetadata(id: string): Promise<any | null> {
const metadata = this.metadata.get(id)
if (!metadata) {
return null
}
return JSON.parse(JSON.stringify(metadata))
}
public async saveEdge(edge: Edge): Promise<void> {
// Create a deep copy to avoid reference issues
const edgeCopy: Edge = {
id: edge.id,
vector: [...edge.vector],
connections: new Map(),
sourceId: edge.sourceId,
targetId: edge.targetId,
type: edge.type,
weight: edge.weight,
metadata: edge.metadata ? JSON.parse(JSON.stringify(edge.metadata)) : undefined
}
// Copy connections
for (const [level, connections] of edge.connections.entries()) {
edgeCopy.connections.set(level, new Set(connections))
}
this.edges.set(edge.id, edgeCopy)
}
public async getEdge(id: string): Promise<Edge | null> {
const edge = this.edges.get(id)
if (!edge) {
return null
}
// Return a deep copy to avoid reference issues
const edgeCopy: Edge = {
id: edge.id,
vector: [...edge.vector],
connections: new Map(),
sourceId: edge.sourceId,
targetId: edge.targetId,
type: edge.type,
weight: edge.weight,
metadata: edge.metadata ? JSON.parse(JSON.stringify(edge.metadata)) : undefined
}
// Copy connections
for (const [level, connections] of edge.connections.entries()) {
edgeCopy.connections.set(level, new Set(connections))
}
return edgeCopy
}
public async getAllEdges(): Promise<Edge[]> {
const edges: Edge[] = []
for (const edgeId of this.edges.keys()) {
const edge = await this.getEdge(edgeId)
if (edge) {
edges.push(edge)
}
}
return edges
}
public async getEdgesBySource(sourceId: string): Promise<Edge[]> {
const edges: Edge[] = []
for (const edge of this.edges.values()) {
if (edge.sourceId === sourceId) {
const edgeCopy = await this.getEdge(edge.id)
if (edgeCopy) {
edges.push(edgeCopy)
}
}
}
return edges
}
public async getEdgesByTarget(targetId: string): Promise<Edge[]> {
const edges: Edge[] = []
for (const edge of this.edges.values()) {
if (edge.targetId === targetId) {
const edgeCopy = await this.getEdge(edge.id)
if (edgeCopy) {
edges.push(edgeCopy)
}
}
}
return edges
}
public async getEdgesByType(type: string): Promise<Edge[]> {
const edges: Edge[] = []
for (const edge of this.edges.values()) {
if (edge.type === type) {
const edgeCopy = await this.getEdge(edge.id)
if (edgeCopy) {
edges.push(edgeCopy)
}
}
}
return edges
}
public async deleteEdge(id: string): Promise<void> {
this.edges.delete(id)
}
public async clear(): Promise<void> {
this.nodes.clear()
this.edges.clear()
this.metadata.clear()
}
}
/**
* Factory function to create the appropriate storage adapter based on the environment
*/
export async function createStorage(): Promise<StorageAdapter> {
// Check if we're in a Node.js environment
const isNode = typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null
if (isNode) {
// In Node.js, use FileSystemStorage
try {
const fileSystemModule = await import('./fileSystemStorage.js')
return new fileSystemModule.FileSystemStorage()
} catch (error) {
console.warn('Failed to load FileSystemStorage, falling back to in-memory storage:', error)
return new MemoryStorage()
}
} else {
// In browser, try OPFS first
const opfsStorage = new OPFSStorage()
if (opfsStorage.isOPFSAvailable()) {
return opfsStorage
} else {
console.warn('OPFS is not available, falling back to in-memory storage')
return new MemoryStorage()
}
}
}

283
src/types/augmentations.ts Normal file
View file

@ -0,0 +1,283 @@
/** Common types for augmentation system */
type WebSocketConnection = {
connectionId: string
url: string
status: 'connected' | 'disconnected' | 'error'
}
type DataCallback<T> = (data: T) => void
type AugmentationResponse<T> = Promise<{
success: boolean
data: T
error?: string
}>
/**
* Base interface for all Brainy augmentations.
* All augmentations must implement these core properties.
*/
interface IAugmentation {
/** A unique identifier for the augmentation (e.g., "my-reasoner-v1") */
readonly name: string
/** A human-readable description of the augmentation's purpose */
readonly description: string
/**
* Initializes the augmentation. This method is called when Brainy starts up.
* @returns A Promise that resolves when initialization is complete
*/
initialize(): Promise<void>
shutDown(): Promise<void>
getStatus(): Promise<'active' | 'inactive' | 'error'>
}
/**
* Interface for WebSocket support.
* Augmentations that implement this interface can communicate via WebSockets.
*/
interface IWebSocketSupport {
/**
* Establishes a WebSocket connection.
* @param url The WebSocket server URL to connect to
* @param protocols Optional subprotocols
* @returns A Promise resolving to a connection handle or status
*/
connectWebSocket(url: string, protocols?: string | string[]): Promise<WebSocketConnection>
/**
* Sends data through an established WebSocket connection.
* @param connectionId The identifier of the established connection
* @param data The data to send (will be serialized if not a string)
*/
sendWebSocketMessage(connectionId: string, data: unknown): Promise<void>
/**
* Registers a callback for incoming WebSocket messages.
* @param connectionId The identifier of the established connection
* @param callback The function to call when a message is received
*/
onWebSocketMessage(connectionId: string, callback: DataCallback<unknown>): Promise<void>
/**
* Closes an established WebSocket connection.
* @param connectionId The identifier of the established connection
* @param code Optional close code
* @param reason Optional close reason
*/
closeWebSocket(connectionId: string, code?: number, reason?: string): Promise<void>
}
namespace BrainyAugmentations {
/**
* Interface for Cognitions augmentations.
* These augmentations enable advanced reasoning, inference, and logical operations.
*/
export interface ICognitionAugmentation extends IAugmentation {
/**
* Performs a reasoning operation based on current knowledge.
* @param query The specific reasoning task or question
* @param context Optional additional context for the reasoning
*/
reason(query: string, context?: Record<string, unknown>): AugmentationResponse<{
inference: string
confidence: number
}>
/**
* Infers relationships or new facts from existing data.
* @param dataSubset A subset of data to infer from
*/
infer(dataSubset: Record<string, unknown>): AugmentationResponse<Record<string, unknown>>
/**
* Executes a logical operation or rule set.
* @param ruleId The identifier of the rule or logic to apply
* @param input Data to apply the logic to
*/
executeLogic(ruleId: string, input: Record<string, unknown>): AugmentationResponse<boolean>
}
/**
* Interface for Senses augmentations.
* These augmentations ingest and process raw, unstructured data into nouns and verbs.
*/
export interface ISenseAugmentation extends IAugmentation {
/**
* Processes raw input data into structured nouns and verbs.
* @param rawData The raw, unstructured data (e.g., text, image buffer, audio stream)
* @param dataType The type of raw data (e.g., 'text', 'image', 'audio')
*/
processRawData(rawData: Buffer | string, dataType: string): AugmentationResponse<{
nouns: string[]
verbs: string[]
}>
/**
* Registers a listener for real-time data feeds.
* @param feedUrl The URL or identifier of the real-time feed
* @param callback A function to call with processed data
*/
listenToFeed(
feedUrl: string,
callback: DataCallback<{ nouns: string[]; verbs: string[] }>
): Promise<void>
}
/**
* Interface for Perceptions augmentations.
* These augmentations interpret, contextualize, and visualize identified nouns and verbs.
*/
export interface IPerceptionAugmentation extends IAugmentation {
/**
* Interprets and contextualizes processed nouns and verbs.
* @param nouns The list of identified nouns
* @param verbs The list of identified verbs
* @param context Optional additional context for interpretation
*/
interpret(
nouns: string[],
verbs: string[],
context?: Record<string, unknown>
): AugmentationResponse<Record<string, unknown>>
/**
* Organizes and filters information.
* @param data The data to organize (e.g., interpreted perceptions)
* @param criteria Optional criteria for filtering/prioritization
*/
organize(
data: Record<string, unknown>,
criteria?: Record<string, unknown>
): AugmentationResponse<Record<string, unknown>>
/**
* Generates a visualization based on the provided data.
* @param data The data to visualize (e.g., interpreted patterns)
* @param visualizationType The desired type of visualization (e.g., 'graph', 'chart')
*/
generateVisualization(
data: Record<string, unknown>,
visualizationType: string
): AugmentationResponse<string | Buffer | Record<string, unknown>>
}
/**
* Interface for Activations augmentations.
* These augmentations dictate how Brainy initiates actions, responses, or data manipulations.
*/
export interface IActivationAugmentation extends IAugmentation {
/**
* Triggers an action based on a processed command or internal state.
* @param actionName The name of the action to trigger
* @param parameters Optional parameters for the action
*/
triggerAction(
actionName: string,
parameters?: Record<string, unknown>
): AugmentationResponse<unknown>
/**
* Generates an expressive output or response from Brainy.
* @param knowledgeId The identifier of the knowledge to express
* @param format The desired output format (e.g., 'text', 'json')
*/
generateOutput(knowledgeId: string, format: string): AugmentationResponse<string | Record<string, unknown>>
/**
* Interacts with an external system or API.
* @param systemId The identifier of the external system
* @param payload The data to send to the external system
*/
interactExternal(systemId: string, payload: Record<string, unknown>): AugmentationResponse<unknown>
}
/**
* Interface for Dialogs augmentations.
* These augmentations facilitate natural language understanding and generation for conversational interaction.
*/
export interface IDialogAugmentation extends IAugmentation {
/**
* Processes a user's natural language input (query).
* @param naturalLanguageQuery The raw text query from the user
* @param sessionId An optional session ID for conversational context
*/
processUserInput(naturalLanguageQuery: string, sessionId?: string): AugmentationResponse<{
intent: string
nouns: string[]
verbs: string[]
context: Record<string, unknown>
}>
/**
* Generates a natural language response based on Brainy's knowledge and interpreted input.
* @param interpretedInput The output from `processUserInput` or similar
* @param knowledgeContext Relevant knowledge retrieved from Brainy
* @param sessionId An optional session ID for conversational context
*/
generateResponse(
interpretedInput: Record<string, unknown>,
knowledgeContext: Record<string, unknown>,
sessionId?: string
): AugmentationResponse<string>
/**
* Manages and updates conversational context.
* @param sessionId The session ID
* @param contextUpdate The data to update the context with
*/
manageContext(sessionId: string, contextUpdate: Record<string, unknown>): Promise<void>
}
/**
* Interface for Conduits augmentations.
* These augmentations establish and manage high-bandwidth, dedicated channels for structured, programmatic two-way data exchange.
*/
export interface IConduitAugmentation extends IAugmentation {
/**
* Establishes a connection for programmatic data exchange.
* @param targetSystemId The identifier of the external system to connect to
* @param config Configuration details for the connection (e.g., API keys, endpoints)
*/
establishConnection(
targetSystemId: string,
config: Record<string, unknown>
): AugmentationResponse<WebSocketConnection>
/**
* Reads structured data directly from Brainy's knowledge graph.
* @param query A structured query (e.g., graph query language, object path)
* @param options Optional query options (e.g., depth, filters)
*/
readData(
query: Record<string, unknown>,
options?: Record<string, unknown>
): AugmentationResponse<unknown>
/**
* Writes or updates structured data directly into Brainy's knowledge graph.
* @param data The structured data to write/update
* @param options Optional write options (e.g., merge, overwrite)
*/
writeData(
data: Record<string, unknown>,
options?: Record<string, unknown>
): AugmentationResponse<unknown>
/**
* Monitors a specific data stream or event within Brainy for external systems.
* @param streamId The identifier of the data stream or event
* @param callback A function to call when new data/events occur
*/
monitorStream(streamId: string, callback: DataCallback<unknown>): Promise<void>
}
}
/** WebSocket-enabled augmentation interfaces */
type IWebSocketCognitionAugmentation = BrainyAugmentations.ICognitionAugmentation & IWebSocketSupport
type IWebSocketSenseAugmentation = BrainyAugmentations.ISenseAugmentation & IWebSocketSupport
type IWebSocketPerceptionAugmentation = BrainyAugmentations.IPerceptionAugmentation & IWebSocketSupport
type IWebSocketActivationAugmentation = BrainyAugmentations.IActivationAugmentation & IWebSocketSupport
type IWebSocketDialogAugmentation = BrainyAugmentations.IDialogAugmentation & IWebSocketSupport
type IWebSocketConduitAugmentation = BrainyAugmentations.IConduitAugmentation & IWebSocketSupport

View file

@ -0,0 +1,12 @@
/**
* Type declarations for the File System Access API
* Extends the FileSystemDirectoryHandle interface to include the [Symbol.asyncIterator] method
*/
// Extend the FileSystemDirectoryHandle interface
interface FileSystemDirectoryHandle {
[Symbol.asyncIterator](): AsyncIterableIterator<[string, FileSystemHandle]>;
}
// Export something to make this a module
export const fileSystemTypesLoaded = true;

129
src/types/graphTypes.ts Normal file
View file

@ -0,0 +1,129 @@
// Common metadata types
/**
* Represents a high-precision timestamp with seconds and nanoseconds
* Used for tracking creation and update times of graph elements
*/
interface Timestamp {
seconds: number
nanoseconds: number
}
/**
* Metadata about the creator/source of a graph noun
* Tracks which augmentation and model created the element
*/
interface CreatorMetadata {
augmentation: string // Name of the augmentation that created this element
version: string // Version of the augmentation
model: string // Model identifier used in creation
modelVersion: string // Version of the model
}
/**
* Base interface for nodes (nouns) in the graph
* Represents entities like people, places, things, etc.
*/
export interface GraphNoun {
id: string // Unique identifier for the noun
createdBy: CreatorMetadata // Information about what created this noun
noun: NounType // Type classification of the noun
createdAt: Timestamp // When the noun was created
updatedAt: Timestamp // When the noun was last updated
data?: Record<string, unknown> // Additional flexible data storage
embedding?: number[] // Vector representation of the noun
}
/**
* Base interface for edges (verbs) in the graph
* Represents relationships between nouns
*/
export interface GraphVerb {
id: string // Unique identifier for the verb
source: string // ID of the source noun
target: string // ID of the target noun
label?: string // Optional descriptive label
verb: VerbType // Type of relationship
createdAt: Timestamp // When the verb was created
updatedAt: Timestamp // When the verb was last updated
data?: Record<string, unknown> // Additional flexible data storage
embedding?: number[] // Vector representation of the relationship
confidence?: number // Confidence score (0-1)
weight?: number // Strength/importance of the relationship
}
/**
* Version of GraphVerb for embedded relationships
* Used when the source is implicit from the parent document
*/
export type EmbeddedGraphVerb = Omit<GraphVerb, 'source'>
// Proper Noun interfaces - extend GraphNoun with specific noun types
/**
* Represents a person entity in the graph
*/
export interface Person extends GraphNoun {
noun: typeof NounType.Person
}
/**
* Represents a physical location in the graph
*/
export interface Place extends GraphNoun {
noun: typeof NounType.Place
}
/**
* Represents a physical or virtual object in the graph
*/
export interface Thing extends GraphNoun {
noun: typeof NounType.Thing
}
/**
* Represents an event or occurrence in the graph
*/
export interface Event extends GraphNoun {
noun: typeof NounType.Event
}
/**
* Represents an abstract concept or idea in the graph
*/
export interface Concept extends GraphNoun {
noun: typeof NounType.Concept
}
/**
* Represents content (text, media, etc.) in the graph
*/
export interface Content extends GraphNoun {
noun: typeof NounType.Content
}
/**
* Defines valid noun types for graph entities
* Used for categorizing different types of nodes
*/
export const NounType = {
Person: 'person', // Person entities
Place: 'place', // Physical locations
Thing: 'thing', // Physical or virtual objects
Event: 'event', // Events or occurrences
Concept: 'concept', // Abstract concepts or ideas
Content: 'content' // Content items
} as const
export type NounType = (typeof NounType)[keyof typeof NounType]
/**
* Defines valid verb types for relationships
* Used for categorizing different types of connections
*/
export const VerbType = {
AttributedTo: 'attributedTo', // Indicates attribution or authorship
Controls: 'controls', // Indicates control or ownership
Created: 'created', // Indicates creation or authorship
Earned: 'earned', // Indicates achievement or acquisition
Owns: 'owns' // Indicates ownership
} as const
export type VerbType = (typeof VerbType)[keyof typeof VerbType]

View file

@ -0,0 +1,14 @@
// Type declarations for TensorFlow.js models
// This file is a placeholder for TensorFlow.js model types
// We're using type assertions in the code instead of module declarations
// Define some basic types that might be useful
export interface TensorflowModel {
load(): Promise<any>;
embed(data: string[]): any;
dispose(): void;
}
// Export a dummy constant to make this a proper module
export const tensorflowModelsLoaded = true;

88
src/utils/distance.ts Normal file
View file

@ -0,0 +1,88 @@
/**
* Distance functions for vector similarity calculations
*/
import { DistanceFunction, Vector } from '../coreTypes.js'
/**
* Calculates the Euclidean distance between two vectors
* Lower values indicate higher similarity
*/
export const euclideanDistance: DistanceFunction = (a: Vector, b: Vector): number => {
if (a.length !== b.length) {
throw new Error('Vectors must have the same dimensions')
}
let sum = 0
for (let i = 0; i < a.length; i++) {
const diff = a[i] - b[i]
sum += diff * diff
}
return Math.sqrt(sum)
}
/**
* Calculates the cosine distance between two vectors
* Lower values indicate higher similarity
* Range: 0 (identical) to 2 (opposite)
*/
export const cosineDistance: DistanceFunction = (a: Vector, b: Vector): number => {
if (a.length !== b.length) {
throw new Error('Vectors must have the same dimensions')
}
let dotProduct = 0
let normA = 0
let normB = 0
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
if (normA === 0 || normB === 0) {
return 2 // Maximum distance for zero vectors
}
const similarity = dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))
// Convert cosine similarity (-1 to 1) to distance (0 to 2)
return 1 - similarity
}
/**
* Calculates the Manhattan (L1) distance between two vectors
* Lower values indicate higher similarity
*/
export const manhattanDistance: DistanceFunction = (a: Vector, b: Vector): number => {
if (a.length !== b.length) {
throw new Error('Vectors must have the same dimensions')
}
let sum = 0
for (let i = 0; i < a.length; i++) {
sum += Math.abs(a[i] - b[i])
}
return sum
}
/**
* Calculates the dot product similarity between two vectors
* Higher values indicate higher similarity
* Converted to a distance metric (lower is better)
*/
export const dotProductDistance: DistanceFunction = (a: Vector, b: Vector): number => {
if (a.length !== b.length) {
throw new Error('Vectors must have the same dimensions')
}
let dotProduct = 0
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i]
}
// Convert to a distance metric (lower is better)
return -dotProduct
}

168
src/utils/embedding.ts Normal file
View file

@ -0,0 +1,168 @@
/**
* Embedding functions for converting data to vectors
*/
import { EmbeddingFunction, EmbeddingModel, Vector } from '../coreTypes.js'
/**
* Simple character-based embedding function
* This is a very basic implementation for demo purposes
*/
export class SimpleEmbedding implements EmbeddingModel {
private initialized = false
/**
* Initialize the embedding model
*/
public async init(): Promise<void> {
this.initialized = true
return Promise.resolve()
}
/**
* Embed text into a vector using character frequencies
* @param data Text to embed
*/
public async embed(data: string): Promise<Vector> {
if (!this.initialized) {
await this.init()
}
// Only handle string data
if (typeof data !== 'string') {
throw new Error('SimpleEmbedding only supports string data')
}
// Normalize the text
const normalizedText = data.toLowerCase().trim()
// Create a simple 4-dimensional vector based on character frequencies
const vector: Vector = [0, 0, 0, 0]
// Count vowels, consonants, numbers, and special characters
for (let i = 0; i < normalizedText.length; i++) {
const char = normalizedText[i]
if ('aeiou'.includes(char)) {
vector[0] += 0.1 // Vowels affect first dimension
} else if ('bcdfghjklmnpqrstvwxyz'.includes(char)) {
vector[1] += 0.1 // Consonants affect second dimension
} else if ('0123456789'.includes(char)) {
vector[2] += 0.1 // Numbers affect third dimension
} else {
vector[3] += 0.1 // Special chars affect fourth dimension
}
}
// Normalize the vector
const magnitude = Math.sqrt(
vector.reduce((sum, val) => sum + val * val, 0)
)
if (magnitude > 0) {
return vector.map((val) => val / magnitude)
}
return vector
}
/**
* Dispose of the model resources
*/
public async dispose(): Promise<void> {
this.initialized = false
return Promise.resolve()
}
}
/**
* TensorFlow Universal Sentence Encoder embedding model
* Requires @tensorflow/tfjs and @tensorflow-models/universal-sentence-encoder to be installed
*/
export class UniversalSentenceEncoder implements EmbeddingModel {
private model: any = null
private initialized = false
private tf: any = null
private use: any = null
/**
* Initialize the embedding model
*/
public async init(): Promise<void> {
try {
// Dynamically import TensorFlow.js and Universal Sentence Encoder
// Use type assertions to tell TypeScript these modules exist
this.tf = await import('@tensorflow/tfjs/dist/tf.esm.js' as any)
this.use = await import('@tensorflow-models/universal-sentence-encoder/dist/universal-sentence-encoder.esm.js' as any)
// Load the model
this.model = await this.use.load()
this.initialized = true
} catch (error) {
console.error('Failed to initialize Universal Sentence Encoder:', error)
throw new Error(`Failed to initialize Universal Sentence Encoder: ${error}`)
}
}
/**
* Embed text into a vector using Universal Sentence Encoder
* @param data Text to embed
*/
public async embed(data: string | string[]): Promise<Vector> {
if (!this.initialized) {
await this.init()
}
try {
// Handle different input types
let textToEmbed: string[]
if (typeof data === 'string') {
textToEmbed = [data]
} else if (Array.isArray(data) && data.every(item => typeof item === 'string')) {
textToEmbed = data
} else {
throw new Error('UniversalSentenceEncoder only supports string or string[] data')
}
// Get embeddings
const embeddings = await this.model.embed(textToEmbed)
// Convert to array and return the first embedding
const embeddingArray = await embeddings.array()
return embeddingArray[0]
} catch (error) {
console.error('Failed to embed text with Universal Sentence Encoder:', error)
throw new Error(`Failed to embed text with Universal Sentence Encoder: ${error}`)
}
}
/**
* Dispose of the model resources
*/
public async dispose(): Promise<void> {
if (this.model && this.tf) {
try {
// Dispose of the model and tensors
this.model.dispose()
this.tf.disposeVariables()
this.initialized = false
} catch (error) {
console.error('Failed to dispose Universal Sentence Encoder:', error)
}
}
return Promise.resolve()
}
}
/**
* Create an embedding function from an embedding model
* @param model Embedding model to use
*/
export function createEmbeddingFunction(model: EmbeddingModel): EmbeddingFunction {
return async (data: any): Promise<Vector> => {
return await model.embed(data)
}
}
/**
* Default embedding function using UniversalSentenceEncoder
*/
export const defaultEmbeddingFunction: EmbeddingFunction = createEmbeddingFunction(new UniversalSentenceEncoder())

2
src/utils/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from './distance.js'
export * from './embedding.js'