diff --git a/main.mjs b/main.mjs index 1403f9e..fb48ec6 100644 --- a/main.mjs +++ b/main.mjs @@ -17,5 +17,9 @@ const skills = [{ }]; const history = [], memory = []; -console.log(await ai.language.ask('Can you tell me how to use momentum?', {history, skills})); -console.log(history, memory); +await ai.language.ask('My favorite color is red', {history, memory}); +await ai.language.updateMemory(history, memory); + +history.splice(0, history.length); +console.log(await ai.language.ask('Whats my favorite color?', {history, memory})); +console.log(history); diff --git a/package.json b/package.json index a88a25d..e7befb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/ai-utils", - "version": "1.0.2", + "version": "1.0.3", "description": "AI Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/llm.ts b/src/llm.ts index 9f4a61d..240f878 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -171,8 +171,8 @@ class LLM { let abort = () => {}; return Object.assign(new Promise(async res => { let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || []; - const prompts: string[] = [options.system || this.ai.options.llm?.system || '']; - if(!options.history) options.history = []; + const prompts: string[] = []; + let history = options.history || []; // MCP const mcp = options.mcp || this.ai.options?.llm?.mcp; @@ -192,30 +192,33 @@ class LLM { // Memory if(options.memory) { - const relevant = await this.memoryManager.recollect(message, options.memory); - if(relevant.length) { - const context = relevant.map(m => `### ${m.name}\n${m.content}`).join('\n\n'); - options.history.push({ - id: 'auto_recall_' + Math.random().toString(), role: 'tool', name: 'recall', args: {}, - content: `Knowledge Documents:\n\n${context}` - }); - } - prompts.unshift('You have access to a knowledge base. Relevant documents are injected automatically before each message. Use this knowledge to inform your responses.'); + const relevant = await this.memoryManager.recollect(message, options.memory, 1); + prompts.unshift(`You have access to the following memory files: +${options.memory.map(m => `- ${m.name}: ${m.description}`).join('\n')} +${relevant.length ? ` +The closest memory has been added primitively: +\`\`\` +Name: ${relevant[0].name} +Description: ${relevant[0].description} +${relevant[0].content} +\`\`\` +`: ''}`.trim()); + tools.push(this.memoryManager.tools.read(options.memory)); } + prompts.unshift(options.system || this.ai.options.llm?.system || ''); const resp = await this.models[m].ask(message, {...options, tools, system: prompts.filter(Boolean).join('\n\n')}); // Trim memory injections from history if(options.memory) { - options.history.splice(0, options.history.length, ...options.history.filter(h => - h.role !== 'tool' || h.name !== 'recall')); + history.splice(0, history.length, ...history.filter(h => h.role !== 'tool' || h.name !== 'recall')); } // Auto-memorize before compressing - if(options.compress) { - if(options.memory) await this.memoryManager.memorize(options.history, options.memory, options); - const compressed = await this.compressHistory(options.history, options.compress.max, options.compress.min, options); - options.history.splice(0, options.history.length, ...compressed); + if(options.compress && this.estimateTokens(history) >= options.compress.max) { + if(options.memory) await this.memoryManager.memorize(history, options.memory, options); + const compressed = await this.compressHistory(history, options.compress.max, options.compress.min, options); + if(options.history) options.history.splice(0, options.history.length, ...compressed); } return res(resp); diff --git a/src/memory.ts b/src/memory.ts index 5669dbf..46bdc8a 100644 --- a/src/memory.ts +++ b/src/memory.ts @@ -1,5 +1,6 @@ // memory.ts import {LLMRequest, LLMMessage} from './llm.ts'; +import {AiTool} from './tools.ts'; /** Background information the AI will be fed as a knowledge document */ export type Memory = { @@ -25,9 +26,9 @@ export type MemoryCollection = { export class MemoryManager { tools = { - edit: (memory: Memory) => ({ - name: 'edit', - description: 'Edit a memory. Omit start/end to append. Pass start only to replace from that line on. Pass start+end to replace a specific range. start=0 replaces the whole document.', + edit: (memory: Memory): AiTool => ({ + name: 'edit_memory', + description: 'Edit a memory. Omit start/end to append. Pass start only to replace from that line on (Note line 0 = first line of content / line AFTER description). Pass start+end to replace a specific range. start=0 replaces the whole document. Returns updated document', args: { content: {type: 'string', description: 'New content', required: true}, start: {type: 'number', description: 'First line to replace (0-indexed, inclusive). Omit to append.'}, @@ -40,15 +41,15 @@ export class MemoryManager { else if(args.end === undefined) lines.splice(args.start, lines.length - args.start, ...newLines); else lines.splice(args.start, args.end - args.start + 1, ...newLines); memory.content = lines.join('\n'); - return `Updated memory:\n${memory.content}`; + return memory.content; } }), - extract: (pools: MemoryCollection[]) => ({ - name: 'extract', + extract: (pools: MemoryCollection[]): AiTool => ({ + name: 'extract_facts', description: 'Extract a list of facts to group into a single memory', args: { name: {type: 'string', description: 'Exact name of an existing memory, or a new name if none fits ([pro]nouns only)', required: true}, - description: {type: 'string', description: 'One sentence description of the memory subject, only required if new'}, + description: {type: 'string', description: 'One sentence description of the memory subject', required: true}, facts: {type: 'string', description: 'Comma separated list of extracted facts', required: true}, }, fn: (args: any) => { @@ -59,8 +60,8 @@ export class MemoryManager { }); return 'Success'; }}), - read: (memories: Memory[]) => ({ - name: 'read', + read: (memories: Memory[]): AiTool => ({ + name: 'read_memory', description: 'Read entire memory', args: { name: {type: 'string', description: 'Exact memory name', required: true}, @@ -96,7 +97,7 @@ Rules: - DO NOT extract greetings, pleasantries or generic exchanges - If nothing worth remembering was said, call NO tools -For each fact decide whether it belongs in an existing document or needs a new one, then call the \`extract\` tool. +For each fact decide whether it belongs in an existing document or needs a new one, then call the \`extract_facts\` tool. Existing documents:\n${existingDocs || 'None yet.'}`, tools: [this.tools.extract(pools)] @@ -120,18 +121,17 @@ Existing documents:\n${existingDocs || 'None yet.'}`, { model: this.model || options.model, temperature: 0.2, - system: `You are a document editor. Merge the users list of facts into the following document using the \`edit\` tool; call it as many times as necessary. - -Name: ${mem.name} -Description: ${mem.description} -${mem.content}`, + system: `You are a document editor. Merge the users list of facts into the following document using the \`edit_memory\` tool; call it as many times as necessary: +\`\`\` +${mem.content} +\`\`\``, tools: [this.tools.edit(mem)] } ); if(isNew || mem.description !== existing?.description) { - const [e] = await this.llm.embedding(mem.description); - mem.embedding = e.embedding; + const e = await this.llm.embedding(mem.description); + mem.embedding = e?.[0]?.embedding; } if(isNew) memories.push(mem);