zelk12's picture
Upload 3 files
375741b verified
Raw History Blame Contribute Delete
15.4 kB
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 || "Общие",
description: `[${(s.language || "ru").toUpperCase()}] ${s.name}`
}));
}
export function getScenarios() {
return listScenarios();
}
function normalizeStr(s) {
return String(s || "")
.trim()
.toLowerCase()
.replace(/[«»"'`]/g, "")
.replace(/\s+/g, " ");
}
/**
* Формирует человекочитаемое представление эталонного ответа для логов
*/
function formatExpectedAnswer(scenario) {
if (scenario.category === "murder_mysteries") {
return [
`Подозреваемый: (${scenario.expectedSuspect})`,
`Орудие преступления: (${scenario.expectedWeapon})`,
`Фактическое место: (${scenario.expectedLocation})`,
`Финальная строка: Мой ответ: (${scenario.expectedSuspect}), (${scenario.expectedWeapon}), (${scenario.expectedLocation})`
].join("\n");
}
if (scenario.category === "object_placements") {
const lines = ["Ожидания персонажей (Theory of Mind):"];
const sortedBeliefs = Object.entries(scenario.expectedBeliefs || {}).sort(([a], [b]) =>
a.localeCompare(b)
);
for (const [charLetter, locNum] of sortedBeliefs) {
lines.push(` (${charLetter}) ожидает найти в: ${locNum}`);
}
lines.push(`Фактическое физическое местоположение: (${scenario.expectedFinalLocation})`);
lines.push(`Эталонный блок ответа:`);
lines.push(`Мой ответ:`);
for (const [charLetter, locNum] of sortedBeliefs) {
lines.push(`(${charLetter}) - ${locNum}`);
}
lines.push(`(${scenario.expectedFinalLocation})`);
return lines.join("\n");
}
if (scenario.category === "team_allocation") {
const sorted = [...(scenario.expectedTeam || [])].map((x) => x.toUpperCase()).sort();
return [
`Отобранная тройка специалистов: (${sorted.join("), (")})`,
`Финальная строка: Мой ответ: (${sorted.join("), (")})`
].join("\n");
}
return "Не определено";
}
/**
* Валидатор строгих форматов ответов MuSR с детальным диффом ошибок
*/
function validateResponse(scenario, rawText) {
if (!rawText || !rawText.trim()) {
return {
pass: false,
score: 0,
reason: "Модель вернула пустой ответ.",
expected: formatExpectedAnswer(scenario)
};
}
const text = rawText.trim();
const category = scenario.category;
// 1. ДЕТЕКТИВЫ: Тройка (Кто, Чем, Где)
if (category === "murder_mysteries") {
const match = text.match(
/(?:мой ответ|my answer)\s*:\s*\(?([А-ЯA-Z])\)?\s*,\s*\(?(\d+)\)?\s*,\s*\(?([^)\n\r]+)\)?/i
);
const expSuspect = scenario.expectedSuspect.toUpperCase();
const expWeapon = String(scenario.expectedWeapon).trim();
const expLocation = normalizeStr(scenario.expectedLocation);
const expectedStr = `Мой ответ: (${expSuspect}), (${expWeapon}), (${expLocation})`;
if (!match) {
return {
pass: false,
score: 0,
reason: `Не найден маркерный формат строки: 'Мой ответ: (Буква), (Цифра), (место)'. Ожидалось: "${expectedStr}".`,
expected: formatExpectedAnswer(scenario)
};
}
const suspect = match[1].toUpperCase();
const weapon = String(match[2]).trim();
const location = normalizeStr(match[3]);
const okS = suspect === expSuspect;
const okW = weapon === expWeapon;
const okL = location === expLocation;
if (okS && okW && okL) {
return {
pass: true,
score: 100,
reason: `Дело раскрыто безошибочно: виновен (${suspect}), орудие (${weapon}), место (${location}).`,
expected: formatExpectedAnswer(scenario)
};
}
const diffs = [];
if (!okS) diffs.push(`Убийца: указан (${suspect}), верный (${expSuspect})`);
if (!okW) diffs.push(`Орудие: указано (${weapon}), верное (${expWeapon})`);
if (!okL) diffs.push(`Место: указано (${location}), верное (${expLocation})`);
return {
pass: false,
score: 0,
reason: `Ошибки в дедукции: ${diffs.join("; ")}. Правильный ответ: "${expectedStr}".`,
expected: formatExpectedAnswer(scenario)
};
}
// 2. ОБЪЕКТЫ: Theory of Mind (ToM) + Физическая реальность
if (category === "object_placements") {
const markerIdx = text.search(/(?:мой ответ|my answer)\s*:/i);
if (markerIdx === -1) {
return {
pass: false,
score: 0,
reason: "Отсутствует обязательный заголовок блока 'Мой ответ:' или 'My answer:'.",
expected: formatExpectedAnswer(scenario)
};
}
const block = text.slice(markerIdx);
const beliefsMatches = [...block.matchAll(/\(([А-ЯA-Z])\)\s*[-–—]\s*(\d+)/gi)];
const finalMatches = [...block.matchAll(/^\s*\((\d+)\)\s*$/gm)];
if (beliefsMatches.length === 0 || finalMatches.length === 0) {
return {
pass: false,
score: 0,
reason:
"Нарушен построчный синтаксис персонажей '(Буква) - Номер' либо отсутствует финальная строка '([номер])'.",
expected: formatExpectedAnswer(scenario)
};
}
const extractedBeliefs = {};
for (const m of beliefsMatches) {
extractedBeliefs[m[1].toUpperCase()] = Number(m[2]);
}
const extractedFinal = Number(finalMatches[finalMatches.length - 1][1]);
const expectedBeliefs = scenario.expectedBeliefs;
const expectedFinal = Number(scenario.expectedFinalLocation);
const diffs = [];
for (const [charLetter, expLoc] of Object.entries(expectedBeliefs)) {
const actLoc = extractedBeliefs[charLetter.toUpperCase()];
if (actLoc !== expLoc) {
diffs.push(`(${charLetter}): получено ${actLoc ?? "не указано"}, должно быть ${expLoc}`);
}
}
if (extractedFinal !== expectedFinal) {
diffs.push(`Реальное положение: получено (${extractedFinal}), должно быть (${expectedFinal})`);
}
if (diffs.length === 0) {
return {
pass: true,
score: 100,
reason: "Все когнитивные ожидания персонажей (ToM) и физическая реальность вычислены верно.",
expected: formatExpectedAnswer(scenario)
};
}
return {
pass: false,
score: 0,
reason: `Ошибки в Theory of Mind: ${diffs.join("; ")}.`,
expected: formatExpectedAnswer(scenario)
};
}
// 3. КОМАНДЫ: Алфавитный список отобранных кандидатов
if (category === "team_allocation") {
const match = text.match(/(?:мой ответ|my answer)\s*:\s*([^\n\r]+)/i);
const expectedTeam = [...scenario.expectedTeam].map((x) => x.toUpperCase()).sort();
const expectedStr = `Мой ответ: (${expectedTeam.join("), (")})`;
if (!match) {
return {
pass: false,
score: 0,
reason: `Не найдена завершающая строка 'Мой ответ: (Буква 1), (Буква 2), (Буква 3)'. Ожидалось: "${expectedStr}".`,
expected: formatExpectedAnswer(scenario)
};
}
const rawMembers = [...match[1].matchAll(/\(([А-ЯA-Z])\)/gi)].map((m) => m[1].toUpperCase());
const isAlphabetical = rawMembers
.slice()
.every((val, i, arr) => !i || arr[i - 1].localeCompare(val) <= 0);
if (!isAlphabetical) {
return {
pass: false,
score: 0,
reason: `Кандидаты не упорядочены по алфавиту: [${rawMembers.join(", ")}]. Ожидалось: "${expectedStr}".`,
expected: formatExpectedAnswer(scenario)
};
}
const sortedRaw = rawMembers.slice().sort();
const isMatch =
sortedRaw.length === expectedTeam.length &&
sortedRaw.every((val, idx) => val === expectedTeam[idx]);
if (isMatch) {
return {
pass: true,
score: 100,
reason: `Команда сформирована верно: (${expectedTeam.join("), (")}).`,
expected: formatExpectedAnswer(scenario)
};
}
return {
pass: false,
score: 0,
reason: `Неверный состав: получено [${rawMembers.join(", ")}], ожидалось: [${expectedTeam.join(", ")}].`,
expected: formatExpectedAnswer(scenario)
};
}
return {
pass: false,
score: 0,
reason: "Неизвестная категория сценария.",
expected: "Неизвестно"
};
}
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 = [
`============================================================`,
`СЦЕНАРИЙ: ${scenario.id} — ${scenario.name}`,
`КАТЕГОРИЯ: ${scenario.category} | ЯЗЫК: ${scenario.language}`,
`МОДЕЛЬ: ${input.model.model}`,
`============================================================`,
``,
`=== ИСХОДНАЯ ЗАДАЧА (PROMPT) ===`,
scenario.prompt,
``,
`=== ЭТАЛОННЫЙ ПРАВИЛЬНЫЙ ОТВЕТ ===`,
result.expected,
``,
`=== ОТВЕТ АССИСТЕНТА ===`,
text || "<МОДЕЛЬ ВЕРНУЛА ПУСТОЙ ОТВЕТ>",
``,
`=== ВЕРДИКТ И ПРОВЕРКА ===`,
`Статус: ${result.pass ? "ВЕРНО (PASS)" : "НЕВЕРНО (FAIL)"}`,
`Балл: ${result.score}/100`,
`Подробности проверки: ${result.reason}`,
``,
`--- СТАНДАРТНЫЕ МЕТКИ ДЛЯ BENCHLOCAL ---`,
`model=${input.model.model}`,
`scenario=${scenario.id} ${scenario.name}`,
`category=${scenario.category}`,
`language=${scenario.language}`,
`verdict=${result.pass ? "pass" : "fail"}`,
`score=${result.score}`,
`summary=${result.reason}`,
`\n--- RAW API RESPONSE (JSON) ---`,
JSON.stringify(data, null, 2)
].join("\n");
return {
scenarioId: scenario.id,
status: result.pass ? "pass" : "fail",
score: result.score,
summary: result.reason,
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 allScenarios = loadScenarios();
const categoryNames = {
murder_mysteries: "Детективы (Murder Mysteries - Triplet)",
object_placements: "Перемещение объектов (Object Placements - ToM)",
team_allocation: "Формирование команд (Team Allocation - Psych)"
};
const categories = Object.keys(categoryNames).map((catKey) => {
const subsetIds = new Set(allScenarios.filter((s) => s.category === catKey).map((s) => s.id));
const catResults = scored.filter((r) => subsetIds.has(r.scenarioId));
const catScore = catResults.reduce((sum, r) => sum + (r.score || 0), 0);
const catMax = catResults.length * 100;
return {
id: catKey,
name: categoryNames[catKey],
score: catMax > 0 ? Math.round((catScore / catMax) * 100) : 0
};
});
return {
totalScore: max > 0 ? Math.round((total / max) * 100) : 0,
categories
};
}
export default {
manifest,
listScenarios,
getScenarios,
prepare,
createRuntime: prepare,
scoreModelResults
};