zelk12 commited on
Commit
375741b
·
verified ·
1 Parent(s): a4347dc

Upload 3 files

Browse files
MuSR-mini/benchlocal.pack.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 1,
3
+ "protocolVersion": 1,
4
+ "id": "musr-narrative-prose",
5
+ "name": "MuSR Literary & Cognitive (Long Narrative Prose)",
6
+ "version": "3.0.0",
7
+ "description": "Полнотекстовый бенчмарк многошаговых мягких рассуждений в виде художественных рассказов: детективный дедуктивный триплет, когнитивная модель психики (ToM) с неизоморфными графами и социально-психологический отбор команд со строгой XOR-логикой.",
8
+ "entry": "./index.mjs",
9
+ "samplingDefaults": {
10
+ "temperature": 0.0
11
+ },
12
+ "capabilities": {
13
+ "tools": false,
14
+ "multiTurn": false,
15
+ "streamingProgress": false,
16
+ "verification": false
17
+ }
18
+ }
MuSR-mini/index.mjs ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const manifestPath = path.join(__dirname, "benchlocal.pack.json");
7
+ const scenariosPath = path.join(__dirname, "scenarios.json");
8
+
9
+ export const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
10
+
11
+ function loadScenarios() {
12
+ return JSON.parse(fs.readFileSync(scenariosPath, "utf8"));
13
+ }
14
+
15
+ export function listScenarios() {
16
+ return loadScenarios().map((s) => ({
17
+ id: s.id,
18
+ name: s.name,
19
+ category: s.category || "Общие",
20
+ description: `[${(s.language || "ru").toUpperCase()}] ${s.name}`
21
+ }));
22
+ }
23
+
24
+ export function getScenarios() {
25
+ return listScenarios();
26
+ }
27
+
28
+ function normalizeStr(s) {
29
+ return String(s || "")
30
+ .trim()
31
+ .toLowerCase()
32
+ .replace(/[«»"'`]/g, "")
33
+ .replace(/\s+/g, " ");
34
+ }
35
+
36
+ /**
37
+ * Формирует человекочитаемое представление эталонного ответа для логов
38
+ */
39
+ function formatExpectedAnswer(scenario) {
40
+ if (scenario.category === "murder_mysteries") {
41
+ return [
42
+ `Подозреваемый: (${scenario.expectedSuspect})`,
43
+ `Орудие преступления: (${scenario.expectedWeapon})`,
44
+ `Фактическое место: (${scenario.expectedLocation})`,
45
+ `Финальная строка: Мой ответ: (${scenario.expectedSuspect}), (${scenario.expectedWeapon}), (${scenario.expectedLocation})`
46
+ ].join("\n");
47
+ }
48
+
49
+ if (scenario.category === "object_placements") {
50
+ const lines = ["Ожидания персонажей (Theory of Mind):"];
51
+ const sortedBeliefs = Object.entries(scenario.expectedBeliefs || {}).sort(([a], [b]) =>
52
+ a.localeCompare(b)
53
+ );
54
+ for (const [charLetter, locNum] of sortedBeliefs) {
55
+ lines.push(` (${charLetter}) ожидает найти в: ${locNum}`);
56
+ }
57
+ lines.push(`Фактическое физическое местоположение: (${scenario.expectedFinalLocation})`);
58
+ lines.push(`Эталонный блок ответа:`);
59
+ lines.push(`Мой ответ:`);
60
+ for (const [charLetter, locNum] of sortedBeliefs) {
61
+ lines.push(`(${charLetter}) - ${locNum}`);
62
+ }
63
+ lines.push(`(${scenario.expectedFinalLocation})`);
64
+ return lines.join("\n");
65
+ }
66
+
67
+ if (scenario.category === "team_allocation") {
68
+ const sorted = [...(scenario.expectedTeam || [])].map((x) => x.toUpperCase()).sort();
69
+ return [
70
+ `Отобранная тройка специалистов: (${sorted.join("), (")})`,
71
+ `Финальная строка: Мой ответ: (${sorted.join("), (")})`
72
+ ].join("\n");
73
+ }
74
+
75
+ return "Не определено";
76
+ }
77
+
78
+ /**
79
+ * Валидатор строгих форматов ответов MuSR с детальным диффом ошибок
80
+ */
81
+ function validateResponse(scenario, rawText) {
82
+ if (!rawText || !rawText.trim()) {
83
+ return {
84
+ pass: false,
85
+ score: 0,
86
+ reason: "Модель вернула пустой ответ.",
87
+ expected: formatExpectedAnswer(scenario)
88
+ };
89
+ }
90
+
91
+ const text = rawText.trim();
92
+ const category = scenario.category;
93
+
94
+ // 1. ДЕТЕКТИВЫ: Тройка (Кто, Чем, Где)
95
+ if (category === "murder_mysteries") {
96
+ const match = text.match(
97
+ /(?:мой ответ|my answer)\s*:\s*\(?([А-ЯA-Z])\)?\s*,\s*\(?(\d+)\)?\s*,\s*\(?([^)\n\r]+)\)?/i
98
+ );
99
+
100
+ const expSuspect = scenario.expectedSuspect.toUpperCase();
101
+ const expWeapon = String(scenario.expectedWeapon).trim();
102
+ const expLocation = normalizeStr(scenario.expectedLocation);
103
+ const expectedStr = `Мой ответ: (${expSuspect}), (${expWeapon}), (${expLocation})`;
104
+
105
+ if (!match) {
106
+ return {
107
+ pass: false,
108
+ score: 0,
109
+ reason: `Не найден маркерный формат строки: 'Мой ответ: (Буква), (Цифра), (место)'. Ожидалось: "${expectedStr}".`,
110
+ expected: formatExpectedAnswer(scenario)
111
+ };
112
+ }
113
+
114
+ const suspect = match[1].toUpperCase();
115
+ const weapon = String(match[2]).trim();
116
+ const location = normalizeStr(match[3]);
117
+
118
+ const okS = suspect === expSuspect;
119
+ const okW = weapon === expWeapon;
120
+ const okL = location === expLocation;
121
+
122
+ if (okS && okW && okL) {
123
+ return {
124
+ pass: true,
125
+ score: 100,
126
+ reason: `Дело раскрыто безошибочно: виновен (${suspect}), орудие (${weapon}), место (${location}).`,
127
+ expected: formatExpectedAnswer(scenario)
128
+ };
129
+ }
130
+
131
+ const diffs = [];
132
+ if (!okS) diffs.push(`Убийца: указан (${suspect}), верный (${expSuspect})`);
133
+ if (!okW) diffs.push(`Орудие: указано (${weapon}), верное (${expWeapon})`);
134
+ if (!okL) diffs.push(`Место: указано (${location}), верное (${expLocation})`);
135
+
136
+ return {
137
+ pass: false,
138
+ score: 0,
139
+ reason: `Ошибки в дедукции: ${diffs.join("; ")}. Правильный ответ: "${expectedStr}".`,
140
+ expected: formatExpectedAnswer(scenario)
141
+ };
142
+ }
143
+
144
+ // 2. ОБЪЕКТЫ: Theory of Mind (ToM) + Физическая реальность
145
+ if (category === "object_placements") {
146
+ const markerIdx = text.search(/(?:мой ответ|my answer)\s*:/i);
147
+ if (markerIdx === -1) {
148
+ return {
149
+ pass: false,
150
+ score: 0,
151
+ reason: "Отсутствует обязательный заголовок блока 'Мой ответ:' или 'My answer:'.",
152
+ expected: formatExpectedAnswer(scenario)
153
+ };
154
+ }
155
+
156
+ const block = text.slice(markerIdx);
157
+ const beliefsMatches = [...block.matchAll(/\(([А-ЯA-Z])\)\s*[-–—]\s*(\d+)/gi)];
158
+ const finalMatches = [...block.matchAll(/^\s*\((\d+)\)\s*$/gm)];
159
+
160
+ if (beliefsMatches.length === 0 || finalMatches.length === 0) {
161
+ return {
162
+ pass: false,
163
+ score: 0,
164
+ reason:
165
+ "Нарушен построчный синтаксис персонажей '(Буква) - Номер' либо отсутствует финальная строка '([номер])'.",
166
+ expected: formatExpectedAnswer(scenario)
167
+ };
168
+ }
169
+
170
+ const extractedBeliefs = {};
171
+ for (const m of beliefsMatches) {
172
+ extractedBeliefs[m[1].toUpperCase()] = Number(m[2]);
173
+ }
174
+
175
+ const extractedFinal = Number(finalMatches[finalMatches.length - 1][1]);
176
+ const expectedBeliefs = scenario.expectedBeliefs;
177
+ const expectedFinal = Number(scenario.expectedFinalLocation);
178
+
179
+ const diffs = [];
180
+ for (const [charLetter, expLoc] of Object.entries(expectedBeliefs)) {
181
+ const actLoc = extractedBeliefs[charLetter.toUpperCase()];
182
+ if (actLoc !== expLoc) {
183
+ diffs.push(`(${charLetter}): получено ${actLoc ?? "не указано"}, должно быть ${expLoc}`);
184
+ }
185
+ }
186
+
187
+ if (extractedFinal !== expectedFinal) {
188
+ diffs.push(`Реальное положение: получено (${extractedFinal}), должно быть (${expectedFinal})`);
189
+ }
190
+
191
+ if (diffs.length === 0) {
192
+ return {
193
+ pass: true,
194
+ score: 100,
195
+ reason: "Все когнитивные ожидания персонажей (ToM) и физическая реальность вычислены верно.",
196
+ expected: formatExpectedAnswer(scenario)
197
+ };
198
+ }
199
+
200
+ return {
201
+ pass: false,
202
+ score: 0,
203
+ reason: `Ошибки в Theory of Mind: ${diffs.join("; ")}.`,
204
+ expected: formatExpectedAnswer(scenario)
205
+ };
206
+ }
207
+
208
+ // 3. КОМАНДЫ: Алфавитный список отобранных кандидатов
209
+ if (category === "team_allocation") {
210
+ const match = text.match(/(?:мой ответ|my answer)\s*:\s*([^\n\r]+)/i);
211
+ const expectedTeam = [...scenario.expectedTeam].map((x) => x.toUpperCase()).sort();
212
+ const expectedStr = `Мой ответ: (${expectedTeam.join("), (")})`;
213
+
214
+ if (!match) {
215
+ return {
216
+ pass: false,
217
+ score: 0,
218
+ reason: `Не найдена завершающая строка 'Мой ответ: (Буква 1), (Буква 2), (Буква 3)'. Ожидалось: "${expectedStr}".`,
219
+ expected: formatExpectedAnswer(scenario)
220
+ };
221
+ }
222
+
223
+ const rawMembers = [...match[1].matchAll(/\(([А-ЯA-Z])\)/gi)].map((m) => m[1].toUpperCase());
224
+
225
+ const isAlphabetical = rawMembers
226
+ .slice()
227
+ .every((val, i, arr) => !i || arr[i - 1].localeCompare(val) <= 0);
228
+ if (!isAlphabetical) {
229
+ return {
230
+ pass: false,
231
+ score: 0,
232
+ reason: `Кандидаты не упорядочены по алфавиту: [${rawMembers.join(", ")}]. Ожидалось: "${expectedStr}".`,
233
+ expected: formatExpectedAnswer(scenario)
234
+ };
235
+ }
236
+
237
+ const sortedRaw = rawMembers.slice().sort();
238
+ const isMatch =
239
+ sortedRaw.length === expectedTeam.length &&
240
+ sortedRaw.every((val, idx) => val === expectedTeam[idx]);
241
+
242
+ if (isMatch) {
243
+ return {
244
+ pass: true,
245
+ score: 100,
246
+ reason: `Команда сформирована верно: (${expectedTeam.join("), (")}).`,
247
+ expected: formatExpectedAnswer(scenario)
248
+ };
249
+ }
250
+
251
+ return {
252
+ pass: false,
253
+ score: 0,
254
+ reason: `Неверный состав: получено [${rawMembers.join(", ")}], ожидалось: [${expectedTeam.join(", ")}].`,
255
+ expected: formatExpectedAnswer(scenario)
256
+ };
257
+ }
258
+
259
+ return {
260
+ pass: false,
261
+ score: 0,
262
+ reason: "Неизвестная категория сценария.",
263
+ expected: "Неизвестно"
264
+ };
265
+ }
266
+
267
+ function resolveHelpers(context) {
268
+ if (context && typeof context.getRequiredProvider === "function") return context;
269
+ if (context?.helpers && typeof context.helpers.getRequiredProvider === "function")
270
+ return context.helpers;
271
+ return {
272
+ getRequiredProvider(providerId) {
273
+ const providers = context?.providers || [];
274
+ const found = Array.isArray(providers)
275
+ ? providers.find((p) => p.id === providerId || p.provider === providerId)
276
+ : providers[providerId];
277
+ return found || { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "none" };
278
+ },
279
+ getInferenceEndpoint(modelId) {
280
+ return context?.endpoints?.[modelId] || null;
281
+ }
282
+ };
283
+ }
284
+
285
+ export async function prepare(context) {
286
+ const helpers = resolveHelpers(context);
287
+
288
+ return {
289
+ async runScenario(input) {
290
+ const allScenarios = loadScenarios();
291
+ const scenario = allScenarios.find((s) => s.id === input.scenario.id);
292
+ if (!scenario) {
293
+ return {
294
+ scenarioId: input.scenario.id,
295
+ status: "fail",
296
+ score: 0,
297
+ summary: "Сценарий не найден",
298
+ rawLog: "error=Scenario not found"
299
+ };
300
+ }
301
+
302
+ const provider = helpers.getRequiredProvider(input.model.provider, { enabledOnly: true });
303
+ const inference = helpers.getInferenceEndpoint
304
+ ? helpers.getInferenceEndpoint(input.model.id)
305
+ : null;
306
+
307
+ const rawBaseUrl =
308
+ (inference && inference.status === "running" ? inference.baseUrl : provider?.baseUrl) ||
309
+ "http://127.0.0.1:11434/v1";
310
+ const baseUrl = rawBaseUrl.replace(/\/+$/, "");
311
+ const apiKey = provider?.apiKey || "none";
312
+
313
+ const requestBody = {
314
+ model: input.model.model,
315
+ messages: [{ role: "user", content: scenario.prompt }],
316
+ temperature: 0
317
+ };
318
+
319
+ const response = await fetch(`${baseUrl}/chat/completions`, {
320
+ method: "POST",
321
+ headers: {
322
+ "Content-Type": "application/json",
323
+ Authorization: `Bearer ${apiKey}`
324
+ },
325
+ body: JSON.stringify(requestBody)
326
+ });
327
+
328
+ const data = await response.json();
329
+ const text = (data.choices?.[0]?.message?.content || "").trim();
330
+ const result = validateResponse(scenario, text);
331
+
332
+ // Информативный журнал с задачей, эталоном, выводом модели и диффом ошибок
333
+ const rawLog = [
334
+ `============================================================`,
335
+ `СЦЕНАРИЙ: ${scenario.id} — ${scenario.name}`,
336
+ `КАТЕГОРИЯ: ${scenario.category} | ЯЗЫК: ${scenario.language}`,
337
+ `МОДЕЛЬ: ${input.model.model}`,
338
+ `============================================================`,
339
+ ``,
340
+ `=== ИСХОДНАЯ ЗАДАЧА (PROMPT) ===`,
341
+ scenario.prompt,
342
+ ``,
343
+ `=== ЭТАЛОННЫЙ ПРАВИЛЬНЫЙ ОТВЕТ ===`,
344
+ result.expected,
345
+ ``,
346
+ `=== ОТВЕТ АССИСТЕНТА ===`,
347
+ text || "<МОДЕЛЬ ВЕРНУЛА ПУСТОЙ ОТВЕТ>",
348
+ ``,
349
+ `=== ВЕРДИКТ И ПРОВЕРКА ===`,
350
+ `Статус: ${result.pass ? "ВЕРНО (PASS)" : "НЕВЕРНО (FAIL)"}`,
351
+ `Балл: ${result.score}/100`,
352
+ `Подробности проверки: ${result.reason}`,
353
+ ``,
354
+ `--- СТАНДАРТНЫЕ МЕТКИ ДЛЯ BENCHLOCAL ---`,
355
+ `model=${input.model.model}`,
356
+ `scenario=${scenario.id} ${scenario.name}`,
357
+ `category=${scenario.category}`,
358
+ `language=${scenario.language}`,
359
+ `verdict=${result.pass ? "pass" : "fail"}`,
360
+ `score=${result.score}`,
361
+ `summary=${result.reason}`,
362
+ `\n--- RAW API RESPONSE (JSON) ---`,
363
+ JSON.stringify(data, null, 2)
364
+ ].join("\n");
365
+
366
+ return {
367
+ scenarioId: scenario.id,
368
+ status: result.pass ? "pass" : "fail",
369
+ score: result.score,
370
+ summary: result.reason,
371
+ rawLog
372
+ };
373
+ },
374
+
375
+ async dispose() {}
376
+ };
377
+ }
378
+
379
+ export function scoreModelResults(results) {
380
+ const scored = results || [];
381
+ const total = scored.reduce((sum, r) => sum + (r.score || 0), 0);
382
+ const max = scored.length * 100;
383
+ const allScenarios = loadScenarios();
384
+
385
+ const categoryNames = {
386
+ murder_mysteries: "Детективы (Murder Mysteries - Triplet)",
387
+ object_placements: "Перемещение объектов (Object Placements - ToM)",
388
+ team_allocation: "Формирование команд (Team Allocation - Psych)"
389
+ };
390
+
391
+ const categories = Object.keys(categoryNames).map((catKey) => {
392
+ const subsetIds = new Set(allScenarios.filter((s) => s.category === catKey).map((s) => s.id));
393
+ const catResults = scored.filter((r) => subsetIds.has(r.scenarioId));
394
+ const catScore = catResults.reduce((sum, r) => sum + (r.score || 0), 0);
395
+ const catMax = catResults.length * 100;
396
+ return {
397
+ id: catKey,
398
+ name: categoryNames[catKey],
399
+ score: catMax > 0 ? Math.round((catScore / catMax) * 100) : 0
400
+ };
401
+ });
402
+
403
+ return {
404
+ totalScore: max > 0 ? Math.round((total / max) * 100) : 0,
405
+ categories
406
+ };
407
+ }
408
+
409
+ export default {
410
+ manifest,
411
+ listScenarios,
412
+ getScenarios,
413
+ prepare,
414
+ createRuntime: prepare,
415
+ scoreModelResults
416
+ };
MuSR-mini/scenarios.json ADDED
The diff for this file is too large to render. See raw diff