UNPKG

@fluxgraph/knowledge

Version:

A flexible, database-agnostic knowledge graph implementation for TypeScript

1,338 lines (1,331 loc) 96.5 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/core/KnowledgeGraph.ts var KnowledgeGraph = class { adapter; initialized = false; constructor(adapter) { this.adapter = adapter; } /** * Initialize the knowledge graph */ async initialize() { if (this.initialized) return; await this.adapter.initialize(); this.initialized = true; } /** * Ensure the graph is initialized */ async ensureInitialized() { if (!this.initialized) { await this.initialize(); } } // ============ Node Operations ============ /** * Add a new node to the graph */ async addNode(options) { await this.ensureInitialized(); const id = crypto.randomUUID(); const now = /* @__PURE__ */ new Date(); const newNode = { id, type: options.type, label: options.label, properties: JSON.stringify(options.properties || {}), confidence: options.confidence || 1, createdAt: now, updatedAt: now, sourceSessionIds: options.sourceSessionId ? JSON.stringify([options.sourceSessionId]) : void 0 }; const node = await this.adapter.insertNode(newNode); await this.indexNodeForSearch(node); await this.adapter.insertNodeIndex({ indexKey: `type:${node.type}`, nodeId: node.id, createdAt: now }); return this.normalizeNode(node); } /** * Update an existing node */ async updateNode(nodeId, updates, mergeProperties = true) { await this.ensureInitialized(); const existingNode = await this.adapter.getNode(nodeId); if (!existingNode) return null; const existingProperties = existingNode.properties; const properties = mergeProperties && updates.properties ? { ...existingProperties, ...updates.properties } : updates.properties || existingProperties; const nodeUpdates = { type: updates.type, label: updates.label, properties: JSON.stringify(properties), confidence: updates.confidence, updatedAt: /* @__PURE__ */ new Date() }; if (updates.sourceSessionId) { const existingSessionIds = Array.isArray(existingNode.sourceSessionIds) ? existingNode.sourceSessionIds : []; if (!existingSessionIds.includes(updates.sourceSessionId)) { existingSessionIds.push(updates.sourceSessionId); nodeUpdates.sourceSessionIds = JSON.stringify(existingSessionIds); } } const updatedNode = await this.adapter.updateNode(nodeId, nodeUpdates); if (!updatedNode) return null; await this.adapter.deleteSearchIndex(nodeId); await this.indexNodeForSearch(updatedNode); return this.normalizeNode(updatedNode); } /** * Delete a node and all its edges */ async deleteNode(nodeId) { await this.ensureInitialized(); return await this.adapter.deleteNode(nodeId); } /** * Get a node by ID */ async getNode(nodeId) { await this.ensureInitialized(); const node = await this.adapter.getNode(nodeId); return node ? this.normalizeNode(node) : null; } /** * Find nodes by label (exact or partial match) */ async findNodesByLabel(label, exact = false) { await this.ensureInitialized(); if (exact) { const nodes3 = await this.adapter.queryNodes({ label }, 100); return nodes3.map((n) => this.normalizeNode(n)); } const searchResults = await this.adapter.searchNodes(label.toLowerCase()); const nodeIds = [...new Set(searchResults.map((r) => r.nodeId))]; if (nodeIds.length === 0) return []; const nodes2 = await this.adapter.getNodes(nodeIds); return nodes2.map((n) => this.normalizeNode(n)); } // ============ Edge Operations ============ /** * Add an edge between two nodes */ async addEdge(options) { await this.ensureInitialized(); const [fromNode, toNode] = await Promise.all([this.adapter.getNode(options.fromNodeId), this.adapter.getNode(options.toNodeId)]); if (!fromNode) { throw new Error(`From node ${options.fromNodeId} does not exist`); } if (!toNode) { throw new Error(`To node ${options.toNodeId} does not exist`); } const id = crypto.randomUUID(); const now = /* @__PURE__ */ new Date(); const newEdge = { id, type: options.type, fromNodeId: options.fromNodeId, toNodeId: options.toNodeId, properties: JSON.stringify(options.properties || {}), confidence: options.confidence || 1, createdAt: now, sourceSessionIds: options.sourceSessionId ? JSON.stringify([options.sourceSessionId]) : void 0 }; const edge = await this.adapter.insertEdge(newEdge); await Promise.all([ this.adapter.insertEdgeIndex({ indexKey: `from:${options.fromNodeId}:${options.type}`, edgeId: id, createdAt: now }), this.adapter.insertEdgeIndex({ indexKey: `to:${options.toNodeId}:${options.type}`, edgeId: id, createdAt: now }), this.adapter.insertEdgeIndex({ indexKey: `type:${options.type}`, edgeId: id, createdAt: now }) ]); if (options.bidirectional) { await this.addEdge({ ...options, fromNodeId: options.toNodeId, toNodeId: options.fromNodeId, bidirectional: false // Prevent infinite recursion }); } return this.normalizeEdge(edge); } /** * Delete an edge */ async deleteEdge(edgeId) { await this.ensureInitialized(); return await this.adapter.deleteEdge(edgeId); } /** * Get edges between two nodes */ async getEdgesBetween(fromNodeId, toNodeId, edgeType) { await this.ensureInitialized(); const conditions = { fromNodeId, toNodeId }; if (edgeType) { conditions.type = edgeType; } const edges2 = await this.adapter.queryEdges(conditions); return edges2.map((e) => this.normalizeEdge(e)); } // ============ Query Operations ============ /** * Query nodes by type */ async queryByType(nodeType, options) { await this.ensureInitialized(); const nodes2 = await this.adapter.queryNodes({ type: nodeType }, options?.limit || 100, options?.offset || 0); const edges2 = []; if (options?.includeEdges && nodes2.length > 0) { const nodeIds = nodes2.map((n) => n.id); const edgeResults = await Promise.all([this.adapter.queryEdges({ fromNodeId: nodeIds[0] }), this.adapter.queryEdges({ toNodeId: nodeIds[0] })]); edges2.push(...edgeResults.flat().map((e) => this.normalizeEdge(e))); } return { nodes: nodes2.map((n) => this.normalizeNode(n)), edges: edges2, relevanceScore: 1 }; } /** * Query related nodes starting from a given node */ async queryRelated(nodeId, options) { await this.ensureInitialized(); const startNode = await this.adapter.getNode(nodeId); if (!startNode) { return { nodes: [], edges: [], relevanceScore: 0 }; } const visitedNodes = /* @__PURE__ */ new Map(); const visitedEdges = /* @__PURE__ */ new Map(); visitedNodes.set(nodeId, this.normalizeNode(startNode)); await this.traverseGraph(nodeId, options?.depth || 1, options?.direction || "both", options?.edgeTypes, visitedNodes, visitedEdges); return { nodes: Array.from(visitedNodes.values()), edges: Array.from(visitedEdges.values()), relevanceScore: this.calculateRelevance(visitedNodes.size, visitedEdges.size) }; } /** * Search nodes using text query */ async search(options) { await this.ensureInitialized(); const searchTerms = options.query.toLowerCase().split(/\s+/); const nodeScores = /* @__PURE__ */ new Map(); for (const term of searchTerms) { const results = await this.adapter.searchNodes(term, options.limit || 50); for (const result of results) { const currentScore = nodeScores.get(result.nodeId) || 0; nodeScores.set(result.nodeId, currentScore + result.weight); } } const minScore = options.minScore || 0; const qualifiedNodeIds = Array.from(nodeScores.entries()).filter(([_, score]) => score >= minScore).sort((a, b) => b[1] - a[1]).slice(0, options.limit || 50).map(([id]) => id); if (qualifiedNodeIds.length === 0) { return { nodes: [], edges: [], relevanceScore: 0 }; } const nodes2 = await this.adapter.getNodes(qualifiedNodeIds); const filteredNodes = options.nodeTypes ? nodes2.filter((n) => options.nodeTypes.includes(n.type)) : nodes2; return { nodes: filteredNodes.map((n) => this.normalizeNode(n)), edges: [], relevanceScore: qualifiedNodeIds[0] ? nodeScores.get(qualifiedNodeIds[0]) || 0 : 0 }; } // ============ Graph Traversal ============ /** * Traverse the graph from a starting node */ async traverse(options) { await this.ensureInitialized(); const visitedNodes = /* @__PURE__ */ new Map(); const visitedEdges = /* @__PURE__ */ new Map(); const queue = [{ nodeId: options.startNodeId, depth: 0 }]; const visited = /* @__PURE__ */ new Set(); while (queue.length > 0) { const { nodeId, depth } = queue.shift(); if (visited.has(nodeId) && options.visitOnce !== false) continue; if (depth > (options.maxDepth || Infinity)) continue; visited.add(nodeId); const node = await this.adapter.getNode(nodeId); if (!node) continue; const normalizedNode = this.normalizeNode(node); if (!options.nodeFilter || options.nodeFilter(normalizedNode)) { visitedNodes.set(nodeId, normalizedNode); } const edges2 = []; if (options.direction === "out" || options.direction === "both") { const outEdges = await this.adapter.queryEdges({ from_node_id: nodeId }); edges2.push(...outEdges.map((e) => this.normalizeEdge(e))); } if (options.direction === "in" || options.direction === "both") { const inEdges = await this.adapter.queryEdges({ to_node_id: nodeId }); edges2.push(...inEdges.map((e) => this.normalizeEdge(e))); } for (const edge of edges2) { if (options.edgeTypes && !options.edgeTypes.includes(edge.type)) continue; if (options.edgeFilter && !options.edgeFilter(edge)) continue; visitedEdges.set(edge.id, edge); const nextNodeId = edge.fromNodeId === nodeId ? edge.toNodeId : edge.fromNodeId; if (!visited.has(nextNodeId) || options.visitOnce === false) { queue.push({ nodeId: nextNodeId, depth: depth + 1 }); } } } return { nodes: Array.from(visitedNodes.values()), edges: Array.from(visitedEdges.values()), relevanceScore: 1 }; } /** * Find shortest path between two nodes */ async findShortestPath(fromNodeId, toNodeId, options) { await this.ensureInitialized(); const queue = [{ nodeId: fromNodeId, path: [fromNodeId], edges: [] }]; const visited = /* @__PURE__ */ new Set(); while (queue.length > 0) { const { nodeId, path, edges: edges2 } = queue.shift(); if (nodeId === toNodeId) { const nodes2 = await this.adapter.getNodes(path); const edgeObjects = edges2.length > 0 ? await this.adapter.getEdges(edges2) : []; return { nodes: nodes2.map((n) => this.normalizeNode(n)), edges: edgeObjects.map((e) => this.normalizeEdge(e)), length: path.length - 1 }; } if (visited.has(nodeId)) continue; visited.add(nodeId); const outEdges = await this.adapter.queryEdges({ fromNodeId: nodeId }); for (const edge of outEdges) { if (options?.edgeTypes && !options.edgeTypes.includes(edge.type)) continue; const nextNodeId = edge.toNodeId; if (!visited.has(nextNodeId)) { queue.push({ nodeId: nextNodeId, path: [...path, nextNodeId], edges: [...edges2, edge.id] }); } } } return null; } // ============ Statistics ============ /** * Get graph statistics */ async getStats() { await this.ensureInitialized(); const dbStats = await this.adapter.getStats(); const nodeTypes = await this.execute("SELECT type, COUNT(*) as count FROM kg_nodes GROUP BY type"); const edgeTypes = await this.execute("SELECT type, COUNT(*) as count FROM kg_edges GROUP BY type"); const nodesByType = {}; for (const { type, count } of nodeTypes) { nodesByType[type] = count; } const edgesByType = {}; for (const { type, count } of edgeTypes) { edgesByType[type] = count; } const averageDegree = dbStats.nodeCount > 0 ? dbStats.edgeCount * 2 / dbStats.nodeCount : 0; const possibleEdges = dbStats.nodeCount * (dbStats.nodeCount - 1); const density = possibleEdges > 0 ? dbStats.edgeCount / possibleEdges : 0; return { nodeCount: dbStats.nodeCount, edgeCount: dbStats.edgeCount, nodesByType, edgesByType, averageDegree, density, lastUpdated: /* @__PURE__ */ new Date() }; } // ============ Batch Operations ============ /** * Batch insert nodes */ async batchAddNodes(nodes2) { await this.ensureInitialized(); const successful = []; const errors = []; for (const nodeOptions of nodes2) { try { const node = await this.addNode(nodeOptions); successful.push(node.id); } catch (error) { errors.push({ item: nodeOptions, error }); } } return { successful: successful.length, failed: errors.length, errors: errors.length > 0 ? errors : void 0 }; } /** * Batch insert edges */ async batchAddEdges(edges2) { await this.ensureInitialized(); const successful = []; const errors = []; for (const edgeOptions of edges2) { try { const edge = await this.addEdge(edgeOptions); successful.push(edge.id); } catch (error) { errors.push({ item: edgeOptions, error }); } } return { successful: successful.length, failed: errors.length, errors: errors.length > 0 ? errors : void 0 }; } // ============ Maintenance ============ /** * Vacuum the database to reclaim space */ async vacuum() { await this.ensureInitialized(); await this.adapter.vacuum(); } /** * Close the database connection */ async close() { await this.adapter.close(); this.initialized = false; } // ============ Helper Methods ============ /** * Execute raw SQL query (for advanced use cases) */ async execute(query, params = []) { return await this.adapter.execute(query, params); } /** * Normalize a node from database format */ normalizeNode(node) { return { id: node.id, type: node.type, label: node.label, properties: typeof node.properties === "string" ? JSON.parse(node.properties) : node.properties, confidence: node.confidence, createdAt: node.createdAt instanceof Date ? node.createdAt : new Date(node.createdAt), updatedAt: node.updatedAt instanceof Date ? node.updatedAt : new Date(node.updatedAt), sourceSessionIds: node.sourceSessionIds ? typeof node.sourceSessionIds === "string" ? JSON.parse(node.sourceSessionIds) : node.sourceSessionIds : void 0 }; } /** * Normalize an edge from database format */ normalizeEdge(edge) { return { id: edge.id, type: edge.type, fromNodeId: edge.fromNodeId, toNodeId: edge.toNodeId, properties: typeof edge.properties === "string" ? JSON.parse(edge.properties) : edge.properties, confidence: edge.confidence, createdAt: edge.createdAt instanceof Date ? edge.createdAt : new Date(edge.createdAt), sourceSessionIds: edge.sourceSessionIds ? typeof edge.sourceSessionIds === "string" ? JSON.parse(edge.sourceSessionIds) : edge.sourceSessionIds : void 0 }; } /** * Index a node for search */ async indexNodeForSearch(node) { const searchTerms = /* @__PURE__ */ new Set(); const labelTerms = node.label.toLowerCase().split(/\s+/); labelTerms.forEach((term) => searchTerms.add(term)); const properties = typeof node.properties === "string" ? JSON.parse(node.properties) : node.properties; for (const [, value] of Object.entries(properties)) { if (typeof value === "string") { const terms = value.toLowerCase().split(/\s+/); terms.forEach((term) => searchTerms.add(term)); } } for (const term of searchTerms) { await this.adapter.insertSearchIndex({ term, nodeId: node.id, field: "label", weight: 1 }); } } /** * Traverse graph helper */ async traverseGraph(nodeId, depth, direction, edgeTypes, visitedNodes, visitedEdges, currentDepth = 0) { if (currentDepth >= depth) return; const edges2 = []; if (direction === "out" || direction === "both") { const outEdges = await this.adapter.queryEdges({ fromNodeId: nodeId }); edges2.push(...outEdges); } if (direction === "in" || direction === "both") { const inEdges = await this.adapter.queryEdges({ toNodeId: nodeId }); edges2.push(...inEdges); } for (const edge of edges2) { if (edgeTypes && !edgeTypes.includes(edge.type)) continue; visitedEdges.set(edge.id, this.normalizeEdge(edge)); const nextNodeId = edge.fromNodeId === nodeId ? edge.toNodeId : edge.fromNodeId; if (!visitedNodes.has(nextNodeId)) { const nextNode = await this.adapter.getNode(nextNodeId); if (nextNode) { visitedNodes.set(nextNodeId, this.normalizeNode(nextNode)); await this.traverseGraph(nextNodeId, depth, direction, edgeTypes, visitedNodes, visitedEdges, currentDepth + 1); } } } } /** * Calculate relevance score */ calculateRelevance(nodeCount, edgeCount) { return Math.min(1, (nodeCount + edgeCount) / 10); } }; // src/types/index.ts var CommonEdgeType = /* @__PURE__ */ ((CommonEdgeType2) => { CommonEdgeType2["RELATED_TO"] = "RELATED_TO"; CommonEdgeType2["SIMILAR_TO"] = "SIMILAR_TO"; CommonEdgeType2["OPPOSITE_OF"] = "OPPOSITE_OF"; CommonEdgeType2["PART_OF"] = "PART_OF"; CommonEdgeType2["HAS_PART"] = "HAS_PART"; CommonEdgeType2["KNOWS"] = "KNOWS"; CommonEdgeType2["FRIEND_OF"] = "FRIEND_OF"; CommonEdgeType2["COLLEAGUE_OF"] = "COLLEAGUE_OF"; CommonEdgeType2["REPORTS_TO"] = "REPORTS_TO"; CommonEdgeType2["MANAGES"] = "MANAGES"; CommonEdgeType2["HAS_FAMILY_MEMBER"] = "HAS_FAMILY_MEMBER"; CommonEdgeType2["PARENT_OF"] = "PARENT_OF"; CommonEdgeType2["CHILD_OF"] = "CHILD_OF"; CommonEdgeType2["SPOUSE_OF"] = "SPOUSE_OF"; CommonEdgeType2["SIBLING_OF"] = "SIBLING_OF"; CommonEdgeType2["LIVES_AT"] = "LIVES_AT"; CommonEdgeType2["WORKS_AT"] = "WORKS_AT"; CommonEdgeType2["LOCATED_IN"] = "LOCATED_IN"; CommonEdgeType2["VISITED"] = "VISITED"; CommonEdgeType2["PLANS_TO_VISIT"] = "PLANS_TO_VISIT"; CommonEdgeType2["OWNS"] = "OWNS"; CommonEdgeType2["OWNED_BY"] = "OWNED_BY"; CommonEdgeType2["CREATED_BY"] = "CREATED_BY"; CommonEdgeType2["CREATED"] = "CREATED"; CommonEdgeType2["PAID_TO"] = "PAID_TO"; CommonEdgeType2["RECEIVED_FROM"] = "RECEIVED_FROM"; CommonEdgeType2["SAVED_FOR"] = "SAVED_FOR"; CommonEdgeType2["SPENT_ON"] = "SPENT_ON"; CommonEdgeType2["EARNS_FROM"] = "EARNS_FROM"; CommonEdgeType2["INVESTS_IN"] = "INVESTS_IN"; CommonEdgeType2["EMPLOYED_BY"] = "EMPLOYED_BY"; CommonEdgeType2["EMPLOYS"] = "EMPLOYS"; CommonEdgeType2["HAS_SKILL"] = "HAS_SKILL"; CommonEdgeType2["REQUIRES_SKILL"] = "REQUIRES_SKILL"; CommonEdgeType2["STUDIED_AT"] = "STUDIED_AT"; CommonEdgeType2["GRADUATED_FROM"] = "GRADUATED_FROM"; CommonEdgeType2["HAPPENED_BEFORE"] = "HAPPENED_BEFORE"; CommonEdgeType2["HAPPENED_AFTER"] = "HAPPENED_AFTER"; CommonEdgeType2["HAPPENED_DURING"] = "HAPPENED_DURING"; CommonEdgeType2["CAUSED"] = "CAUSED"; CommonEdgeType2["CAUSED_BY"] = "CAUSED_BY"; CommonEdgeType2["LIKES"] = "LIKES"; CommonEdgeType2["DISLIKES"] = "DISLIKES"; CommonEdgeType2["INTERESTED_IN"] = "INTERESTED_IN"; CommonEdgeType2["PREFERS"] = "PREFERS"; CommonEdgeType2["PARTICIPATED_IN"] = "PARTICIPATED_IN"; CommonEdgeType2["ATTENDED"] = "ATTENDED"; CommonEdgeType2["ORGANIZED"] = "ORGANIZED"; CommonEdgeType2["MENTIONED"] = "MENTIONED"; CommonEdgeType2["REFERENCED"] = "REFERENCED"; CommonEdgeType2["CONTAINS"] = "CONTAINS"; CommonEdgeType2["CONTAINED_IN"] = "CONTAINED_IN"; CommonEdgeType2["DERIVED_FROM"] = "DERIVED_FROM"; CommonEdgeType2["BASED_ON"] = "BASED_ON"; return CommonEdgeType2; })(CommonEdgeType || {}); // src/adapters/base.ts var BaseAdapter = class { config; tablePrefix; constructor(config = {}) { this.config = config; this.tablePrefix = config.tablePrefix || ""; } getTableName(table) { return this.tablePrefix ? `${this.tablePrefix}_${table}` : table; } log(_message, ..._args) { } error(message, error) { console.error(`[KnowledgeGraph Error] ${message}`, error); } }; // src/adapters/sqlite.ts import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import { eq, and, inArray, like } from "drizzle-orm"; // src/schema/index.ts var schema_exports = {}; __export(schema_exports, { edgeIndices: () => edgeIndices, edges: () => edges, graphMetadata: () => graphMetadata, nodeIndices: () => nodeIndices, nodes: () => nodes, searchIndex: () => searchIndex }); import { sqliteTable, text, integer, index, primaryKey, real } from "drizzle-orm/sqlite-core"; var nodes = sqliteTable("kg_nodes", { id: text("id").primaryKey(), type: text("type").notNull(), label: text("label").notNull(), properties: text("properties").notNull(), // JSON string confidence: real("confidence").notNull().default(1), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), sourceSessionIds: text("source_session_ids") // JSON array of session IDs }, (table) => ({ typeIdx: index("idx_nodes_type").on(table.type), labelIdx: index("idx_nodes_label").on(table.label), createdAtIdx: index("idx_nodes_created_at").on(table.createdAt) })); var edges = sqliteTable("kg_edges", { id: text("id").primaryKey(), type: text("type").notNull(), fromNodeId: text("from_node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), toNodeId: text("to_node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), properties: text("properties").notNull().default("{}"), // JSON string confidence: real("confidence").notNull().default(1), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), sourceSessionIds: text("source_session_ids") // JSON array of session IDs }, (table) => ({ typeIdx: index("idx_edges_type").on(table.type), fromNodeIdx: index("idx_edges_from_node").on(table.fromNodeId), toNodeIdx: index("idx_edges_to_node").on(table.toNodeId), fromTypeIdx: index("idx_edges_from_type").on(table.fromNodeId, table.type), toTypeIdx: index("idx_edges_to_type").on(table.toNodeId, table.type) })); var nodeIndices = sqliteTable("kg_node_indices", { indexKey: text("index_key").notNull(), nodeId: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }, (table) => ({ pk: primaryKey({ columns: [table.indexKey, table.nodeId] }), keyIdx: index("idx_node_indices_key").on(table.indexKey), nodeIdx: index("idx_node_indices_node").on(table.nodeId) })); var edgeIndices = sqliteTable("kg_edge_indices", { indexKey: text("index_key").notNull(), edgeId: text("edge_id").notNull().references(() => edges.id, { onDelete: "cascade" }), createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }, (table) => ({ pk: primaryKey({ columns: [table.indexKey, table.edgeId] }), keyIdx: index("idx_edge_indices_key").on(table.indexKey), edgeIdx: index("idx_edge_indices_edge").on(table.edgeId) })); var searchIndex = sqliteTable("kg_search_index", { term: text("term").notNull(), nodeId: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), field: text("field").notNull(), // 'label', 'property:key', etc. weight: real("weight").notNull().default(1) }, (table) => ({ pk: primaryKey({ columns: [table.term, table.nodeId, table.field] }), termIdx: index("idx_search_term").on(table.term), nodeIdx: index("idx_search_node").on(table.nodeId) })); var graphMetadata = sqliteTable("kg_graph_metadata", { key: text("key").primaryKey(), value: text("value").notNull(), updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }); // src/adapters/sqlite.ts var SQLiteAdapter = class extends BaseAdapter { db = null; drizzle = null; constructor(config = {}) { super(config); } async initialize() { try { const dbPath = this.config.connection || ":memory:"; this.db = new Database(dbPath); this.drizzle = drizzle(this.db); this.db.exec("PRAGMA foreign_keys = ON"); if (this.config.autoCreate !== false) { await this.createTables(); } this.log("SQLite adapter initialized", { path: dbPath }); } catch (error) { this.error("Failed to initialize SQLite adapter", error); throw error; } } async createTables() { if (!this.db) throw new Error("Database not initialized"); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ); CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type); CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label); CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT ); CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type); CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id); CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id); CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type); CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id) ); CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key); CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id) ); CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key); CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field) ); CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term); CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); } async execute(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query); return stmt.all(...params); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async executeUpdate(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query); return stmt.run(...params); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.db) throw new Error("Database not initialized"); return new Promise((resolve, reject) => { try { if (!this.db) throw new Error("Database not initialized"); const result = this.db.transaction(async () => { const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("Transaction rollback"); } }; return await fn(tx); })(); resolve(result); } catch (error) { reject(error); } }); } // Node operations async insertNode(node) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(nodes).values(node).returning(); const insertedNode = result[0]; if (!insertedNode) throw new Error("Failed to create node"); return this.deserializeNode(insertedNode); } async updateNode(id, updates) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.update(nodes).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(nodes.id, id)).returning(); return result[0] ? this.deserializeNode(result[0]) : null; } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; const result = await this.executeUpdate(query, [id]); return result.changes > 0; } async getNode(id) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.select().from(nodes).where(eq(nodes.id, id)).limit(1); return result[0] ? this.deserializeNode(result[0]) : null; } async getNodes(ids) { if (!this.drizzle || ids.length === 0) return []; const result = await this.drizzle.select().from(nodes).where(inArray(nodes.id, ids)); return result.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { if (!this.drizzle) throw new Error("Database not initialized"); const whereConditions = Object.entries(conditions).map(([key, value]) => { const column = nodes[key]; return eq(column, value); }); const result = await this.drizzle.select().from(nodes).where(and(...whereConditions)).limit(limit).offset(offset); return result.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(edges).values(edge).returning(); const insertedEdge = result[0]; if (!insertedEdge) throw new Error("Failed to create edge"); return this.deserializeEdge(insertedEdge); } async updateEdge(id, updates) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.update(edges).set(updates).where(eq(edges.id, id)).returning(); return result[0] ? this.deserializeEdge(result[0]) : null; } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.select().from(edges).where(eq(edges.id, id)).limit(1); return result[0] ? this.deserializeEdge(result[0]) : null; } async getEdges(ids) { if (!this.drizzle || ids.length === 0) return []; const result = await this.drizzle.select().from(edges).where(inArray(edges.id, ids)); return result.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { if (!this.drizzle) throw new Error("Database not initialized"); const whereConditions = Object.entries(conditions).map(([key, value]) => { const fieldMap = { "from_node_id": "fromNodeId", "to_node_id": "toNodeId", "created_at": "createdAt", "updated_at": "updatedAt", "source_session_ids": "sourceSessionIds" }; const schemaKey = fieldMap[key] || key; const column = edges[schemaKey]; return eq(column, value); }); const result = await this.drizzle.select().from(edges).where(and(...whereConditions)).limit(limit).offset(offset); return result.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(nodeIndices).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create node index"); return insertedIndex; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(params); return info.changes; } async getNodeIndices(indexKey) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(nodeIndices).where(eq(nodeIndices.indexKey, indexKey)); } async insertEdgeIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(edgeIndices).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create edge index"); return insertedIndex; } async deleteEdgeIndex(indexKey, edgeId) { const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`; const params = edgeId ? [indexKey, edgeId] : [indexKey]; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(params); return info.changes; } async getEdgeIndices(indexKey) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(edgeIndices).where(eq(edgeIndices.indexKey, indexKey)); } // Search operations async insertSearchIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(searchIndex).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create search index"); return insertedIndex; } async deleteSearchIndex(nodeId) { const query = `DELETE FROM kg_search_index WHERE node_id = ?`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run([nodeId]); return info.changes; } async searchNodes(term, limit = 50) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(searchIndex).where(like(searchIndex.term, `%${term}%`)).limit(limit); } // Batch operations async batchInsertNodes(nodes2) { if (!this.drizzle || nodes2.length === 0) return []; const result = await this.drizzle.insert(nodes).values(nodes2).returning(); return result.map((n) => this.deserializeNode(n)); } async batchInsertEdges(edges2) { if (!this.drizzle || edges2.length === 0) return []; const result = await this.drizzle.insert(edges).values(edges2).returning(); return result.map((e) => this.deserializeEdge(e)); } async batchDeleteNodes(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(ids); return info.changes; } async batchDeleteEdges(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(ids); return info.changes; } // Maintenance operations async vacuum() { if (!this.db) throw new Error("Database not initialized"); this.db.exec("VACUUM"); this.log("Database vacuumed"); } async getStats() { if (!this.db) throw new Error("Database not initialized"); const nodeCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_nodes").get(); const edgeCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_edges").get(); const indexCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_node_indices").get(); return { nodeCount: nodeCount.count, edgeCount: edgeCount.count, indexCount: indexCount.count }; } async close() { if (this.db) { this.db.close(); this.db = null; this.drizzle = null; this.log("Database connection closed"); } } // Helper methods deserializeNode(node) { return { ...node, properties: JSON.parse(node.properties), sourceSessionIds: node.sourceSessionIds ? JSON.parse(node.sourceSessionIds) : void 0 }; } deserializeEdge(edge) { return { ...edge, properties: JSON.parse(edge.properties), sourceSessionIds: edge.sourceSessionIds ? JSON.parse(edge.sourceSessionIds) : void 0 }; } }; // src/adapters/d1.ts var D1Adapter = class extends BaseAdapter { db = null; constructor(config) { super(config); if (config.database) { this.db = config.database; } } setDatabase(db) { this.db = db; } async initialize() { if (!this.db) { throw new Error("D1 database not provided. Use setDatabase() or pass it in config."); } if (this.config.autoCreate !== false) { await this.createTables(); } this.log("D1 adapter initialized"); } async createTables() { if (!this.db) throw new Error("Database not initialized"); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL, to_node_id TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT, FOREIGN KEY (from_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id), FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id), FOREIGN KEY (edge_id) REFERENCES kg_edges(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field), FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); } async execute(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query).bind(...params); const result = await stmt.all(); return result.results; } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.db) throw new Error("Database not initialized"); const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("Transaction rollback"); } }; try { return await fn(tx); } catch (error) { this.error("Transaction failed", error); throw error; } } // Node operations async insertNode(node) { const id = node.id || crypto.randomUUID(); const now = Date.now(); const query = ` INSERT INTO kg_nodes (id, type, label, properties, confidence, created_at, updated_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, node.type, node.label, JSON.stringify(node.properties || {}), node.confidence || 1, node.createdAt?.getTime() || now, node.updatedAt?.getTime() || now, node.sourceSessionIds ? JSON.stringify(node.sourceSessionIds) : null ]); return this.getNode(id); } async updateNode(id, updates) { const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.label !== void 0) { setClauses.push("label = ?"); params.push(updates.label); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } setClauses.push("updated_at = ?"); params.push(Date.now()); params.push(id); const query = `UPDATE kg_nodes SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getNode(id); } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; await this.execute(query, [id]); return true; } async getNode(id) { const query = `SELECT * FROM kg_nodes WHERE id = ? LIMIT 1`; const results = await this.execute(query, [id]); if (results.length === 0) return null; return this.deserializeNode(results[0]); } async getNodes(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_nodes WHERE id IN (${placeholders})`; const results = await this.execute(query, ids); return results.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; for (const [key, value] of Object.entries(conditions)) { whereClauses.push(`${key} = ?`); params.push(value); } params.push(limit, offset); const query = ` SELECT * FROM kg_nodes ${whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : ""} LIMIT ? OFFSET ? `; const results = await this.execute(query, params); return results.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { const id = edge.id || crypto.randomUUID(); const now = Date.now(); const query = ` INSERT INTO kg_edges (id, type, from_node_id, to_node_id, properties, confidence, created_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, edge.type, edge.fromNodeId, edge.toNodeId, JSON.stringify(edge.properties || {}), edge.confidence || 1, edge.createdAt?.getTime() || now, edge.sourceSessionIds ? JSON.stringify(edge.sourceSessionIds) : null ]); return this.getEdge(id); } async updateEdge(id, updates) { const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.fromNodeId !== void 0) { setClauses.push("from_node_id = ?"); params.push(updates.fromNodeId); } if (updates.toNodeId !== void 0) { setClauses.push("to_node_id = ?"); params.push(updates.toNodeId); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } params.push(id); const query = `UPDATE kg_edges SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getEdge(id); } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { const query = `SELECT * FROM kg_edges WHERE id = ? LIMIT 1`; const results = await this.execute(query, [id]); if (results.length === 0) return null; return this.deserializeEdge(results[0]); } async getEdges(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_edges WHERE id IN (${placeholders})`; const results = await this.execute(query, ids); return results.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; const fieldMap = { "fromNodeId": "from_node_id", "toNodeId": "to_node_id", "from_node_id": "from_node_id", "to_node_id": "to_node_id", "createdAt": "created_at", "updatedAt": "updated_at", "sourceSessionIds": "source_session_ids" }; for (const [key, value] of Object.entries(conditions)) { const dbKey = fieldMap[key] || key; whereClauses.push(`${dbKey} = ?`); params.push(value); } params.push(limit, offset); const query = ` SELECT * FROM kg_edges ${whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : ""} LIMIT ? OFFSET ? `; const results = await this.execute(query, params); return results.map((e) => this.deserializeEdge(e)); } // Index operat