Patched memory merging
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/ai-utils",
|
"name": "@ztimson/ai-utils",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"description": "AI Utility library",
|
"description": "AI Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {AiTool} from './tools.ts';
|
|||||||
import {KDPoint, KDTree} from './kd-tree.ts';
|
import {KDPoint, KDTree} from './kd-tree.ts';
|
||||||
import {escapeRegex} from '@ztimson/utils';
|
import {escapeRegex} from '@ztimson/utils';
|
||||||
|
|
||||||
const MERGE_THRESHOLD = 0.88;
|
const MERGE_THRESHOLD = 0.12;
|
||||||
const PENDING_HEADING = '## Pending';
|
const PENDING_HEADING = '## Pending';
|
||||||
const GENERIC_TEMPLATE = `# {{Title}}
|
const GENERIC_TEMPLATE = `# {{Title}}
|
||||||
|
|
||||||
@@ -191,13 +191,13 @@ export type MemoryOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MemoryManager {
|
export class MemoryManager {
|
||||||
private recentlyTouched = new Map<string, number>();
|
private mergeLock: Promise<any> = Promise.resolve();
|
||||||
|
|
||||||
private queues = new Map<string, {
|
private queues = new Map<string, {
|
||||||
dirty: boolean,
|
dirty: boolean,
|
||||||
request: {abort?: () => void} | null,
|
request: {abort?: () => void} | null,
|
||||||
task: Promise<void>,
|
task: Promise<void>,
|
||||||
}>();
|
}>();
|
||||||
|
private recentlyTouched = new Map<string, number>();
|
||||||
|
|
||||||
tools = {
|
tools = {
|
||||||
forget: (memories: Memory[] | MemoryCache): AiTool => ({
|
forget: (memories: Memory[] | MemoryCache): AiTool => ({
|
||||||
@@ -351,10 +351,6 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
|||||||
return memories.map(m => ({name: m.name, description: m.description}));
|
return memories.map(m => ({name: m.name, description: m.description}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find the nearest node above the similarity threshold and fold the smaller/less-connected one into
|
|
||||||
* the other. Journals are exempt — they're partitioned by date, not topic, and merging across weeks
|
|
||||||
* would wreck the timeline. Returns 'merged' if `node` absorbed another (caller should re-run the doc
|
|
||||||
* agent), 'absorbed' if `node` itself got folded away (caller should stop touching it), or null. */
|
|
||||||
private async checkMerge(node: Memory, memories: Memory[] | MemoryCache, options: LLMRequest, threshold = MERGE_THRESHOLD): Promise<Memory | null> {
|
private async checkMerge(node: Memory, memories: Memory[] | MemoryCache, options: LLMRequest, threshold = MERGE_THRESHOLD): Promise<Memory | null> {
|
||||||
if (!node.embedding?.length || node.name.startsWith('Journal/')) return null;
|
if (!node.embedding?.length || node.name.startsWith('Journal/')) return null;
|
||||||
const store = this.access(memories);
|
const store = this.access(memories);
|
||||||
@@ -404,8 +400,9 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
|||||||
do {
|
do {
|
||||||
entry.dirty = false;
|
entry.dirty = false;
|
||||||
await this.docAgent(current, store.list, options, entry);
|
await this.docAgent(current, store.list, options, entry);
|
||||||
const merged = await this.checkMerge(current, memories, options);
|
this.mergeLock = this.mergeLock.then(() => this.checkMerge(current, memories, options));
|
||||||
if (merged) { current = merged; entry.dirty = true; }
|
const merged = await this.mergeLock;
|
||||||
|
if(merged) current = merged;
|
||||||
} while (entry.dirty);
|
} while (entry.dirty);
|
||||||
})().finally(() => {
|
})().finally(() => {
|
||||||
this.queues.delete(key);
|
this.queues.delete(key);
|
||||||
@@ -432,11 +429,9 @@ ${ghosts.length ? `${ghosts.map(g => `- ${g}: (Ghost)`).join('\n')}` : ''}`,
|
|||||||
If it has a "## Pending" section, fold all new material into the appropriate part, resolve overlap, then remove the section entirely. If no section, just tidy per the rules below.
|
If it has a "## Pending" section, fold all new material into the appropriate part, resolve overlap, then remove the section entirely. If no section, just tidy per the rules below.
|
||||||
|
|
||||||
Use this loose structure, adapting headings to what the content needs:
|
Use this loose structure, adapting headings to what the content needs:
|
||||||
|
\`\`\`markdown
|
||||||
# Title
|
${GENERIC_TEMPLATE}
|
||||||
## Summary
|
\`\`\`
|
||||||
## Details
|
|
||||||
## Related
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- Contradictions: newer facts always win — delete outdated statements entirely
|
- Contradictions: newer facts always win — delete outdated statements entirely
|
||||||
@@ -483,11 +478,9 @@ ${currentBody}
|
|||||||
system: `You are a knowledge base editor merging two overlapping Obsidian documents into one. Newer facts win on contradiction.
|
system: `You are a knowledge base editor merging two overlapping Obsidian documents into one. Newer facts win on contradiction.
|
||||||
|
|
||||||
Structure loosely:
|
Structure loosely:
|
||||||
|
\`\`\`markdown
|
||||||
# Title
|
${GENERIC_TEMPLATE}
|
||||||
## Summary
|
\`\`\`
|
||||||
## Details
|
|
||||||
## Related
|
|
||||||
|
|
||||||
Combine both documents, resolve duplication and contradictions.
|
Combine both documents, resolve duplication and contradictions.
|
||||||
|
|
||||||
@@ -617,16 +610,16 @@ ${stripHeader(b.content)}
|
|||||||
touched.push(node);
|
touched.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const node of touched) {
|
await Promise.all(touched.map(async node => {
|
||||||
const [e] = await this.llm.embedding(`${node.description}\n\n${stripHeader(node.content)}`.trim());
|
const [e] = await this.llm.embedding(`${node.description}\n\n${stripHeader(node.content)}`.trim());
|
||||||
if (e) node.embedding = e.embedding;
|
if (e) node.embedding = e.embedding;
|
||||||
this.touch(node.name);
|
this.touch(node.name);
|
||||||
}
|
}));
|
||||||
|
|
||||||
if (touched.length) {
|
if (touched.length) {
|
||||||
store.commit();
|
store.commit();
|
||||||
(pending as any).content = `Saved to ${touched.map(n => `[[${n.name}]]`).join(', ')}`;
|
(pending as any).content = `Saved to ${touched.map(n => `[[${n.name}]]`).join(', ')}`;
|
||||||
await Promise.all(touched.map(node => this.reconcile(node, memories, options).catch(() => {})));
|
Promise.all(touched.map(node => this.reconcile(node, memories, options).catch(() => {})));
|
||||||
} else {
|
} else {
|
||||||
(pending as any).content = 'Nothing worth remembering.';
|
(pending as any).content = 'Nothing worth remembering.';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user