import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const manifestPath = path.join(__dirname, "benchlocal.pack.json"); const scenariosPath = path.join(__dirname, "scenarios.json"); export const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); function loadScenarios() { return JSON.parse(fs.readFileSync(scenariosPath, "utf8")); } export function listScenarios() { return loadScenarios().map((s) => ({ id: s.id, name: s.name, category: s.category || "General", description: `[${(s.language || "ru").toUpperCase()}] ${s.name}` })); } export function getScenarios() { return listScenarios(); } /** * Валидатор: ищет строго заданный маркер "Мой ответ: (БУКВА)" * Модель может свободно рассуждать, но в конце обязана выдать именно этот маркер. * Весь промежуточный текст, формулы и размышления игнорируются. */ function validateResponse(scenario, rawText) { const text = (rawText || "").trim(); const expected = (scenario.correctAnswer || "").trim().toUpperCase(); // Ищем маркер формата: "Мой ответ: (БУКВА)" (или англоязычный аналог "My answer: (LETTER)") const pattern = /(?:Мой\s*ответ|My\s*answer):\s*\(([A-J])\)/gi; const matches = [...text.matchAll(pattern)]; if (matches.length === 0) { return { pass: false, score: 0, extracted: null, reason: `Нарушен формат: не найдена обязательная конструкция 'Мой ответ: (БУКВА)'. Окончание ответа: "${text.slice(-100)}"` }; } // Берем последнее вхождение маркера в тексте (итоговый вердикт модели) const lastMatch = matches[matches.length - 1]; const extracted = lastMatch[1].toUpperCase(); if (extracted !== expected) { return { pass: false, score: 0, extracted, reason: `Неверный ответ: модель выбрала '(${extracted})', ожидалось '(${expected})'` }; } return { pass: true, score: 100, extracted, reason: `Успех: корректно выбран вариант '(${extracted})' в строгом формате 'Мой ответ: (X)'` }; } function resolveHelpers(context) { if (context && typeof context.getRequiredProvider === "function") { return context; } if (context?.helpers && typeof context.helpers.getRequiredProvider === "function") { return context.helpers; } return { getRequiredProvider(providerId) { const providers = context?.providers || []; const found = Array.isArray(providers) ? providers.find((p) => p.id === providerId || p.provider === providerId) : providers[providerId]; return found || { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "none" }; }, getInferenceEndpoint(modelId) { return context?.endpoints?.[modelId] || null; } }; } export async function prepare(context) { const helpers = resolveHelpers(context); return { async runScenario(input) { const allScenarios = loadScenarios(); const scenario = allScenarios.find((s) => s.id === input.scenario.id); if (!scenario) { return { scenarioId: input.scenario.id, status: "fail", score: 0, summary: "Сценарий не найден", rawLog: "error=Scenario not found" }; } const provider = helpers.getRequiredProvider(input.model.provider, { enabledOnly: true }); const inference = helpers.getInferenceEndpoint ? helpers.getInferenceEndpoint(input.model.id) : null; const rawBaseUrl = (inference && inference.status === "running" ? inference.baseUrl : provider?.baseUrl) || "http://127.0.0.1:11434/v1"; const baseUrl = rawBaseUrl.replace(/\/+$/, ""); const apiKey = provider?.apiKey || "none"; const requestBody = { model: input.model.model, messages: [{ role: "user", content: scenario.prompt }], temperature: 0 }; const response = await fetch(`${baseUrl}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` }, body: JSON.stringify(requestBody) }); const data = await response.json(); const text = (data.choices?.[0]?.message?.content || "").trim(); const result = validateResponse(scenario, text); const rawLog = [ `model=${input.model.model}`, `category=${scenario.category}`, `language=${scenario.language}`, `scenario=${scenario.id} - ${scenario.name}`, `prompt=${scenario.prompt}`, `assistant_full_cot_response=${text}`, `extracted_letter=${result.extracted || "NONE"}`, `expected_letter=${scenario.correctAnswer}`, `verdict=${result.pass ? "pass" : "fail"}`, `score=${result.score}`, `summary=${result.reason}`, `\n--- RAW API RESPONSE ---`, JSON.stringify(data, null, 2) ].join("\n"); return { scenarioId: scenario.id, category: scenario.category, language: scenario.language, status: result.pass ? "pass" : "fail", score: result.score, summary: result.reason, rawLog: rawLog }; }, async dispose() {} }; } export function scoreModelResults(results) { const scored = results || []; const total = scored.reduce((sum, r) => sum + (r.score || 0), 0); const max = scored.length * 100; const catMap = new Map(); for (const r of scored) { const cat = r.category || "General"; if (!catMap.has(cat)) { catMap.set(cat, { total: 0, count: 0 }); } const entry = catMap.get(cat); entry.total += r.score || 0; entry.count += 1; } const categories = Array.from(catMap.entries()).map(([name, data]) => ({ name, score: data.count > 0 ? Math.round((data.total / (data.count * 100)) * 100) : 0, count: data.count })); return { totalScore: max > 0 ? Math.round((total / max) * 100) : 0, categories }; } export default { manifest, listScenarios, getScenarios, prepare, createRuntime: prepare, scoreModelResults };