diff --git a/README.md b/README.md index 2e9c593..f21360d 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ Self-hosted автономный агент (MVP). Концепт позаимствован из [Animus](https://github.com/Craigtut/animus) (Craigtut): heartbeat, -4 слоя памяти, persona в 8 слоёв. Движок — **Spark X2.5-4B** (локально, self-hosted). +4 слоя памяти, persona в 8 слоёв. Движок — **Ornith-1.5-9B** (локально, self-hosted, Ollama). -## Почему Spark X2.5-4B +## Почему Ornith-1.5-9B -- Локальный inference (llama.cpp / Ollama), никаких облачных ключей -- Нативная поддержка agent-инструментов (tool-calling) — саб-агенты -- 1M контекст (для heartbeat держим 4K-16K + внешняя память) -- ~59 t/s на Q4_K_M (RTX A2000 12GB) +- Локальный inference (Ollama), никаких облачных ключей +- Нативная поддержка tool-calling (саб-агенты) + thinking +- 262K контекст (для heartbeat держим 4K-16K + внешняя память) +- ~40-60 t/s на Q6_K -> ⚠️ 4B — это слабость для сложного reasoning. Стратегия: -> heartbeat-мысли на Spark (быстро/дёшево), саб-агенты на Ornith-9B (качественнее). +> ⚠️ Spark X2.5-4B не грузится в Ollama (unknown architecture 'spark2_5'). +> Ornith-9B — рабочая замена: thinking + tools, дефицитный ресурс. ## Архитектура @@ -57,9 +57,8 @@ npm run build # tsc -p tsconfig.json (проверка типов) npm start # node dist/index.js ``` -Spark X2.5-4B должен быть доступен по OpenAI-совместимому API: -- Ollama: `http://localhost:11434` -- llama.cpp: `http://localhost:8080/v1` +Ornith-1.5-9B должен быть доступен по OpenAI-совместимому API (Ollama): +- Ollama: `http://localhost:11434` (модель `ornith-1.5-9b:latest`) ## Roadmap diff --git a/index.ts b/index.ts index aafb518..3116925 100644 --- a/index.ts +++ b/index.ts @@ -27,7 +27,7 @@ const mind = new Mind( heartbeat, memory, DEFAULT_PERSONA, - new SparkLLMClient('http://localhost:11434'), // Ollama: Spark X2.5-4B + new SparkLLMClient('http://localhost:11434', 'ornith-1.5-9b:latest'), // Ornith-9B — движок ); // Подключаем heartbeat к оркестратору — теперь тики реально обрабатываются diff --git a/src/mind.ts b/src/mind.ts index 2ec78d5..a77f12c 100644 --- a/src/mind.ts +++ b/src/mind.ts @@ -3,10 +3,10 @@ * Mind не выполняет долгую работу сам — он оркестратор: * думает, чувствует, решает, делегирует саб-агентам. * - * Spark X2.5-4B как движок: - * - через llama.cpp/Ollama (OpenAI-compatible server) + * Ornith-1.5-9B как движок: + * - через Ollama (OpenAI-compatible /v1/server) * - поддерживает tool-calling (саб-агенты) - * - 1M контекст, но для heartbeat держим 4K-16K + внешняя память + * - 262K контекст, но для heartbeat держим 4K-16K + внешняя память */ import { Heartbeat, TickTrigger } from './heartbeat.js'; @@ -102,15 +102,17 @@ function parseSparkOutput(content: string): LLMResult { } if (parsed && typeof parsed === 'object') { + const thoughts = tryParseJson(parsed.thoughts); + const reply = tryParseJson(parsed.decision?.reply ?? parsed.reply); return { - text: parsed.text ?? parsed.reply ?? '', - thoughts: parsed.thoughts ?? '', + text: typeof parsed.text === 'string' ? parsed.text : '', + thoughts: typeof thoughts === 'string' ? thoughts : typeof thoughts === 'object' ? JSON.stringify(thoughts) : '', emotion: { valence: clampNum(parsed.emotion?.valence ?? parsed.valence, 0.5), arousal: clampNum(parsed.emotion?.arousal ?? parsed.arousal, 0.5), }, decision: { - reply: parsed.decision?.reply ?? parsed.reply ?? null, + reply: typeof reply === 'string' ? reply : reply ? JSON.stringify(reply) : null, subAgent: parsed.decision?.subAgent ?? parsed.subAgent ?? undefined, action: parsed.decision?.action ?? parsed.action ?? undefined, }, @@ -130,6 +132,14 @@ function clampNum(v: number, fallback: number): number { return typeof v === 'number' && !Number.isNaN(v) ? v : fallback; } +/** Если значение — строка с JSON (модель запарила вложенный объект), распарсим. */ +function tryParseJson(v: unknown): unknown { + if (typeof v !== 'string') return v; + const trimmed = v.trim(); + if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return v; + try { return JSON.parse(trimmed); } catch { return v; } +} + export class Mind { private heartbeat: Heartbeat; private memory: MemorySystem; diff --git a/test-embedding.ts b/test-embedding.ts deleted file mode 100644 index 92002b4..0000000 --- a/test-embedding.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Тест embedding-потока (P2). - * Проверка: bge-m3 эмбеддит, long-term ищет по cosine, consolidation работает. - */ -import { OllamaEmbeddingClient, cosineSimilarity } from './src/embedding.js'; -import { MemorySystem } from './src/memory.js'; - -(async () => { -const embedding = new OllamaEmbeddingClient('http://localhost:11434'); -const memory = new MemorySystem(10, embedding); - -console.log('[test] эмбеддим 3 тестовые фразы...'); -await memory.addLongTerm('Как настроить Yandex Direct для локального бизнеса', 'fact'); -await memory.addLongTerm('Сайт-лидер рынка: структура и SEO', 'procedure'); -await memory.addLongTerm('Ипотечный калькулятор повышает конверсию', 'outcome'); - -console.log('[test] long-term наполнен, ищем по запросу...'); -const results = await memory.searchLongTerm('Как снизить CPA в Директе?'); -console.log('[test] top-K результатов:'); -for (const m of results) { - console.log(` - ${m.type}: ${m.text}`); -} - -console.log('[test] consolidation переносит мысли в long-term...'); -await memory.consolidate([ - { id: 'th-1', text: 'Нужно настроить sub-agent для Ornith-9B', createdAt: Date.now() }, -]); -console.log('[test] OK — embedding-поток работает'); -})(); diff --git a/test-integration.ts b/test-integration.ts deleted file mode 100644 index 06fda27..0000000 --- a/test-integration.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Интеграционный тест полного пайплайна (P1). - * Проверка: LLM (gemma4:E4B) → parseSparkOutput → memory → consolidation. - * Spark X2.5-4B в этом Ollama не грузится (unknown architecture), поэтому - * временно используем gemma4:E4B. - */ -import { SparkLLMClient } from './src/mind.js'; -import { MemorySystem } from './src/memory.js'; -import { OllamaEmbeddingClient } from './src/embedding.js'; -import { DEFAULT_PERSONA } from './src/persona.js'; - -(async () => { -const llm = new SparkLLMClient('http://localhost:11434', 'gemma4:E4B'); -const embedding = new OllamaEmbeddingClient('http://localhost:11434'); -const memory = new MemorySystem(10, embedding); - -const systemPrompt = 'Ты ассистент. Отвечай JSON: {thoughts, emotion:{valence,arousal}, decision:{reply, subAgent}}'; -const context = 'Ты думал про Yandex Direct. Напиши мысли и решение.'; - -console.log('[test] LLM generate...'); -const result = await llm.generate(context, systemPrompt); -console.log('[test] thoughts:', JSON.stringify(result.thoughts)); -console.log('[test] emotion:', JSON.stringify(result.emotion)); -console.log('[test] decision:', JSON.stringify(result.decision)); - -// Raw-запрос, чтобы увидеть что реально вернула модель -const raw = await fetch('http://localhost:11434/v1/chat/completions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gemma4:E4B', - response_format: { type: 'json_object' }, - messages: [ - { role: 'system', content: 'Отвечай только JSON: {thoughts, emotion:{valence,arousal}, decision:{reply}}' }, - { role: 'user', content: context }, - ], - temperature: 0.7, - max_tokens: 512, - }), -}); -const rawText = await (await raw.json()).choices?.[0]?.message?.content ?? ''; -console.log('[test] RAW:', JSON.stringify(rawText)); - -console.log('[test] addThought...'); -memory.addThought(result.thoughts); - -console.log('[test] consolidation...'); -await memory.consolidate(memory.recentThoughts()); -console.log('[test] long-term size:', memory['longTerm'].length); - -console.log('[test] OK — полный пайплайн работает'); -})();