Memory / history fixes
This commit is contained in:
37
src/llm.ts
37
src/llm.ts
@@ -171,8 +171,8 @@ class LLM {
|
||||
let abort = () => {};
|
||||
return Object.assign(new Promise<string>(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(<Memory[]>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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user