P1: robust JSON structured output (markdown fence cleanup, structured-output instruction, subAgent excluded from base schema to prevent gemma4:E4B silence); P2: bge-m3 embeddings via Ollama + cosine search; full pipeline verified on gemma4:E4B
This commit is contained in:
1 parent
a6fde100ca
commit
89b73db8a4
2 files changed
+77
-5
No files matched your search
+25
-5
@@ -45,6 +45,20 @@ export class SparkLLMClient implements LLMClient {
|
|||||||
constructor(private baseUrl: string, private modelName = 'spark-x2.5-4b') {}
|
constructor(private baseUrl: string, private modelName = 'spark-x2.5-4b') {}
|
||||||
|
|
||||||
async generate(context: string, systemPrompt: string): Promise<LLMResult> {
|
async generate(context: string, systemPrompt: string): Promise<LLMResult> {
|
||||||
|
// Инструкция 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
|
// OpenAI-compatible /v1/chat/completions
|
||||||
const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -53,7 +67,7 @@ export class SparkLLMClient implements LLMClient {
|
|||||||
model: this.modelName,
|
model: this.modelName,
|
||||||
response_format: { type: 'json_object' }, // structured output
|
response_format: { type: 'json_object' }, // structured output
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: `${systemPrompt}\n\n${structuredOutput}` },
|
||||||
{ role: 'user', content: context },
|
{ role: 'user', content: context },
|
||||||
],
|
],
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
@@ -69,13 +83,19 @@ export class SparkLLMClient implements LLMClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseSparkOutput(content: string): LLMResult {
|
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;
|
let parsed: any = null;
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(content);
|
parsed = JSON.parse(cleaned);
|
||||||
} catch {
|
} catch {
|
||||||
// fallback: ищем JSON-блок в тексте
|
// fallback: ищем первый {...} блок в тексте
|
||||||
const match = content.match(/({[\s\S]*})/);
|
const match = cleaned.match(/({[\s\S]*})/);
|
||||||
if (match) {
|
if (match) {
|
||||||
try { parsed = JSON.parse(match[1]); } catch {}
|
try { parsed = JSON.parse(match[1]); } catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 — полный пайплайн работает');
|
||||||
|
})();
|
||||||
Reference in new issue
Block a user