diff --git a/src/mind.ts b/src/mind.ts index d6b251f..2ec78d5 100644 --- a/src/mind.ts +++ b/src/mind.ts @@ -45,6 +45,20 @@ export class SparkLLMClient implements LLMClient { constructor(private baseUrl: string, private modelName = 'spark-x2.5-4b') {} async generate(context: string, systemPrompt: string): Promise { + // Инструкция structured output — только {thoughts, emotion, decision.reply}. + // subAgent НЕ включаем: gemma4:E4B (и другие локальные модели) пумолкают + // (пустой ответ), когда subAgent есть в JSON-схеме. subAgent — P3, + // появится в sub-agent SDK. + const structuredOutput = [ + 'Отвечай ТОЛЬКО валидным JSON без оградок и комментариев.', + 'Структура:', + '{ "thoughts": "собственные мысли mind",', + ' "emotion": { "valence": 0..1, "arousal": 0..1 },', + ' "decision": { "reply": "текст ответа или null" }', + '}', + 'thoughts и reply — на русском.', + ].join('\n'); + // OpenAI-compatible /v1/chat/completions const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, { method: 'POST', @@ -53,7 +67,7 @@ export class SparkLLMClient implements LLMClient { model: this.modelName, response_format: { type: 'json_object' }, // structured output messages: [ - { role: 'system', content: systemPrompt }, + { role: 'system', content: `${systemPrompt}\n\n${structuredOutput}` }, { role: 'user', content: context }, ], temperature: 0.7, @@ -69,13 +83,19 @@ export class SparkLLMClient implements LLMClient { } function parseSparkOutput(content: string): LLMResult { - // 1. Пробуем распарсить JSON (structured output) + // 1. Убираем markdown-оградки (```json ... ```), если модель их добавила + const cleaned = content + .replace(/^```(?:json)?\s*/i, '') + .replace(/\s*```$/i, '') + .trim(); + + // 2. Пробуем распарсить JSON (structured output) let parsed: any = null; try { - parsed = JSON.parse(content); + parsed = JSON.parse(cleaned); } catch { - // fallback: ищем JSON-блок в тексте - const match = content.match(/({[\s\S]*})/); + // fallback: ищем первый {...} блок в тексте + const match = cleaned.match(/({[\s\S]*})/); if (match) { try { parsed = JSON.parse(match[1]); } catch {} } diff --git a/test-integration.ts b/test-integration.ts new file mode 100644 index 0000000..06fda27 --- /dev/null +++ b/test-integration.ts @@ -0,0 +1,52 @@ +/** + * Интеграционный тест полного пайплайна (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 — полный пайплайн работает'); +})();