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:
1 parent
89b73db8a4
commit
a01f183680
5 files changed
+27
-99
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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 к оркестратору — теперь тики реально обрабатываются
|
||||
|
||||
+16
-6
@@ -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;
|
||||
|
||||
@@ -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-поток работает');
|
||||
})();
|
||||
@@ -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 — полный пайплайн работает');
|
||||
})();
|
||||
Reference in new issue
Block a user