P0-P2: heartbeat→mind wiring, structured JSON output, bge-m3 embeddings

This commit is contained in:
n1kt4r committed 2026-09-14 16:20:45 +05:00
1 parent 702700dcb9
commit a6fde100ca
9 files changed
+184 -35

No files matched your search

+22 -15
View File
@@ -7,6 +7,8 @@
* long-term — в LanceDB (или Qdrant на a2000 для прода).
*/
import { EmbeddingClient, cosineSimilarity } from './embedding.js';
export interface Thought {
id: string;
text: string;
@@ -39,8 +41,12 @@ export class MemorySystem {
private notepads = new Map<string, ContactNotepad>();
private selfObservationsList: SelfObservation[] = [];
private longTerm: LongTermMemory[] = [];
/** Embedding-клиент (Ollama bge-m3). Если null — long-term без векторного поиска. */
private embeddingClient: EmbeddingClient | null;
constructor(private recentCount: number) {}
constructor(private recentCount: number, embeddingClient?: EmbeddingClient) {
this.embeddingClient = embeddingClient ?? null;
}
// === Short-term: последние мысли/опыт/сообщения ===
addThought(text: string): Thought {
@@ -74,19 +80,26 @@ export class MemorySystem {
return this.selfObservationsList;
}
// === Long-term: факты/опыт/процедуры/исходы + векторный поиск ===
addLongTerm(text: string, type: LongTermMemory['type']): LongTermMemory {
// === Long-term: факты/опыт/процедуры/исходы + эмбедд для векторного поиска ===
async addLongTerm(text: string, type: LongTermMemory['type']): Promise<LongTermMemory> {
const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() };
if (this.embeddingClient) {
m.embedding = await this.embeddingClient.embed(text);
}
this.longTerm.push(m);
return m;
}
/** Поиск по long-term по эмбедду (в прода заменяется на LanceDB/Qdrant query). */
searchLongTerm(queryEmbedding: number[], topK = 3): LongTermMemory[] {
// cosine similarity — заглушка, в прода: LanceDB.search
/** Поиск по long-term по тексту через эмбедд (в прода заменяется на LanceDB/Qdrant). */
async searchLongTerm(query: string, topK = 3): Promise<LongTermMemory[]> {
if (!this.embeddingClient) {
// без embedding-клиента — простой fallback
return this.longTerm.slice(0, topK);
}
const q = await this.embeddingClient.embed(query);
const scored = this.longTerm
.filter(m => m.embedding)
.map(m => ({ m, score: cosine(queryEmbedding, m.embedding!) }))
.map(m => ({ m, score: cosineSimilarity(q, m.embedding!) }))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(x => x.m);
@@ -94,16 +107,10 @@ export class MemorySystem {
}
/** Консолидация: перенос недавних мыслей в long-term (по спецификации Animus). */
consolidate(recent: Thought[]): void {
async consolidate(recent: Thought[]): Promise<void> {
for (const t of recent) {
// эвристика: только «значимые» мысли (по тегам/ключевым словам)
this.addLongTerm(t.text, 'experience');
await this.addLongTerm(t.text, 'experience');
}
}
}
function cosine(a: number[], b: number[]): number {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; }
return Math.sqrt(na) * Math.sqrt(nb) ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
}