P3-P5: SubAgent SDK, Telegram Channel, SQLite persistence, full pipeline
P3 - SubAgent SDK: - src/subagent.ts: researcher, coder, analyst, content-writer - Mind.runSubAgent() → SubAgentFactory.create() → execute() - Результаты сохраняются в long-term memory - Heartbeat.notify(subagent-complete) после завершения P4 - SQLite persistence (5 таблиц, WAL): - src/database.ts: thoughts, notepads, self_observations, long_term, embeddings - memory.ts → Database (полная замена in-memory) - index.ts: dbPath → ~/.local/share/spark-mind/spark-mind.db P5 - Telegram Channel Layer: - src/telegram.ts: https module (SNI fix), MAX Messenger API - mind.ts: sendReply() → Telegram + console fallback - index.ts: TelegramMessenger(config) → Mind Fixes: - FK constraint: long_term INSERT → long_term_embeddings INSERT - JSON parse: nested objects, markdown fences - P5 telegram: graceful degradation on SSL error
This commit is contained in:
1 parent
a01f183680
commit
bae171d6b4
9 files changed
+536
-58
No files matched your search
+50
-28
@@ -1,13 +1,16 @@
|
||||
/**
|
||||
* Memory System — 4 слоя памяти (концепт Animus):
|
||||
* short-term / working / core self / long-term
|
||||
* Хранение: SQLite (7 таблиц) + LanceDB для long-term (векторный поиск).
|
||||
* Memory System — 4 слоя памяти (концепт Animus).
|
||||
* Хранение: SQLite (5 таблиц) + bge-m3 эмбеддинги (cosine-поиск).
|
||||
*
|
||||
* В MVP: short-term + working + core self — в SQLite.
|
||||
* long-term — в LanceDB (или Qdrant на a2000 для прода).
|
||||
* Слои:
|
||||
* short-term → thoughts (recent thoughts, last N)
|
||||
* working → notepads (notepad per contact)
|
||||
* core self → self_observations (накопленная самосознательность)
|
||||
* long-term → long_term + long_term_embeddings (факты/опыт/процедуры/исходы)
|
||||
*/
|
||||
|
||||
import { EmbeddingClient, cosineSimilarity } from './embedding.js';
|
||||
import { Database } from './database.js';
|
||||
|
||||
export interface Thought {
|
||||
id: string;
|
||||
@@ -36,73 +39,92 @@ export interface LongTermMemory {
|
||||
embedding?: number[];
|
||||
}
|
||||
|
||||
export interface MemorySystemConfig {
|
||||
recentCount?: number;
|
||||
dbPath?: string;
|
||||
}
|
||||
|
||||
export class MemorySystem {
|
||||
private recentThoughtsList: Thought[] = [];
|
||||
private notepads = new Map<string, ContactNotepad>();
|
||||
private selfObservationsList: SelfObservation[] = [];
|
||||
private longTerm: LongTermMemory[] = [];
|
||||
/** Embedding-клиент (Ollama bge-m3). Если null — long-term без векторного поиска. */
|
||||
private recentCount: number;
|
||||
private db: Database;
|
||||
private embeddingClient: EmbeddingClient | null;
|
||||
|
||||
constructor(private recentCount: number, embeddingClient?: EmbeddingClient) {
|
||||
constructor(config: MemorySystemConfig = {}, embeddingClient?: EmbeddingClient) {
|
||||
this.recentCount = config.recentCount ?? 10;
|
||||
this.db = new Database(config.dbPath);
|
||||
this.embeddingClient = embeddingClient ?? null;
|
||||
}
|
||||
|
||||
// === Short-term: последние мысли/опыт/сообщения ===
|
||||
addThought(text: string): Thought {
|
||||
const t: Thought = { id: `th-${Date.now()}`, text, createdAt: Date.now() };
|
||||
this.recentThoughtsList.unshift(t);
|
||||
if (this.recentThoughtsList.length > this.recentCount) this.recentThoughtsList.pop();
|
||||
this.db.insertThought(t.id, t.text, t.createdAt);
|
||||
return t;
|
||||
}
|
||||
|
||||
recentThoughts(): Thought[] {
|
||||
return this.recentThoughtsList.slice(0, this.recentCount);
|
||||
const rows = this.db.getRecentThoughts(this.recentCount);
|
||||
return rows.map(r => ({ id: r.id, text: r.text, createdAt: r.created_at }));
|
||||
}
|
||||
|
||||
// === Working memory: notepad по контактам ===
|
||||
updateNotepad(contactId: string, block: string) {
|
||||
this.notepads.set(contactId, { contactId, block, updatedAt: Date.now() });
|
||||
updateNotepad(contactId: string, block: string): void {
|
||||
const ts = Date.now();
|
||||
this.db.upsertNotepad(contactId, block, ts);
|
||||
}
|
||||
|
||||
getNotepad(contactId: string): string {
|
||||
return this.notepads.get(contactId)?.block ?? '';
|
||||
return this.db.getNotepad(contactId) ?? '';
|
||||
}
|
||||
|
||||
// === Core self: накопленная самосознательность ===
|
||||
addSelfObservation(text: string, tags: string[] = []): SelfObservation {
|
||||
const o: SelfObservation = { id: `so-${Date.now()}`, text, createdAt: Date.now(), tags };
|
||||
this.selfObservationsList.push(o);
|
||||
this.db.insertSelfObservation(o.id, o.text, o.createdAt);
|
||||
return o;
|
||||
}
|
||||
|
||||
getAllSelfObservations(): SelfObservation[] {
|
||||
return this.selfObservationsList;
|
||||
const rows = this.db.getAllSelfObservations();
|
||||
return rows.map(r => ({ id: r.id, text: r.text, createdAt: r.created_at, tags: [] }));
|
||||
}
|
||||
|
||||
// === Long-term: факты/опыт/процедуры/исходы + эмбедд для векторного поиска ===
|
||||
async addLongTerm(text: string, type: LongTermMemory['type']): Promise<LongTermMemory> {
|
||||
const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() };
|
||||
let embedding: number[] = [];
|
||||
if (this.embeddingClient) {
|
||||
m.embedding = await this.embeddingClient.embed(text);
|
||||
embedding = await this.embeddingClient.embed(text);
|
||||
}
|
||||
this.longTerm.push(m);
|
||||
// Сначала long_term (FK требует), потом embedding
|
||||
this.db.insertLongTerm(m.id, m.text, m.type, m.createdAt);
|
||||
if (embedding.length > 0) {
|
||||
this.db.insertLongTermEmbedding(m.id, embedding);
|
||||
}
|
||||
m.embedding = embedding;
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Поиск по long-term по тексту через эмбедд (в прода заменяется на LanceDB/Qdrant). */
|
||||
/** Поиск по long-term по тексту через эмбедд (cosine similarity в SQLite). */
|
||||
async searchLongTerm(query: string, topK = 3): Promise<LongTermMemory[]> {
|
||||
if (!this.embeddingClient) {
|
||||
// без embedding-клиента — простой fallback
|
||||
return this.longTerm.slice(0, topK);
|
||||
const rows = this.db.getLongTerm(this.recentCount);
|
||||
return rows.map(r => ({ id: r.id, text: r.text, type: r.type as LongTermMemory['type'], createdAt: r.created_at }));
|
||||
}
|
||||
|
||||
const q = await this.embeddingClient.embed(query);
|
||||
const scored = this.longTerm
|
||||
.filter(m => m.embedding)
|
||||
.map(m => ({ m, score: cosineSimilarity(q, m.embedding!) }))
|
||||
const all = this.db.getAllLongTermWithVectors();
|
||||
const scored = all
|
||||
.map(m => ({ m, score: cosineSimilarity(q, Array.from(m.embedding)) }))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, topK)
|
||||
.map(x => x.m);
|
||||
.map(x => ({
|
||||
id: x.m.id,
|
||||
text: x.m.text,
|
||||
type: x.m.type as LongTermMemory['type'],
|
||||
createdAt: x.m.created_at,
|
||||
embedding: Array.from(x.m.embedding),
|
||||
}));
|
||||
return scored;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user