Files
nexusAI/packages/memory-service/src/entities/index.js
2026-04-27 03:41:56 -07:00

110 lines
3.9 KiB
JavaScript

const {getDB} = require('../db');
const { parseRow } = require ('@nexusai/shared')
/******* Entities ********/
// Upsert an entity - insert or update if (name, type) already exists
function upsertEntity(name, type, notes = null, metadata = null, source = 'extraction') {
const db = getDB();
const stmt = db.prepare(`
INSERT INTO entities (name, type, notes, metadata, source, last_seen_at)
VALUES (?, ?, ?, ?, ?, unixepoch())
ON CONFLICT(name, type) DO UPDATE SET
-- First extraction wins: notes are never overwritten once set.
-- Revisit during Memory Consolidation Lifecycle (Phase 2) — once entity
-- quality scoring exists, a higher-confidence extraction should be able
-- to replace stale notes rather than being silently dropped.
notes = COALESCE(entities.notes, excluded.notes),
metadata = excluded.metadata,
mention_count = entities.mention_count + 1,
last_seen_at = unixepoch(),
updated_at = unixepoch()
`);
stmt.run(name, type, notes, metadata ? JSON.stringify(metadata) : null, source);
return getEntityByNameType(name, type);
}
// Get an entity by its ID
function getEntity(id) {
const db = getDB();
return parseRow(db.prepare(`SELECT * FROM entities WHERE id = ?`).get(id));
}
// Get all entities of a given type
function getEntitiesByType(type) {
const db = getDB();
return db.prepare(`SELECT * FROM entities WHERE type = ? ORDER BY name`).all(type).map(parseRow);
}
// Delete an entity by ID, cascades to delete relationships involving this entity
function deleteEntity(id) {
const db = getDB();
db.prepare(`DELETE FROM entities WHERE id = ?`).run(id);
}
/********* Relationships *********/
// Upsert a relationship, insert or ignore if (from_id, to_id, label) already exists
function upsertRelationship(fromId, toId, label, notes = null, metadata = null) {
const db = getDB();
const stmt = db.prepare(`
INSERT INTO relationships (from_id, to_id, label, notes, metadata)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(from_id, to_id, label) DO UPDATE SET
mention_count = relationships.mention_count + 1,
-- First extraction wins for notes — same policy as entities.
notes = COALESCE(relationships.notes, excluded.notes)
`);
stmt.run(fromId, toId, label, notes, metadata ? JSON.stringify(metadata) : null);
return getRelationship(fromId, toId, label);
}
// Retrieve a relationship by (from_id, to_id, label)
function getRelationship(fromId, toId, label) {
const db = getDB();
return parseRow(
db.prepare(`SELECT * FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`)
.get(fromId, toId, label)
);
}
// Retrieves an entity by its unique (name, type) combination
function getEntityByNameType(name, type) {
const db = getDB();
return parseRow(db.prepare(`SELECT * FROM entities WHERE name = ? AND type = ?`).get(name, type));
}
// Retrive all relationships originating from a given entity
function getOutboundRelationships(entityId) {
const db = getDB();
return db.prepare(`SELECT * FROM relationships WHERE from_id = ?`).all(entityId).map(parseRow);
}
// Delete a specific relationship by (from_id, to_id, label)
function deleteRelationship(fromId, toId, label) {
const db = getDB();
db.prepare(`DELETE FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`).run(fromId, toId, label);
}
function linkEntityToEpisode(entityId, episodeId) {
const db = getDB();
db.prepare(`
INSERT OR IGNORE INTO entity_episodes (entity_id, episode_id)
VALUES (?, ?)
`).run(entityId, episodeId);
}
module.exports = {
upsertEntity,
getEntity,
getEntitiesByType,
getEntityByNameType,
deleteEntity,
linkEntityToEpisode,
upsertRelationship,
getRelationship,
getOutboundRelationships,
deleteRelationship
}