Switch LLM engine to Ornith-1.5-9B (Spark X2.5-4B not loadable in Ollama); robust nested-JSON parsing; update README

This commit is contained in:
n1kt4r committed 2026-09-14 17:28:40 +05:00
1 parent 89b73db8a4
commit a01f183680
5 files changed
+27 -99

No files matched your search

+10 -11
View File
@@ -2,17 +2,17 @@
Self-hosted автономный агент (MVP). Концепт позаимствован из Self-hosted автономный агент (MVP). Концепт позаимствован из
[Animus](https://github.com/Craigtut/animus) (Craigtut): heartbeat, [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), никаких облачных ключей - Локальный inference (Ollama), никаких облачных ключей
- Нативная поддержка agent-инструментов (tool-calling) — саб-агенты - Нативная поддержка tool-calling (саб-агенты) + thinking
- 1M контекст (для heartbeat держим 4K-16K + внешняя память) - 262K контекст (для heartbeat держим 4K-16K + внешняя память)
- ~59 t/s на Q4_K_M (RTX A2000 12GB) - ~40-60 t/s на Q6_K
> ⚠️ 4B — это слабость для сложного reasoning. Стратегия: > ⚠️ Spark X2.5-4B не грузится в Ollama (unknown architecture 'spark2_5').
> heartbeat-мысли на Spark (быстро/дёшево), саб-агенты на Ornith-9B (качественнее). > Ornith-9B — рабочая замена: thinking + tools, дефицитный ресурс.
## Архитектура ## Архитектура
@@ -57,9 +57,8 @@ npm run build # tsc -p tsconfig.json (проверка типов)
npm start # node dist/index.js npm start # node dist/index.js
``` ```
Spark X2.5-4B должен быть доступен по OpenAI-совместимому API: Ornith-1.5-9B должен быть доступен по OpenAI-совместимому API (Ollama):
- Ollama: `http://localhost:11434` - Ollama: `http://localhost:11434` (модель `ornith-1.5-9b:latest`)
- llama.cpp: `http://localhost:8080/v1`
## Roadmap ## Roadmap
+1 -1
View File
@@ -27,7 +27,7 @@ const mind = new Mind(
heartbeat, heartbeat,
memory, memory,
DEFAULT_PERSONA, 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 к оркестратору — теперь тики реально обрабатываются // Подключаем heartbeat к оркестратору — теперь тики реально обрабатываются
+16 -6
View File
@@ -3,10 +3,10 @@
* Mind не выполняет долгую работу сам — он оркестратор: * Mind не выполняет долгую работу сам — он оркестратор:
* думает, чувствует, решает, делегирует саб-агентам. * думает, чувствует, решает, делегирует саб-агентам.
* *
* Spark X2.5-4B как движок: * Ornith-1.5-9B как движок:
* - через llama.cpp/Ollama (OpenAI-compatible server) * - через Ollama (OpenAI-compatible /v1/server)
* - поддерживает tool-calling (саб-агенты) * - поддерживает tool-calling (саб-агенты)
* - 1M контекст, но для heartbeat держим 4K-16K + внешняя память * - 262K контекст, но для heartbeat держим 4K-16K + внешняя память
*/ */
import { Heartbeat, TickTrigger } from './heartbeat.js'; import { Heartbeat, TickTrigger } from './heartbeat.js';
@@ -102,15 +102,17 @@ function parseSparkOutput(content: string): LLMResult {
} }
if (parsed && typeof parsed === 'object') { if (parsed && typeof parsed === 'object') {
const thoughts = tryParseJson(parsed.thoughts);
const reply = tryParseJson(parsed.decision?.reply ?? parsed.reply);
return { return {
text: parsed.text ?? parsed.reply ?? '', text: typeof parsed.text === 'string' ? parsed.text : '',
thoughts: parsed.thoughts ?? '', thoughts: typeof thoughts === 'string' ? thoughts : typeof thoughts === 'object' ? JSON.stringify(thoughts) : '',
emotion: { emotion: {
valence: clampNum(parsed.emotion?.valence ?? parsed.valence, 0.5), valence: clampNum(parsed.emotion?.valence ?? parsed.valence, 0.5),
arousal: clampNum(parsed.emotion?.arousal ?? parsed.arousal, 0.5), arousal: clampNum(parsed.emotion?.arousal ?? parsed.arousal, 0.5),
}, },
decision: { 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, subAgent: parsed.decision?.subAgent ?? parsed.subAgent ?? undefined,
action: parsed.decision?.action ?? parsed.action ?? 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; 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 { export class Mind {
private heartbeat: Heartbeat; private heartbeat: Heartbeat;
private memory: MemorySystem; private memory: MemorySystem;
-29
View File
@@ -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-поток работает');
})();
-52
View File
@@ -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 — полный пайплайн работает');
})();