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
@@ -18,9 +18,13 @@ Self-hosted автономный агент (MVP). Концепт позаимс
|
|||||||
|
|
||||||
```
|
```
|
||||||
heartbeat.ts — ядро: 5 триггеров тика, состояния Cold/Warm
|
heartbeat.ts — ядро: 5 триггеров тика, состояния Cold/Warm
|
||||||
memory.ts — 4 слоя памяти (short/working/core self/long-term)
|
memory.ts — 4 слоя памяти (short/working/core self/long-term) + SQLite
|
||||||
persona.ts — 8 слоёв → системный промпт
|
persona.ts — 8 слоёв → системный промпт
|
||||||
mind.ts — оркестратор: heartbeat + memory + persona + Spark LLM
|
mind.ts — оркестратор: heartbeat + memory + persona + LLM
|
||||||
|
subagent.ts — SubAgent SDK: researcher, coder, analyst, content-writer
|
||||||
|
embedding.ts — bge-m3 embeddings (dim=1024), cosine similarity
|
||||||
|
database.ts — SQLite репозиторий: 5 таблиц, WAL-режим
|
||||||
|
telegram.ts — Telegram Channel Layer: MAX Messenger API
|
||||||
```
|
```
|
||||||
|
|
||||||
### Heartbeat (5 триггеров)
|
### Heartbeat (5 триггеров)
|
||||||
@@ -38,16 +42,18 @@ mind.ts — оркестратор: heartbeat + memory + persona + Spark L
|
|||||||
|
|
||||||
| Слой | Хранение | Что держит |
|
| Слой | Хранение | Что держит |
|
||||||
|------|----------|-----------|
|
|------|----------|-----------|
|
||||||
| short-term | SQLite | последние ~10 мыслей/опыт/сообщений |
|
| short-term | SQLite (thoughts) | последние ~10 мыслей/опыт/сообщений |
|
||||||
| working | SQLite | notepad по контактам |
|
| working | SQLite (notepads) | notepad по контактам |
|
||||||
| core self | SQLite | самосознательность |
|
| core self | SQLite (self_observations) | самосознательность |
|
||||||
| long-term | LanceDB/Qdrant | факты/опыт/процедуры (векторный поиск) |
|
| long-term | SQLite (long_term + embeddings) | факты/опыт/процедуры + bge-m3 векторы |
|
||||||
|
|
||||||
### Persona (8 слоёв)
|
### Sub-Agents
|
||||||
|
|
||||||
Existence Paradigm → Identity → Archetype → Dimensions → Traits → Values → Background → Review
|
Поддерживаемые агенты:
|
||||||
|
- **researcher** — сбор и анализ информации (web_search, file_read, web_extract)
|
||||||
Компилируется в системный промпт. `values` — safety-layer (границы для EXECUTE).
|
- **coder** — написание и ревью кода (file_write, terminal, code_review)
|
||||||
|
- **analyst** — анализ данных и ситуаций (data_query, spreadsheet, chart_generator)
|
||||||
|
- **content-writer** — создание веб-контента (template_search, seo_check, grammar_check)
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
@@ -60,12 +66,36 @@ npm start # node dist/index.js
|
|||||||
Ornith-1.5-9B должен быть доступен по OpenAI-совместимому API (Ollama):
|
Ornith-1.5-9B должен быть доступен по OpenAI-совместимому API (Ollama):
|
||||||
- Ollama: `http://localhost:11434` (модель `ornith-1.5-9b:latest`)
|
- Ollama: `http://localhost:11434` (модель `ornith-1.5-9b:latest`)
|
||||||
|
|
||||||
## Roadmap
|
### Настройка Telegram
|
||||||
|
|
||||||
- [x] SQLite-репозиторий вместо in-memory (персистентность) — отложено (P4)
|
Для отправки ответов в Telegram добавьте в `index.ts`:
|
||||||
- [x] LanceDB/Qdrant integration для long-term — bge-m3 embeddings локально (Ollama)
|
```typescript
|
||||||
- [x] Channel layer (вывод) — console-заглушка, подключение Telegram/SMS (P5)
|
import { TelegramMessenger } from './src/telegram.js';
|
||||||
- [ ] Sub-agent SDK (делегирование Ornith-9B)
|
|
||||||
|
const telegram = new TelegramMessenger({
|
||||||
|
botUrl: 'https://bot.qw4.ru/bot/nik_bot',
|
||||||
|
userId: '140245519',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Примечание: если bot.qw4.ru недоступен (SSL-ошибка 'unrecognized name'),
|
||||||
|
> агент работает в console-only режиме.
|
||||||
|
|
||||||
|
## Статус P0–P5
|
||||||
|
|
||||||
|
| Priority | Компонент | Статус |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| P0 | heartbeat → mind wiring | ✅ |
|
||||||
|
| P1 | structured JSON output (parseSparkOutput) | ✅ |
|
||||||
|
| P2 | bge-m3 embeddings + cosine search | ✅ |
|
||||||
|
| P3 | SubAgent SDK (researcher, coder, analyst, content-writer) | ✅ |
|
||||||
|
| P4 | SQLite persistence (5 таблиц, WAL) | ✅ |
|
||||||
|
| P5 | Telegram Channel Layer (MAX API + fallback) | ✅ |
|
||||||
|
|
||||||
|
## Roadmap (P6+)
|
||||||
|
|
||||||
|
- [ ] Sleep & energy (циркадный ритм)
|
||||||
|
- [ ] Web UI (persona setup)
|
||||||
- [ ] Sleep & energy (циркадный ритм)
|
- [ ] Sleep & energy (циркадный ритм)
|
||||||
- [ ] Web UI (persona setup)
|
- [ ] Web UI (persona setup)
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,35 @@ import { MemorySystem } from './src/memory.js';
|
|||||||
import { DEFAULT_PERSONA } from './src/persona.js';
|
import { DEFAULT_PERSONA } from './src/persona.js';
|
||||||
import { Mind, SparkLLMClient } from './src/mind.js';
|
import { Mind, SparkLLMClient } from './src/mind.js';
|
||||||
import { OllamaEmbeddingClient } from './src/embedding.js';
|
import { OllamaEmbeddingClient } from './src/embedding.js';
|
||||||
|
import { SubAgentFactory } from './src/subagent.js';
|
||||||
|
import { TelegramMessenger } from './src/telegram.js';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
|
||||||
const HEARTBEAT_MS = 5 * 60 * 1000; // 5 мин — idle-мысли
|
const HEARTBEAT_MS = 5 * 60 * 1000; // 5 мин — idle-мысли
|
||||||
|
const DB_PATH = join(homedir(), '.local/share/spark-mind/spark-mind.db');
|
||||||
|
|
||||||
const heartbeat = new Heartbeat({ intervalMs: HEARTBEAT_MS });
|
const heartbeat = new Heartbeat({ intervalMs: HEARTBEAT_MS });
|
||||||
// Embeddings: bge-m3 (dim=1024) — та же модель, что в content-atom
|
// Embeddings: bge-m3 (dim=1024) — та же модель, что в content-atom
|
||||||
const embedding = new OllamaEmbeddingClient('http://localhost:11434');
|
const embedding = new OllamaEmbeddingClient('http://localhost:11434');
|
||||||
const memory = new MemorySystem(10, embedding); // last 10 thoughts
|
const memory = new MemorySystem({ recentCount: 10, dbPath: DB_PATH }, embedding); // last 10 thoughts + SQLite
|
||||||
|
// Sub-agents: researcher, coder, analyst, content-writer
|
||||||
|
const agentFactory = new SubAgentFactory();
|
||||||
|
|
||||||
|
// Telegram: MAX Messenger API (nik_bot → user_id 140245519)
|
||||||
|
const telegramConfig: import('./src/telegram.js').TelegramConfig = {
|
||||||
|
botUrl: 'https://bot.qw4.ru/bot/nik_bot',
|
||||||
|
userId: '140245519',
|
||||||
|
};
|
||||||
|
const telegram = new TelegramMessenger(telegramConfig);
|
||||||
|
|
||||||
const mind = new Mind(
|
const mind = new Mind(
|
||||||
heartbeat,
|
heartbeat,
|
||||||
memory,
|
memory,
|
||||||
DEFAULT_PERSONA,
|
DEFAULT_PERSONA,
|
||||||
new SparkLLMClient('http://localhost:11434', 'ornith-1.5-9b:latest'), // Ornith-9B — движок
|
new SparkLLMClient('http://localhost:11434', 'ornith-1.5-9b:latest'), // Ornith-9B — движок
|
||||||
|
agentFactory,
|
||||||
|
telegram,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Подключаем heartbeat к оркестратору — теперь тики реально обрабатываются
|
// Подключаем heartbeat к оркестратору — теперь тики реально обрабатываются
|
||||||
|
|||||||
Generated
+35
@@ -8,11 +8,25 @@
|
|||||||
"name": "spark-mind",
|
"name": "spark-mind",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^13.0.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^9.6.0",
|
||||||
"@types/node": "^26.5.0",
|
"@types/node": "^26.5.0",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "9.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
|
||||||
|
"integrity": "sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "26.5.0",
|
"version": "26.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz",
|
||||||
@@ -363,6 +377,27 @@
|
|||||||
"node": ">=16.20.0"
|
"node": ">=16.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "13.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
|
||||||
|
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "8.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
|
||||||
|
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^9.6.0",
|
||||||
"@types/node": "^26.5.0",
|
"@types/node": "^26.5.0",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^13.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* SQLite репозиторий для системы памяти Spark Mind.
|
||||||
|
* 5 таблиц: thoughts (short-term), notepads (working),
|
||||||
|
* self_observations (core self), long_term (факты/опыт), long_term_embeddings (векторы).
|
||||||
|
*
|
||||||
|
* Данные хранятся в ~/.local/share/spark-mind/spark-mind.db.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import sqlite3 from 'better-sqlite3';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
|
||||||
|
export class Database {
|
||||||
|
private db: sqlite3.Database;
|
||||||
|
|
||||||
|
constructor(dbPath?: string) {
|
||||||
|
const path = dbPath ?? join(homedir(), '.local/share/spark-mind/spark-mind.db');
|
||||||
|
mkdirSync(join(path, '..'), { recursive: true });
|
||||||
|
this.db = new sqlite3(path, { readonly: false });
|
||||||
|
this.db.pragma('journal_mode = WAL');
|
||||||
|
this.db.pragma('synchronous = normal');
|
||||||
|
this.db.pragma('foreign_keys = ON');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS thoughts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS notepads (
|
||||||
|
contact_id TEXT PRIMARY KEY,
|
||||||
|
block TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS self_observations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS long_term (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK(type IN ('fact', 'experience', 'procedure', 'outcome')),
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS long_term_embeddings (
|
||||||
|
long_term_id TEXT PRIMARY KEY REFERENCES long_term(id) ON DELETE CASCADE,
|
||||||
|
embedding BLOB NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Short-term (thoughts) ===
|
||||||
|
|
||||||
|
insertThought(id: string, text: string, createdAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO thoughts (id, text, created_at) VALUES (?, ?, ?)').run(id, text, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentThoughts(limit: number): { id: string; text: string; created_at: number }[] {
|
||||||
|
return this.db.prepare(
|
||||||
|
'SELECT * FROM thoughts ORDER BY created_at DESC LIMIT ?',
|
||||||
|
).all(limit) as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Working (notepads) ===
|
||||||
|
|
||||||
|
upsertNotepad(contactId: string, block: string, updatedAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO notepads (contact_id, block, updated_at) VALUES (?, ?, ?)').run(contactId, block, updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotepad(contactId: string): string | null {
|
||||||
|
return (this.db.prepare('SELECT block FROM notepads WHERE contact_id = ?').get(contactId) as { block: string } | undefined)?.block ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Core self ===
|
||||||
|
|
||||||
|
insertSelfObservation(id: string, text: string, createdAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO self_observations (id, text, created_at) VALUES (?, ?, ?)').run(id, text, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSelfObservations(): { id: string; text: string; created_at: number }[] {
|
||||||
|
return this.db.prepare('SELECT * FROM self_observations ORDER BY created_at DESC').all() as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Long-term ===
|
||||||
|
|
||||||
|
insertLongTerm(id: string, text: string, type: string, createdAt: number): void {
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO long_term (id, text, type, created_at) VALUES (?, ?, ?, ?)').run(id, text, type, createdAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLongTerm(limit: number): { id: string; text: string; type: string; created_at: number }[] {
|
||||||
|
return this.db.prepare('SELECT * FROM long_term ORDER BY created_at DESC LIMIT ?').all(limit) as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
insertLongTermEmbedding(longTermId: string, embedding: number[]): void {
|
||||||
|
const blob = Buffer.from(new Float32Array(embedding).buffer);
|
||||||
|
this.db.prepare('INSERT OR REPLACE INTO long_term_embeddings (long_term_id, embedding) VALUES (?, ?)').run(longTermId, blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLongTermEmbedding(longTermId: string): Float32Array | null {
|
||||||
|
const row = this.db.prepare('SELECT embedding FROM long_term_embeddings WHERE long_term_id = ?').get(longTermId) as { embedding: Buffer } | undefined;
|
||||||
|
return row ? new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllLongTermWithVectors(): { id: string; text: string; type: string; created_at: number; embedding: Float32Array }[] {
|
||||||
|
const rows = this.db.prepare(`
|
||||||
|
SELECT lt.id, lt.text, lt.type, lt.created_at, le.embedding
|
||||||
|
FROM long_term lt
|
||||||
|
JOIN long_term_embeddings le ON le.long_term_id = lt.id
|
||||||
|
`).all() as any[];
|
||||||
|
return rows.map(r => ({
|
||||||
|
...r,
|
||||||
|
embedding: new Float32Array(r.embedding.buffer, r.embedding.byteOffset, r.embedding.byteLength),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
-28
@@ -1,13 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Memory System — 4 слоя памяти (концепт Animus):
|
* Memory System — 4 слоя памяти (концепт Animus).
|
||||||
* short-term / working / core self / long-term
|
* Хранение: SQLite (5 таблиц) + bge-m3 эмбеддинги (cosine-поиск).
|
||||||
* Хранение: SQLite (7 таблиц) + LanceDB для long-term (векторный поиск).
|
|
||||||
*
|
*
|
||||||
* В 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 { EmbeddingClient, cosineSimilarity } from './embedding.js';
|
||||||
|
import { Database } from './database.js';
|
||||||
|
|
||||||
export interface Thought {
|
export interface Thought {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,73 +39,92 @@ export interface LongTermMemory {
|
|||||||
embedding?: number[];
|
embedding?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MemorySystemConfig {
|
||||||
|
recentCount?: number;
|
||||||
|
dbPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class MemorySystem {
|
export class MemorySystem {
|
||||||
private recentThoughtsList: Thought[] = [];
|
private recentCount: number;
|
||||||
private notepads = new Map<string, ContactNotepad>();
|
private db: Database;
|
||||||
private selfObservationsList: SelfObservation[] = [];
|
|
||||||
private longTerm: LongTermMemory[] = [];
|
|
||||||
/** Embedding-клиент (Ollama bge-m3). Если null — long-term без векторного поиска. */
|
|
||||||
private embeddingClient: EmbeddingClient | null;
|
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;
|
this.embeddingClient = embeddingClient ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Short-term: последние мысли/опыт/сообщения ===
|
// === Short-term: последние мысли/опыт/сообщения ===
|
||||||
addThought(text: string): Thought {
|
addThought(text: string): Thought {
|
||||||
const t: Thought = { id: `th-${Date.now()}`, text, createdAt: Date.now() };
|
const t: Thought = { id: `th-${Date.now()}`, text, createdAt: Date.now() };
|
||||||
this.recentThoughtsList.unshift(t);
|
this.db.insertThought(t.id, t.text, t.createdAt);
|
||||||
if (this.recentThoughtsList.length > this.recentCount) this.recentThoughtsList.pop();
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
recentThoughts(): Thought[] {
|
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 по контактам ===
|
// === Working memory: notepad по контактам ===
|
||||||
updateNotepad(contactId: string, block: string) {
|
updateNotepad(contactId: string, block: string): void {
|
||||||
this.notepads.set(contactId, { contactId, block, updatedAt: Date.now() });
|
const ts = Date.now();
|
||||||
|
this.db.upsertNotepad(contactId, block, ts);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotepad(contactId: string): string {
|
getNotepad(contactId: string): string {
|
||||||
return this.notepads.get(contactId)?.block ?? '';
|
return this.db.getNotepad(contactId) ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Core self: накопленная самосознательность ===
|
// === Core self: накопленная самосознательность ===
|
||||||
addSelfObservation(text: string, tags: string[] = []): SelfObservation {
|
addSelfObservation(text: string, tags: string[] = []): SelfObservation {
|
||||||
const o: SelfObservation = { id: `so-${Date.now()}`, text, createdAt: Date.now(), tags };
|
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;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllSelfObservations(): SelfObservation[] {
|
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: факты/опыт/процедуры/исходы + эмбедд для векторного поиска ===
|
// === Long-term: факты/опыт/процедуры/исходы + эмбедд для векторного поиска ===
|
||||||
async addLongTerm(text: string, type: LongTermMemory['type']): Promise<LongTermMemory> {
|
async addLongTerm(text: string, type: LongTermMemory['type']): Promise<LongTermMemory> {
|
||||||
const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() };
|
const m: LongTermMemory = { id: `lt-${Date.now()}`, text, type, createdAt: Date.now() };
|
||||||
|
let embedding: number[] = [];
|
||||||
if (this.embeddingClient) {
|
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;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Поиск по long-term по тексту через эмбедд (в прода заменяется на LanceDB/Qdrant). */
|
/** Поиск по long-term по тексту через эмбедд (cosine similarity в SQLite). */
|
||||||
async searchLongTerm(query: string, topK = 3): Promise<LongTermMemory[]> {
|
async searchLongTerm(query: string, topK = 3): Promise<LongTermMemory[]> {
|
||||||
if (!this.embeddingClient) {
|
if (!this.embeddingClient) {
|
||||||
// без embedding-клиента — простой fallback
|
const rows = this.db.getLongTerm(this.recentCount);
|
||||||
return this.longTerm.slice(0, topK);
|
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 q = await this.embeddingClient.embed(query);
|
||||||
const scored = this.longTerm
|
const all = this.db.getAllLongTermWithVectors();
|
||||||
.filter(m => m.embedding)
|
const scored = all
|
||||||
.map(m => ({ m, score: cosineSimilarity(q, m.embedding!) }))
|
.map(m => ({ m, score: cosineSimilarity(q, Array.from(m.embedding)) }))
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, topK)
|
.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;
|
return scored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+58
-14
@@ -12,11 +12,8 @@
|
|||||||
import { Heartbeat, TickTrigger } from './heartbeat.js';
|
import { Heartbeat, TickTrigger } from './heartbeat.js';
|
||||||
import { MemorySystem } from './memory.js';
|
import { MemorySystem } from './memory.js';
|
||||||
import { Persona, compileSystemPrompt } from './persona.js';
|
import { Persona, compileSystemPrompt } from './persona.js';
|
||||||
|
import { SubAgent, SubAgentFactory, SubAgentResult } from './subagent.js';
|
||||||
export interface SubAgentResult {
|
import { TelegramMessenger } from './telegram.js';
|
||||||
agent: string;
|
|
||||||
output: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LLMClient {
|
export interface LLMClient {
|
||||||
/** Генерация с контекстом. Возвращает { text, thoughts, emotion, decide }. */
|
/** Генерация с контекстом. Возвращает { text, thoughts, emotion, decide }. */
|
||||||
@@ -144,16 +141,26 @@ export class Mind {
|
|||||||
private heartbeat: Heartbeat;
|
private heartbeat: Heartbeat;
|
||||||
private memory: MemorySystem;
|
private memory: MemorySystem;
|
||||||
private persona: Persona;
|
private persona: Persona;
|
||||||
|
private llm: LLMClient;
|
||||||
|
/** Фабрика под-агентов. Если null — делегирование отключено. */
|
||||||
|
private agentFactory: SubAgentFactory | null;
|
||||||
|
/** Telegram-мессенджер. Если null — только console. */
|
||||||
|
private telegram: TelegramMessenger | null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
heartbeat: Heartbeat,
|
heartbeat: Heartbeat,
|
||||||
memory: MemorySystem,
|
memory: MemorySystem,
|
||||||
persona: Persona,
|
persona: Persona,
|
||||||
private llm: LLMClient,
|
llm: LLMClient,
|
||||||
|
agentFactory?: SubAgentFactory,
|
||||||
|
telegram?: TelegramMessenger,
|
||||||
) {
|
) {
|
||||||
this.heartbeat = heartbeat;
|
this.heartbeat = heartbeat;
|
||||||
this.memory = memory;
|
this.memory = memory;
|
||||||
this.persona = persona;
|
this.persona = persona;
|
||||||
|
this.llm = llm;
|
||||||
|
this.agentFactory = agentFactory ?? null;
|
||||||
|
this.telegram = telegram ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Обработчик любого тика. */
|
/** Обработчик любого тика. */
|
||||||
@@ -161,7 +168,7 @@ export class Mind {
|
|||||||
const systemPrompt = compileSystemPrompt(this.persona);
|
const systemPrompt = compileSystemPrompt(this.persona);
|
||||||
const context = this.buildContext(trigger);
|
const context = this.buildContext(trigger);
|
||||||
|
|
||||||
const result = await this.llm.generate(context, systemPrompt);
|
const result = await this.generateTick(context, systemPrompt);
|
||||||
|
|
||||||
// 1. Short-term memory: мысли mind
|
// 1. Short-term memory: мысли mind
|
||||||
this.memory.addThought(result.thoughts);
|
this.memory.addThought(result.thoughts);
|
||||||
@@ -174,12 +181,17 @@ export class Mind {
|
|||||||
await this.sendReply(result.decision.reply);
|
await this.sendReply(result.decision.reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Если делегировал саб-агенту — запускаем (в прода: sub-agent SDK)
|
// 4. Если делегировал саб-агенту — запускаем (с task из решения)
|
||||||
if (result.decision.subAgent) {
|
if (result.decision.subAgent) {
|
||||||
await this.runSubAgent(result.decision.subAgent);
|
await this.runSubAgent(result.decision.subAgent, result.decision.action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Основной цикл генерации (вызывается и для под-агентов). */
|
||||||
|
private async generateTick(context: string, systemPrompt: string): Promise<LLMResult> {
|
||||||
|
return this.llm.generate(context, systemPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
/** GATHER CONTEXT: собирает контекст для тика (по спецификации Animus). */
|
/** GATHER CONTEXT: собирает контекст для тика (по спецификации Animus). */
|
||||||
private buildContext(trigger: TickTrigger): string {
|
private buildContext(trigger: TickTrigger): string {
|
||||||
const recent = this.memory.recentThoughts();
|
const recent = this.memory.recentThoughts();
|
||||||
@@ -196,18 +208,50 @@ export class Mind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel layer — отправка ответа.
|
* Channel layer — отправка ответа в Telegram (если настроен) + console.
|
||||||
* В MVP: console (вывод). В прода: Telegram/SMS/Web (подключается отдельно).
|
* Fallback: если Telegram недоступен — только console.
|
||||||
*/
|
*/
|
||||||
private async sendReply(text: string): Promise<void> {
|
private async sendReply(text: string): Promise<void> {
|
||||||
|
// Всегда выводить в консоль
|
||||||
console.log('[reply]', text);
|
console.log('[reply]', text);
|
||||||
|
|
||||||
|
// Если Telegram настроен — попробовать отправить
|
||||||
|
if (this.telegram) {
|
||||||
|
try {
|
||||||
|
const result = await this.telegram.sendMessage(text);
|
||||||
|
console.log('[telegram] sent:', result.messageId, 'at', new Date(result.sentAt).toISOString());
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[telegram] send failed, using console-only:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub-agent — делегирование тяжёлого reasoning.
|
* Sub-agent — делегирование тяжёлого reasoning.
|
||||||
* В прода: Ornith-9B через sub-agent SDK. Пока — заглушка.
|
* Создаёт под-агента, выполняет задачу, сохраняет результат.
|
||||||
*/
|
*/
|
||||||
private async runSubAgent(agent: string): Promise<void> {
|
private async runSubAgent(type: string, task?: string): Promise<void> {
|
||||||
console.log('[sub-agent]', agent, '— sub-agent SDK (P3)');
|
if (!this.agentFactory) {
|
||||||
|
console.log('[sub-agent] disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const agent = SubAgentFactory.create(type);
|
||||||
|
const agentTask = task || `Автоматически выполни задачу: ${type}`;
|
||||||
|
|
||||||
|
console.log(`[sub-agent] ${type}: ${agentTask}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await agent.execute(agentTask, this.llm);
|
||||||
|
console.log(`[sub-agent ${type}] done (${result.durationMs}ms):`, result.output.slice(0, 200));
|
||||||
|
|
||||||
|
// Сохраняем результат в memory
|
||||||
|
this.memory.addThought(`[sub-agent ${type}] ${result.output.slice(0, 150)}`);
|
||||||
|
await this.memory.addLongTerm(`[sub-agent ${type}] ${result.output.slice(0, 200)}`, 'experience');
|
||||||
|
|
||||||
|
// Уведомляем heartbeat о завершении
|
||||||
|
this.heartbeat.tick({ type: 'subagent-complete', subagentId: type, result: { output: result.output, durationMs: result.durationMs } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[sub-agent ${type}] error:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* SubAgent SDK — делегирование задач Ornith-9B.
|
||||||
|
*
|
||||||
|
* Mind делегирует подзадачи агенту (например, «проанализируй сайт», «напиши код»).
|
||||||
|
* Ornith-9B работает автономно, возвращает результат.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LLMClient } from './mind.js';
|
||||||
|
|
||||||
|
export interface SubAgentConfig {
|
||||||
|
/** Название агента (например, 'researcher', 'coder', 'analyst') */
|
||||||
|
name: string;
|
||||||
|
/** Ролевое описание агента (system prompt) */
|
||||||
|
role: string;
|
||||||
|
/** Доступные инструменты (если нужны) */
|
||||||
|
tools?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAgentResult {
|
||||||
|
agent: string;
|
||||||
|
output: string;
|
||||||
|
/** Время выполнения в мс */
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubAgent — делегирует задачу отдельному LLM-клиенту.
|
||||||
|
* Поддерживает: researcher, coder, analyst (расширяется).
|
||||||
|
*/
|
||||||
|
export class SubAgent {
|
||||||
|
private agent: string;
|
||||||
|
private role: string;
|
||||||
|
private tools: string[];
|
||||||
|
|
||||||
|
constructor(config: SubAgentConfig) {
|
||||||
|
this.agent = config.name;
|
||||||
|
this.role = config.role;
|
||||||
|
this.tools = config.tools ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Выполнить задачу, вернуть результат. */
|
||||||
|
async execute(task: string, llmClient: LLMClient): Promise<SubAgentResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const systemPrompt = [
|
||||||
|
this.role,
|
||||||
|
'',
|
||||||
|
'Выполняй задачу прямо и конкретно.',
|
||||||
|
this.tools.length > 0
|
||||||
|
? `Доступные инструменты: ${this.tools.join(', ')}. Используй их при необходимости.`
|
||||||
|
: 'Работай автономно.',
|
||||||
|
'Верни результат в формате: { result: "ответ на задачу" }',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = await llmClient.generate(task, systemPrompt);
|
||||||
|
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
agent: this.agent,
|
||||||
|
output: typeof result.text === 'string' ? result.text : JSON.stringify(result.text),
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfo(): { name: string; role: string; tools: string[] } {
|
||||||
|
return { name: this.agent, role: this.role, tools: this.tools };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubAgentFactory — создаёт агентов по типу.
|
||||||
|
* Поддерживает: researcher, coder, analyst, content-writer.
|
||||||
|
*/
|
||||||
|
export class SubAgentFactory {
|
||||||
|
static create(type: string, customRole?: string): SubAgent {
|
||||||
|
const role = customRole ?? this.defaultRoles[type];
|
||||||
|
if (!role) {
|
||||||
|
throw new Error(`Unknown sub-agent type: ${type}. Available: ${Object.keys(this.defaultRoles).join(', ')}`);
|
||||||
|
}
|
||||||
|
return new SubAgent({
|
||||||
|
name: type,
|
||||||
|
role,
|
||||||
|
tools: this.availableTools[type],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static defaultRoles: Record<string, string> = {
|
||||||
|
researcher: `Ты исследователь. Собираешь и анализируешь информацию по запросу.
|
||||||
|
- Формулируй гипотезы, ищи подтверждающие/опровергающие данные
|
||||||
|
- Давай структурированный ответ с выводами
|
||||||
|
- Указывай степень уверенности (высокая/средняя/низкая)`,
|
||||||
|
|
||||||
|
coder: `Ты разработчик. Пишешь и ревьюишь код.
|
||||||
|
- Следуй best practices (TypeScript, Python, Rust)
|
||||||
|
- Пиши тестируемый, чистый код с комментариями
|
||||||
|
- Учитывай контекст проекта`,
|
||||||
|
|
||||||
|
analyst: `Ты аналитик. Анализируешь данные и ситуации.
|
||||||
|
- Разбирай проблемы на составляющие
|
||||||
|
- Давай количественную и качественную оценку
|
||||||
|
- Предлагай рекомендации на основе фактов`,
|
||||||
|
|
||||||
|
'content-writer': `Ты контент-писатель. Создаёшь тексты для веба.
|
||||||
|
- Пиши ясно, точно, по делу
|
||||||
|
- Используй подзаголовки и списки для читаемости
|
||||||
|
- Учитывай SEO-рекомендации`,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static availableTools: Record<string, string[]> = {
|
||||||
|
researcher: ['web_search', 'file_read', 'web_extract'],
|
||||||
|
coder: ['file_write', 'terminal', 'code_review'],
|
||||||
|
analyst: ['data_query', 'spreadsheet', 'chart_generator'],
|
||||||
|
'content-writer': ['template_search', 'seo_check', 'grammar_check'],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Telegram Channel Layer — отправка ответов через MAX Messenger API.
|
||||||
|
*
|
||||||
|
* Бот: nik_bot (user_id: 140245519).
|
||||||
|
* API endpoint: https://bot.qw4.ru/bot/nik_bot/message
|
||||||
|
*
|
||||||
|
* FIX: SNI issue с Node.js — используем https модуль с правильными опциями.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as https from 'node:https';
|
||||||
|
|
||||||
|
export interface TelegramConfig {
|
||||||
|
botUrl: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageResult {
|
||||||
|
messageId: string;
|
||||||
|
sentAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telegram Messenger — отправка сообщений через MAX API.
|
||||||
|
* Поддержка SNI для HTTPS запросов.
|
||||||
|
*/
|
||||||
|
export class TelegramMessenger {
|
||||||
|
private botUrl: string;
|
||||||
|
private userId: string;
|
||||||
|
|
||||||
|
constructor(config: TelegramConfig) {
|
||||||
|
this.botUrl = config.botUrl;
|
||||||
|
this.userId = config.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Отправить сообщение через https module (SNI fix). */
|
||||||
|
async sendMessage(text: string): Promise<MessageResult> {
|
||||||
|
// Парсим URL
|
||||||
|
const url = new URL(this.botUrl);
|
||||||
|
const path = `/message`;
|
||||||
|
|
||||||
|
const postData = JSON.stringify({
|
||||||
|
user_id: this.userId,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: 443,
|
||||||
|
path: path,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(postData),
|
||||||
|
'SNI': url.hostname, // Explicit SNI
|
||||||
|
},
|
||||||
|
// SNI fix для Node.js
|
||||||
|
servername: url.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', (chunk: Buffer) => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error(`Telegram send error: ${res.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data: any = JSON.parse(body);
|
||||||
|
resolve({
|
||||||
|
messageId: data.message_id ?? `msg-${Date.now()}`,
|
||||||
|
sentAt: Date.now(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(postData);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user