diff --git a/src/memory/README.md b/src/memory/README.md index 66bdbb41..cbd631f6 100644 --- a/src/memory/README.md +++ b/src/memory/README.md @@ -36,15 +36,25 @@ Observations are discrete pieces of information about an entity. They are: - Attached to specific entities - Can be added or removed independently - Should be atomic (one fact per observation) +- Must include a Unix timestamp Example: ```json { "entityName": "John_Smith", "observations": [ - "Speaks fluent Spanish", - "Graduated in 2019", - "Prefers morning meetings" + { + "content": "Speaks fluent Spanish", + "timestamp": 1734940800 + }, + { + "content": "Graduated in 2019", + "timestamp": 1734940800 + }, + { + "content": "Prefers morning meetings", + "timestamp": 1734940800 + } ] } ``` @@ -171,7 +181,8 @@ Follow these steps for each interaction: - If any new information was gathered during the interaction, update your memory as follows: a) Create entities for recurring organizations, people, and significant events b) Connect them to the current entities using relations - b) Store facts about them as observations + c) Store facts about them as observations + d) IMPORTANT: Always include the Unix timestamp with each observation (e.g., for December 17, 2024, use timestamp: 1734940800) ``` ## License diff --git a/src/memory/index.ts b/src/memory/index.ts index 0117c920..3cee13dc 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -10,16 +10,37 @@ import { promises as fs } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; - // Define the path to the JSONL file, you can change this to your desired local path const __dirname = path.dirname(fileURLToPath(import.meta.url)); const MEMORY_FILE_PATH = path.join(__dirname, 'memory.json'); +// Define the TimeManager interface +interface TimeManager { + getCurrentTimestamp(): number; +} + +// Implementation that standardizes all timestamps +class UnixTimeManager implements TimeManager { + private lastTimestamp: number = 0; + +// Reducing latency + getCurrentTimestamp(): number { + const now = Math.floor(Date.now() / 1000); + if (now !== this.lastTimestamp) { + this.lastTimestamp = now; + } + return this.lastTimestamp; + } +} + +// Create a single instance to use throughout the app +const timeManager = new UnixTimeManager(); + // We are storing our memory using entities, relations, and observations in a graph structure interface Entity { name: string; entityType: string; - observations: string[]; + observations: { content: string; timestamp: number }[]; // Updated to include timestamp } interface Relation { @@ -35,6 +56,12 @@ interface KnowledgeGraph { // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph class KnowledgeGraphManager { + private timeManager: TimeManager; + + constructor(timeManager: TimeManager) { + this.timeManager = timeManager; + } + private async loadGraph(): Promise { try { const data = await fs.readFile(MEMORY_FILE_PATH, "utf-8"); @@ -81,20 +108,24 @@ class KnowledgeGraphManager { return newRelations; } - async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: string[] }[]> { - const graph = await this.loadGraph(); - const results = observations.map(o => { - const entity = graph.entities.find(e => e.name === o.entityName); - if (!entity) { - throw new Error(`Entity with name ${o.entityName} not found`); - } - const newObservations = o.contents.filter(content => !entity.observations.includes(content)); - entity.observations.push(...newObservations); - return { entityName: o.entityName, addedObservations: newObservations }; - }); - await this.saveGraph(graph); - return results; - } + async addObservations(observations: { entityName: string; contents: string[] }[]): Promise<{ entityName: string; addedObservations: { content: string; timestamp: number }[] }[]> { + const graph = await this.loadGraph(); + const timestamp = this.timeManager.getCurrentTimestamp(); + const results = observations.map(o => { + const entity = graph.entities.find(e => e.name === o.entityName); + if (!entity) { + throw new Error(`Entity with name ${o.entityName} not found`); + } + const newObservations = o.contents.map(content => ({ + content, + timestamp + })); + entity.observations.push(...newObservations); + return { entityName: o.entityName, addedObservations: newObservations }; + }); + await this.saveGraph(graph); + return results; +} async deleteEntities(entityNames: string[]): Promise { const graph = await this.loadGraph(); @@ -108,7 +139,7 @@ class KnowledgeGraphManager { deletions.forEach(d => { const entity = graph.entities.find(e => e.name === d.entityName); if (entity) { - entity.observations = entity.observations.filter(o => !d.observations.includes(o)); + entity.observations = entity.observations.filter(o => !d.observations.includes(o.content)); } }); await this.saveGraph(graph); @@ -136,7 +167,7 @@ class KnowledgeGraphManager { const filteredEntities = graph.entities.filter(e => e.name.toLowerCase().includes(query.toLowerCase()) || e.entityType.toLowerCase().includes(query.toLowerCase()) || - e.observations.some(o => o.toLowerCase().includes(query.toLowerCase())) + e.observations.some(o => o.content.toLowerCase().includes(query.toLowerCase())) ); // Create a Set of filtered entity names for quick lookup @@ -178,9 +209,7 @@ class KnowledgeGraphManager { } } -const knowledgeGraphManager = new KnowledgeGraphManager(); - - +const knowledgeGraphManager = new KnowledgeGraphManager(timeManager); // The server instance and tools exposed to Claude const server = new Server({ name: "memory-server", @@ -189,7 +218,7 @@ const server = new Server({ capabilities: { tools: {}, }, - },); + }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { @@ -209,7 +238,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { entityType: { type: "string", description: "The type of the entity" }, observations: { type: "array", - items: { type: "string" }, + items: { + type: "object", + properties: { + content: { type: "string", description: "An observation content" }, + timestamp: { type: "number", description: "The timestamp of the observation" } + }, + required: ["timestamp"], + }, description: "An array of observation contents associated with the entity" }, }, @@ -256,11 +292,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { entityName: { type: "string", description: "The name of the entity to add the observations to" }, contents: { type: "array", - items: { type: "string" }, - description: "An array of observation contents to add" + items: { + type: "object", + properties: { + content: { type: "string", description: "An observation content" }, + timestamp: { type: "number", description: "The timestamp of the observation" } + }, + required: ["content", "timestamp"] + }, + description: "An array of observations with content and timestamp" }, }, - required: ["entityName", "contents"], + required: ["entityName"], }, }, },