mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Add modular memory service API scaffold
This commit is contained in:
1 parent
8055132f32
commit
9c2524540c
9 files changed
+311
No files matched your search
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @returns {{ get: (key: string) => unknown, set: (key: string, value: unknown) => void, delete: (key: string) => boolean, clear: () => void }}
|
||||
*/
|
||||
export function createMemoryCache() {
|
||||
const store = new Map();
|
||||
|
||||
return {
|
||||
get(key) {
|
||||
return store.get(key);
|
||||
},
|
||||
set(key, value) {
|
||||
store.set(key, value);
|
||||
},
|
||||
delete(key) {
|
||||
return store.delete(key);
|
||||
},
|
||||
clear() {
|
||||
store.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function fallbackEmbedding(text) {
|
||||
const chars = [...text];
|
||||
if (chars.length === 0) return [0];
|
||||
const mean = chars.reduce((sum, char) => sum + char.codePointAt(0), 0) / chars.length;
|
||||
return [Number(mean.toFixed(4)), chars.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {{ embedText?: (value: string) => Promise<number[]> | number[] }} [options]
|
||||
* @returns {Promise<number[]>}
|
||||
*/
|
||||
export async function embedText(text, options = {}) {
|
||||
if (typeof options.embedText === 'function') {
|
||||
return options.embedText(text);
|
||||
}
|
||||
return fallbackEmbedding(text);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { createMemoryCache } from './cache.js';
|
||||
import { embedText } from './embedder.js';
|
||||
import { normalizeEvents } from './ingestor.js';
|
||||
import { selectPrunableMemories } from './pruner.js';
|
||||
import { filterAndRankMemories } from './retriever.js';
|
||||
import { startScheduler } from './scheduler.js';
|
||||
import { createMemoryRecord } from './schema.js';
|
||||
import { summarizeEvents } from './summarizer.js';
|
||||
|
||||
const memoryStore = new Map();
|
||||
const cache = createMemoryCache();
|
||||
|
||||
/**
|
||||
* Ingests agent events and stores them as memory records.
|
||||
*
|
||||
* @param {import('./schema.js').MemoryEvent[]} events - Ordered list of raw events to persist.
|
||||
* @param {{ maxSummaryLength?: number, embedText?: (value: string) => Promise<number[]> | number[] }} [options] - Optional ingest tuning.
|
||||
* @returns {Promise<import('./schema.js').MemoryRecord[]>} Stored memory records with generated IDs/embeddings.
|
||||
*/
|
||||
export async function ingestEvents(events, options = {}) {
|
||||
const normalizedEvents = normalizeEvents(events);
|
||||
const records = [];
|
||||
|
||||
for (const event of normalizedEvents) {
|
||||
const summary = summarizeEvents([event], options);
|
||||
const embedding = await embedText(`${summary}\n${event.content}`, options);
|
||||
const record = createMemoryRecord({
|
||||
agent_id: event.agent_id,
|
||||
content: event.content,
|
||||
summary,
|
||||
tags: event.tags,
|
||||
metadata: event.metadata,
|
||||
embedding,
|
||||
created_at: event.timestamp,
|
||||
updated_at: event.timestamp,
|
||||
});
|
||||
|
||||
memoryStore.set(record.id, record);
|
||||
records.push(record);
|
||||
}
|
||||
|
||||
cache.clear();
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves top-k memories for an agent query.
|
||||
*
|
||||
* @param {{ agent_id: string, query?: string, tags?: string[], timeframe?: { from?: number, to?: number }, k?: number }} params - Retrieval filter and ranking inputs.
|
||||
* @returns {Promise<import('./schema.js').MemoryRecord[]>} Sorted list of relevant memories.
|
||||
*/
|
||||
export async function getRelevantMemories(params) {
|
||||
const cacheKey = JSON.stringify(params);
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const ranked = filterAndRankMemories([...memoryStore.values()], params);
|
||||
cache.set(cacheKey, ranked);
|
||||
return ranked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a memory pruning pass and removes unpinned stale records.
|
||||
*
|
||||
* @param {{ retentionMs?: number, now?: number, scheduleEveryMs?: number }} [options] - Prune configuration; if scheduleEveryMs is set, returns a scheduler handle.
|
||||
* @returns {Promise<{ pruned: import('./schema.js').MemoryRecord[], scheduler?: { stop: () => void } }>} Pruned records and optional scheduler handle.
|
||||
*/
|
||||
export async function runPruneCycle(options = {}) {
|
||||
const retentionMs = options.retentionMs ?? (1000 * 60 * 60 * 24 * 30);
|
||||
const prunable = selectPrunableMemories([...memoryStore.values()], {
|
||||
retentionMs,
|
||||
now: options.now,
|
||||
});
|
||||
|
||||
for (const memory of prunable) {
|
||||
memoryStore.delete(memory.id);
|
||||
}
|
||||
|
||||
cache.clear();
|
||||
|
||||
const result = { pruned: prunable };
|
||||
if (Number.isFinite(options.scheduleEveryMs) && options.scheduleEveryMs > 0) {
|
||||
result.scheduler = startScheduler(() => runPruneCycle({ retentionMs }), {
|
||||
intervalMs: options.scheduleEveryMs,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins a memory to prevent pruning and boost retrieval ranking.
|
||||
*
|
||||
* @param {string} id - Memory record identifier.
|
||||
* @returns {import('./schema.js').MemoryRecord | null} Updated memory record or null when not found.
|
||||
*/
|
||||
export function pinMemory(id) {
|
||||
const memory = memoryStore.get(id);
|
||||
if (!memory) return null;
|
||||
|
||||
const updated = { ...memory, pinned: true, updated_at: Date.now() };
|
||||
memoryStore.set(id, updated);
|
||||
cache.clear();
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a pin from a memory so it can be pruned normally.
|
||||
*
|
||||
* @param {string} id - Memory record identifier.
|
||||
* @returns {import('./schema.js').MemoryRecord | null} Updated memory record or null when not found.
|
||||
*/
|
||||
export function unpinMemory(id) {
|
||||
const memory = memoryStore.get(id);
|
||||
if (!memory) return null;
|
||||
|
||||
const updated = { ...memory, pinned: false, updated_at: Date.now() };
|
||||
memoryStore.set(id, updated);
|
||||
cache.clear();
|
||||
return updated;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { normalizeEvent } from './schema.js';
|
||||
|
||||
/**
|
||||
* @param {import('./schema.js').MemoryEvent[]} events
|
||||
* @returns {import('./schema.js').MemoryEvent[]}
|
||||
*/
|
||||
export function normalizeEvents(events) {
|
||||
return events.map(normalizeEvent);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @param {import('./schema.js').MemoryRecord[]} memories
|
||||
* @param {{ retentionMs: number, now?: number }} options
|
||||
* @returns {import('./schema.js').MemoryRecord[]}
|
||||
*/
|
||||
export function selectPrunableMemories(memories, options) {
|
||||
const now = options.now ?? Date.now();
|
||||
const cutoff = now - options.retentionMs;
|
||||
|
||||
return memories.filter((memory) => !memory.pinned && memory.updated_at < cutoff);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @param {import('./schema.js').MemoryRecord[]} memories
|
||||
* @param {{ agent_id: string, query?: string, tags?: string[], timeframe?: { from?: number, to?: number }, k?: number }} params
|
||||
* @returns {import('./schema.js').MemoryRecord[]}
|
||||
*/
|
||||
export function filterAndRankMemories(memories, params) {
|
||||
const { agent_id, query = '', tags = [], timeframe, k = 10 } = params;
|
||||
const loweredQuery = query.toLowerCase();
|
||||
|
||||
const filtered = memories.filter((memory) => {
|
||||
if (memory.agent_id !== agent_id) return false;
|
||||
|
||||
const inTimeframe = !timeframe
|
||||
|| ((timeframe.from == null || memory.created_at >= timeframe.from)
|
||||
&& (timeframe.to == null || memory.created_at <= timeframe.to));
|
||||
|
||||
if (!inTimeframe) return false;
|
||||
|
||||
if (tags.length > 0) {
|
||||
const memoryTags = new Set(memory.tags);
|
||||
const matchesAllTags = tags.every((tag) => memoryTags.has(tag));
|
||||
if (!matchesAllTags) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return filtered
|
||||
.map((memory) => {
|
||||
const haystack = `${memory.summary} ${memory.content}`.toLowerCase();
|
||||
const queryScore = loweredQuery && haystack.includes(loweredQuery) ? 1 : 0;
|
||||
const pinBoost = memory.pinned ? 1 : 0;
|
||||
const recencyScore = memory.updated_at;
|
||||
|
||||
return {
|
||||
memory,
|
||||
score: (queryScore * 10) + (pinBoost * 5) + recencyScore,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, k)
|
||||
.map((entry) => entry.memory);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @param {() => Promise<unknown> | unknown} task
|
||||
* @param {{ intervalMs: number }} options
|
||||
* @returns {{ stop: () => void }}
|
||||
*/
|
||||
export function startScheduler(task, options) {
|
||||
const handle = setInterval(() => {
|
||||
void task();
|
||||
}, options.intervalMs);
|
||||
|
||||
return {
|
||||
stop() {
|
||||
clearInterval(handle);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @typedef {Object} MemoryEvent
|
||||
* @property {string} agent_id
|
||||
* @property {string} content
|
||||
* @property {number} [timestamp]
|
||||
* @property {string[]} [tags]
|
||||
* @property {Record<string, unknown>} [metadata]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MemoryRecord
|
||||
* @property {string} id
|
||||
* @property {string} agent_id
|
||||
* @property {string} summary
|
||||
* @property {string} content
|
||||
* @property {number} created_at
|
||||
* @property {number} updated_at
|
||||
* @property {string[]} tags
|
||||
* @property {number[] | null} embedding
|
||||
* @property {boolean} pinned
|
||||
* @property {Record<string, unknown>} metadata
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {MemoryEvent} event
|
||||
* @returns {MemoryEvent}
|
||||
*/
|
||||
export function normalizeEvent(event) {
|
||||
return {
|
||||
...event,
|
||||
timestamp: Number.isFinite(event?.timestamp) ? event.timestamp : Date.now(),
|
||||
tags: Array.isArray(event?.tags) ? event.tags : [],
|
||||
metadata: event?.metadata && typeof event.metadata === 'object' ? event.metadata : {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Partial<MemoryRecord>} record
|
||||
* @returns {MemoryRecord}
|
||||
*/
|
||||
export function createMemoryRecord(record) {
|
||||
const now = Date.now();
|
||||
return {
|
||||
id: record.id || crypto.randomUUID(),
|
||||
agent_id: record.agent_id || '',
|
||||
summary: record.summary || '',
|
||||
content: record.content || '',
|
||||
created_at: Number.isFinite(record.created_at) ? record.created_at : now,
|
||||
updated_at: Number.isFinite(record.updated_at) ? record.updated_at : now,
|
||||
tags: Array.isArray(record.tags) ? record.tags : [],
|
||||
embedding: Array.isArray(record.embedding) ? record.embedding : null,
|
||||
pinned: Boolean(record.pinned),
|
||||
metadata: record.metadata && typeof record.metadata === 'object' ? record.metadata : {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @param {import('./schema.js').MemoryEvent[]} events
|
||||
* @param {{ maxSummaryLength?: number }} [options]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function summarizeEvents(events, options = {}) {
|
||||
const maxSummaryLength = options.maxSummaryLength ?? 280;
|
||||
const joined = events.map((event) => event.content).join(' ').trim();
|
||||
if (!joined) return '';
|
||||
return joined.length > maxSummaryLength ? `${joined.slice(0, maxSummaryLength - 1)}…` : joined;
|
||||
}
|
||||
Reference in new issue
Block a user