Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aa6cdf329 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ztimson/ai-utils",
|
||||
"version": "1.2.13",
|
||||
"version": "1.3.0",
|
||||
"description": "AI Utility library",
|
||||
"author": "Zak Timson",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -3,8 +3,6 @@ export * from './antrhopic';
|
||||
export * from './audio';
|
||||
export * from './llm';
|
||||
export * from './memory';
|
||||
export * from './memory-cache';
|
||||
export * from './memory-graph';
|
||||
export * from './open-ai';
|
||||
export * from './provider';
|
||||
export * from './tools';
|
||||
|
||||
164
src/llm.ts
164
src/llm.ts
@@ -1,17 +1,31 @@
|
||||
import {snakeCase} from '@ztimson/utils';
|
||||
import {AbortablePromise, Ai} from './ai.ts';
|
||||
import {Anthropic} from './antrhopic.ts';
|
||||
import {MemoryCache} from './memory-cache.ts';
|
||||
import {OpenAi} from './open-ai.ts';
|
||||
import {LLMProvider} from './provider.ts';
|
||||
import {AiTool, AiToolArg} from './tools.ts';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {dirname, join} from 'path';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {Memory, MemoryManager} from './memory.ts';
|
||||
import {Memory, MemoryCache, MemoryManager, MemoryOptions} from './memory.ts';
|
||||
|
||||
export type AnthropicConfig = {proto: 'anthropic', token: string};
|
||||
export type OpenAiConfig = {proto: 'openai', host?: string, token: string};
|
||||
|
||||
export type Agent = {
|
||||
name: string;
|
||||
description?: string;
|
||||
model?: string | null;
|
||||
temperature?: number;
|
||||
system: string;
|
||||
delegate?: boolean;
|
||||
skills?: Skill[] | null;
|
||||
tools?: AiTool[] | null;
|
||||
mcp?: McpServer[] | null;
|
||||
/** Explicit whitelist of agents this agent may delegate to. Default: none - must opt-in, self is always excluded */
|
||||
agents?: string[] | null;
|
||||
}
|
||||
|
||||
export type LLMMessage = {
|
||||
/** Message originator */
|
||||
role: 'assistant' | 'system' | 'user';
|
||||
@@ -56,13 +70,17 @@ export type LLMRequest = {
|
||||
/** Compress old messages in the chat to free up context */
|
||||
compress?: {max: number; min: number};
|
||||
/** User's memory documents - RAG injected automatically each turn */
|
||||
memory?: Memory[] | MemoryCache;
|
||||
memory?: Memory[] | MemoryCache | MemoryOptions;
|
||||
/** Model to use for memory operations */
|
||||
memoryModel?: string;
|
||||
/** Skill documents the AI can browse and read on demand */
|
||||
skills?: Skill[];
|
||||
/** MCP servers to connect and expose as tools */
|
||||
mcp?: McpServer[];
|
||||
/** Subagents exposed as delegatable/wrapped tools */
|
||||
agents?: Agent[];
|
||||
/** @internal recursion guard for nested agent delegation */
|
||||
_agentDepth?: number;
|
||||
}
|
||||
|
||||
export type McpServer = {
|
||||
@@ -83,6 +101,7 @@ export type Skill = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MAX_AGENT_DEPTH = 5;
|
||||
|
||||
class LLM {
|
||||
private memoryManager!: MemoryManager;
|
||||
@@ -100,6 +119,60 @@ class LLM {
|
||||
this.memoryManager = new MemoryManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap agents as tools. Nested delegation is opt-in only (empty by default, like
|
||||
* tools/skills/mcp) and an agent can never call itself even if explicitly whitelisted.
|
||||
* Delegate results are queued in `pending` and spliced into history by `ask()` after
|
||||
* the provider's own end-of-turn history sync has already run.
|
||||
*/
|
||||
private setupAgent(agents: Agent[] = [], allAgents: Agent[], pending: Map<string, {resp: string, subHistory: LLMMessage[]}[]>, aborts: (() => void)[], depth = 0): AiTool[] {
|
||||
return agents.map(a => {
|
||||
const toolName = `${a.delegate ? '' : 'sub'}agent_${snakeCase(a.name)}`;
|
||||
return {
|
||||
name: toolName,
|
||||
description: `${a.delegate ? 'Delegate to ' : ''}Subagent: ${a.description || a.name}`,
|
||||
args: {
|
||||
context: {type: 'string', description: 'Summary of related messages, samples, files, etc...', required: true},
|
||||
instructions: {type: 'string', description: 'Detailed instructions for subagent to complete', required: true},
|
||||
},
|
||||
fn: async (args: any, stream: any) => {
|
||||
if(depth >= MAX_AGENT_DEPTH) return 'Max agent delegation depth exceeded';
|
||||
|
||||
const subHistory: LLMMessage[] = [];
|
||||
// Opt-in only, self always excluded regardless of whitelist
|
||||
const nested = (a.agents || [])
|
||||
.map(name => allAgents.find(x => x.name === name))
|
||||
.filter((x): x is Agent => !!x && x.name !== a.name);
|
||||
|
||||
const request = this.ask(`${args.instructions}${args.context ? `\n\n<context>${args.context}</context>` : ''}`, {
|
||||
system: `You are a specialized subagent. ${a.delegate ? 'Your output streams directly to the user for the remainder of this turn.' : 'You are wrapped in a tool call that will be analysis by an LLM'}
|
||||
As a subagent, focus on executing your task completely using available tools and returning only the final result - no commentary, questions, or dialogue.
|
||||
|
||||
${a.system}`,
|
||||
model: a.model || undefined,
|
||||
temperature: a.temperature,
|
||||
stream: a.delegate ? stream : undefined,
|
||||
history: subHistory,
|
||||
mcp: a.mcp || undefined,
|
||||
skills: a.skills || undefined,
|
||||
tools: a.tools || undefined,
|
||||
agents: nested,
|
||||
_agentDepth: depth + 1,
|
||||
} as any);
|
||||
aborts.push(request.abort);
|
||||
const resp = await request;
|
||||
|
||||
if(a.delegate) {
|
||||
if(!pending.has(toolName)) pending.set(toolName, []);
|
||||
pending.get(toolName)!.push({resp, subHistory});
|
||||
return '';
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async setupMcp(servers: McpServer[] = []): Promise<{prompt: string, tools: AiTool[]}> {
|
||||
if(!servers?.length) return {prompt: '', tools: []};
|
||||
const allTools: AiTool[] = [];
|
||||
@@ -170,9 +243,11 @@ class LLM {
|
||||
if(!this.models[m]) throw new Error(`Model does not exist: ${m}`);
|
||||
let request: AbortablePromise<string> | null = null;
|
||||
let aborted = false;
|
||||
const nestedAborts: (() => void)[] = [];
|
||||
const abort = () => {
|
||||
aborted = true;
|
||||
request?.abort?.();
|
||||
nestedAborts.forEach(a => a());
|
||||
};
|
||||
|
||||
const promise = (async () => {
|
||||
@@ -196,22 +271,46 @@ class LLM {
|
||||
tools.push(...s.tools);
|
||||
}
|
||||
|
||||
// Agents
|
||||
const agents = options.agents || this.ai.options?.llm?.agents;
|
||||
const pendingDelegates = new Map<string, {resp: string, subHistory: LLMMessage[]}[]>();
|
||||
if(agents?.length) tools.push(...this.setupAgent(agents, agents, pendingDelegates, nestedAborts, options._agentDepth || 0));
|
||||
|
||||
// Memory
|
||||
if (options.memory) {
|
||||
const mems = options.memory instanceof MemoryCache ? options.memory.memories : options.memory;
|
||||
const mem = MemoryManager.normalize(options.memory);
|
||||
if(mem) {
|
||||
const mems = mem.memory instanceof MemoryCache ? mem.memory.memories : mem.memory;
|
||||
if(mems.length) {
|
||||
const relevant = await this.memoryManager.recollect(message, options.memory, 5);
|
||||
if(mem.inject) {
|
||||
const pool = 15; // candidates considered, cheap since only refs are listed
|
||||
const budget = mem.maxTokens ?? 2000; // actual content injected
|
||||
const relevant = await this.memoryManager.recollect(message, mem.memory, pool);
|
||||
|
||||
let used = 0;
|
||||
const preloaded: typeof relevant = [];
|
||||
const listed: typeof relevant = [];
|
||||
for(const r of relevant) {
|
||||
const t = this.estimateTokens(r.content);
|
||||
if(used + t <= budget || preloaded.length === 0) {
|
||||
preloaded.push(r);
|
||||
used += t;
|
||||
} else listed.push(r);
|
||||
}
|
||||
|
||||
prompts.unshift(`You have access to the following memory files:
|
||||
${mems.map(m => `- ${m.name}: ${m.description}`).join('\n')}
|
||||
${relevant.length ? `
|
||||
Relevant memories have been preloaded:
|
||||
${relevant.map(r => `
|
||||
**${r.name}**
|
||||
${r.description}
|
||||
${r.content}
|
||||
`).join('\n---\n')}
|
||||
` : ''}`.trim());
|
||||
tools.push(this.memoryManager.tools.read(options.memory));
|
||||
${mems.map(m => `- ${m.name}: ${m.description}`).join('\n')}
|
||||
${preloaded.length ? `
|
||||
Relevant memories have been preloaded:
|
||||
${preloaded.map(r => `
|
||||
**${r.name}**
|
||||
${r.description}
|
||||
${r.content}
|
||||
`).join('\n---\n')}
|
||||
` : ''}${listed.length ? `
|
||||
Also relevant but not preloaded (use \`memory_recall\`): ${listed.map(r => r.name).join(', ')}
|
||||
` : ''}`.trim());
|
||||
}
|
||||
if(mem.tool) tools.push(this.memoryManager.tools.read(mem.memory));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,16 +318,33 @@ class LLM {
|
||||
|
||||
prompts.unshift(options.system || this.ai.options.llm?.system || '');
|
||||
request = this.models[m].ask(message, {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
|
||||
const resp = await request;
|
||||
let resp = await request;
|
||||
|
||||
// Spice delegated agents response into history
|
||||
let lastDelegateResp: string | null = null;
|
||||
if(pendingDelegates.size) {
|
||||
for(let i = 0; i < history.length; i++) {
|
||||
const h = history[i];
|
||||
if(h.role !== 'tool' || h.content !== '') continue;
|
||||
const queue = pendingDelegates.get(h.name);
|
||||
if(!queue?.length) continue;
|
||||
const {resp: delegateResp, subHistory} = queue.shift()!;
|
||||
const insert: LLMMessage[] = [...subHistory.filter(sh => sh.role === 'tool'), {role: 'assistant', content: delegateResp, timestamp: Date.now()}];
|
||||
history.splice(i + 1, 0, ...insert);
|
||||
lastDelegateResp = delegateResp;
|
||||
i += insert.length;
|
||||
}
|
||||
}
|
||||
|
||||
// If the orchestrator added no commentary of its own, its answer IS the delegate's answer
|
||||
if(typeof resp === 'string' && !resp.trim() && lastDelegateResp !== null) resp = lastDelegateResp;
|
||||
|
||||
// Trim memory injections from history
|
||||
if(options.memory) {
|
||||
history.splice(0, history.length, ...history.filter(h => h.role !== 'tool' || h.name !== 'recall'));
|
||||
}
|
||||
if(mem?.tool) history.splice(0, history.length, ...history.filter(h => h.role !== 'tool' || h.name !== 'recall'));
|
||||
|
||||
// Auto-memorize before compressing
|
||||
if(options.compress && this.estimateTokens(history) >= options.compress.max) {
|
||||
if(options.memory) await this.memoryManager.memorize(history, options.memory, {model: options.memoryModel || this.defaultModel, ...options});
|
||||
if(mem?.update) await this.memoryManager.memorize(history, mem.memory, {model: options.memoryModel || this.defaultModel, ...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);
|
||||
}
|
||||
@@ -399,7 +515,6 @@ class LLM {
|
||||
*/
|
||||
fuzzyMatch(target, ...searchTerms) {
|
||||
if (searchTerms.length < 2) throw new Error('Requires at least 2 strings to compare');
|
||||
|
||||
const levenshtein = (a, b) => {
|
||||
const m = a.length, n = b.length;
|
||||
if (!m) return n;
|
||||
@@ -415,13 +530,10 @@ class LLM {
|
||||
}
|
||||
return dp[m][n];
|
||||
};
|
||||
|
||||
const similarity = (a, b) => {
|
||||
a = a.toLowerCase(); b = b.toLowerCase();
|
||||
const dist = levenshtein(a, b);
|
||||
return 1 - dist / Math.max(a.length, b.length, 1);
|
||||
return 1 - levenshtein(a, b) / Math.max(a.length, b.length, 1);
|
||||
};
|
||||
|
||||
const similarities = searchTerms.map(t => similarity(target, t));
|
||||
return {
|
||||
avg: similarities.reduce((acc, s) => acc + s, 0) / similarities.length,
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import {KDPoint, KDTree} from './kd-tree.ts';
|
||||
import {Memory, MemoryRef} from './memory.ts';
|
||||
|
||||
export class MemoryCache {
|
||||
private tree: KDTree<MemoryRef>;
|
||||
public memories: Memory[];
|
||||
|
||||
get length() { return this.memories.length; }
|
||||
|
||||
constructor(memories: Memory[]) {
|
||||
this.memories = memories;
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
|
||||
private buildTree(): KDTree<MemoryRef> {
|
||||
const embedded = this.memories.filter(m => m.embedding?.length);
|
||||
if (!embedded.length) return new KDTree<MemoryRef>(0);
|
||||
|
||||
const dims = embedded[0].embedding.length;
|
||||
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
|
||||
vector: m.embedding,
|
||||
payload: {name: m.name, description: m.description},
|
||||
}));
|
||||
|
||||
return new KDTree<MemoryRef>(dims, 'cosine', points);
|
||||
}
|
||||
|
||||
search(query: number[], limit: number): MemoryRef[] {
|
||||
const results = this.tree.knn(query, limit);
|
||||
return results.map(r => r.point.payload);
|
||||
}
|
||||
|
||||
add(memory: Memory): void {
|
||||
this.memories.push(memory);
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
update(memory: Memory): void {
|
||||
const idx = this.memories.findIndex(m => m.name === memory.name);
|
||||
if (idx !== -1) {
|
||||
this.memories[idx] = memory;
|
||||
} else {
|
||||
this.memories.push(memory);
|
||||
}
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
const idx = this.memories.findIndex(m => m.name === name);
|
||||
if (idx !== -1) {
|
||||
this.memories.splice(idx, 1);
|
||||
this.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
rebuild(): void {
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import {MemoryCache} from './memory-cache.ts';
|
||||
import {extractMetadata, Memory, MemoryNode} from './memory.ts';
|
||||
|
||||
export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
|
||||
const mems = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const nameSet = new Set(mems.map(m => m.name));
|
||||
const ghosts = new Set<string>();
|
||||
|
||||
const nodes: MemoryNode[] = mems.map(m => {
|
||||
const {links, backlinks} = extractMetadata(m.content);
|
||||
return {
|
||||
name: m.name,
|
||||
missing: false,
|
||||
links,
|
||||
backlinks,
|
||||
};
|
||||
});
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const link of node.links) {
|
||||
if (!nameSet.has(link)) ghosts.add(link);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...nodes,
|
||||
...[...ghosts].map(name => ({
|
||||
name,
|
||||
missing: true,
|
||||
links: [],
|
||||
backlinks: nodes
|
||||
.filter(n => n.links.includes(name))
|
||||
.map(n => n.name),
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
export function renderMemoryGraph(nodes) {
|
||||
if (!nodes.length) return 'No memories yet.';
|
||||
|
||||
const groups = new Map();
|
||||
for (const node of nodes) {
|
||||
const [prefix, ...rest] = node.name.split('/');
|
||||
const group = rest.length ? prefix : 'Root';
|
||||
const label = rest.length ? rest.join('/') : node.name;
|
||||
if (!groups.has(group)) groups.set(group, []);
|
||||
groups.get(group).push({...node, label});
|
||||
}
|
||||
|
||||
const ghostCount = nodes.filter(n => n.missing).length;
|
||||
const lines = [`Memory Graph (${nodes.length} nodes, ${ghostCount} ghost${ghostCount === 1 ? '' : 's'})`, ''];
|
||||
|
||||
for (const group of [...groups.keys()].sort()) {
|
||||
const items = groups.get(group).sort((a, b) => a.label.localeCompare(b.label));
|
||||
lines.push(`${group}/`);
|
||||
items.forEach((n, i) => {
|
||||
const last = i === items.length - 1;
|
||||
const branch = last ? '└─' : '├─';
|
||||
const pad = last ? ' ' : '│ ';
|
||||
const tag = n.missing ? ' (ghost)' : '';
|
||||
lines.push(` ${branch} ${n.label}${tag}`);
|
||||
if (n.links.length) lines.push(` ${pad} → ${n.links.join(', ')}`);
|
||||
if (n.backlinks.length) lines.push(` ${pad} ← ${n.backlinks.join(', ')}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n').trimEnd();
|
||||
}
|
||||
147
src/memory.ts
147
src/memory.ts
@@ -1,6 +1,143 @@
|
||||
import {LLMRequest, LLMMessage} from './llm.ts';
|
||||
import {MemoryCache} from './memory-cache.ts';
|
||||
import {AiTool} from './tools.ts';
|
||||
import {KDPoint, KDTree} from './kd-tree.ts';
|
||||
|
||||
export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
|
||||
const mems = memories instanceof MemoryCache ? memories.memories : memories;
|
||||
const nameSet = new Set(mems.map(m => m.name));
|
||||
const ghosts = new Set<string>();
|
||||
|
||||
const nodes: MemoryNode[] = mems.map(m => {
|
||||
const {links, backlinks} = extractMetadata(m.content);
|
||||
return {
|
||||
name: m.name,
|
||||
missing: false,
|
||||
links,
|
||||
backlinks,
|
||||
};
|
||||
});
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const link of node.links) {
|
||||
if (!nameSet.has(link)) ghosts.add(link);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...nodes,
|
||||
...[...ghosts].map(name => ({
|
||||
name,
|
||||
missing: true,
|
||||
links: [],
|
||||
backlinks: nodes
|
||||
.filter(n => n.links.includes(name))
|
||||
.map(n => n.name),
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
export function renderMemoryGraph(nodes) {
|
||||
if (!nodes.length) return 'No memories yet.';
|
||||
|
||||
const groups = new Map();
|
||||
for (const node of nodes) {
|
||||
const [prefix, ...rest] = node.name.split('/');
|
||||
const group = rest.length ? prefix : 'Root';
|
||||
const label = rest.length ? rest.join('/') : node.name;
|
||||
if (!groups.has(group)) groups.set(group, []);
|
||||
groups.get(group).push({...node, label});
|
||||
}
|
||||
|
||||
const ghostCount = nodes.filter(n => n.missing).length;
|
||||
const lines = [`Memory Graph (${nodes.length} nodes, ${ghostCount} ghost${ghostCount === 1 ? '' : 's'})`, ''];
|
||||
|
||||
for (const group of [...groups.keys()].sort()) {
|
||||
const items = groups.get(group).sort((a, b) => a.label.localeCompare(b.label));
|
||||
lines.push(`${group}/`);
|
||||
items.forEach((n, i) => {
|
||||
const last = i === items.length - 1;
|
||||
const branch = last ? '└─' : '├─';
|
||||
const pad = last ? ' ' : '│ ';
|
||||
const tag = n.missing ? ' (ghost)' : '';
|
||||
lines.push(` ${branch} ${n.label}${tag}`);
|
||||
if (n.links.length) lines.push(` ${pad} → ${n.links.join(', ')}`);
|
||||
if (n.backlinks.length) lines.push(` ${pad} ← ${n.backlinks.join(', ')}`);
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n').trimEnd();
|
||||
}
|
||||
|
||||
export class MemoryCache {
|
||||
private tree: KDTree<MemoryRef>;
|
||||
public memories: Memory[];
|
||||
|
||||
get length() { return this.memories.length; }
|
||||
|
||||
constructor(memories: Memory[]) {
|
||||
this.memories = memories;
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
|
||||
private buildTree(): KDTree<MemoryRef> {
|
||||
const embedded = this.memories.filter(m => m.embedding?.length);
|
||||
if (!embedded.length) return new KDTree<MemoryRef>(0);
|
||||
|
||||
const dims = embedded[0].embedding.length;
|
||||
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
|
||||
vector: m.embedding,
|
||||
payload: {name: m.name, description: m.description},
|
||||
}));
|
||||
|
||||
return new KDTree<MemoryRef>(dims, 'cosine', points);
|
||||
}
|
||||
|
||||
search(query: number[], limit: number): MemoryRef[] {
|
||||
const results = this.tree.knn(query, limit);
|
||||
return results.map(r => r.point.payload);
|
||||
}
|
||||
|
||||
add(memory: Memory): void {
|
||||
this.memories.push(memory);
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
update(memory: Memory): void {
|
||||
const idx = this.memories.findIndex(m => m.name === memory.name);
|
||||
if (idx !== -1) {
|
||||
this.memories[idx] = memory;
|
||||
} else {
|
||||
this.memories.push(memory);
|
||||
}
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
remove(name: string): void {
|
||||
const idx = this.memories.findIndex(m => m.name === name);
|
||||
if (idx !== -1) {
|
||||
this.memories.splice(idx, 1);
|
||||
this.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
rebuild(): void {
|
||||
this.tree = this.buildTree();
|
||||
}
|
||||
}
|
||||
|
||||
export type MemoryOptions = {
|
||||
/** Memory object */
|
||||
memory: Memory[] | MemoryCache;
|
||||
/** Inject N memories into the system prompt */
|
||||
inject?: boolean;
|
||||
/** expose recall tool to LLM */
|
||||
tool?: boolean;
|
||||
/** Update memory on compression */
|
||||
update?: boolean;
|
||||
/** Max context size of memories to inject to each call (removed immediately after use) */
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export type Memory = {
|
||||
name: string;
|
||||
@@ -83,8 +220,6 @@ function getWeekSunday(monday: string): string {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class MemoryManager {
|
||||
private pendingMemorizations = new Map<string, {
|
||||
memories: Memory[] | MemoryCache,
|
||||
@@ -128,6 +263,12 @@ export class MemoryManager {
|
||||
|
||||
constructor(private llm: any) {}
|
||||
|
||||
static normalize(m?: Memory[] | MemoryCache | MemoryOptions) {
|
||||
if(!m) return null;
|
||||
const raw = m instanceof MemoryCache || Array.isArray(m);
|
||||
return raw ? {memory: <Memory[] | MemoryCache>m, inject: true, tool: true, update: true} : {inject: true, tool: true, update: true, ...m};
|
||||
}
|
||||
|
||||
private async createTempMemory(conversation: string): Promise<Memory> {
|
||||
const timestamp = Date.now();
|
||||
const content = `---
|
||||
|
||||
Reference in New Issue
Block a user