zelk12 commited on
Commit
27111ee
·
verified ·
1 Parent(s): b78eeb7

Upload 3 files

Browse files
ifeval-mini/benchlocal.pack.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 1,
3
+ "protocolVersion": 1,
4
+ "id": "ifeval-hard",
5
+ "name": "IFEval-Hard (90 комплексных задач)",
6
+ "version": "1.0.0",
7
+ "description": "Аутентичный сложный IFEval с мульти-композициями: 30 RU, 30 EN и 30 Code-Switching задач (по 2-3 строгих ограничения на промпт)",
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
+ }
ifeval-mini/index.mjs ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ function readJsonSafe(filePath, fallback) {
10
+ try {
11
+ if (fs.existsSync(filePath)) {
12
+ const raw = fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, "").trim();
13
+ return JSON.parse(raw);
14
+ }
15
+ } catch (err) {
16
+ console.error(`[IFEval-Hard] Ошибка чтения ${filePath}:`, err.message);
17
+ }
18
+ return fallback;
19
+ }
20
+
21
+ const defaultManifest = {
22
+ schemaVersion: 1,
23
+ protocolVersion: 1,
24
+ id: "ifeval-hard",
25
+ name: "IFEval-Hard (90 комплексных задач)",
26
+ version: "1.0.0",
27
+ description: "Аутентичный сложный IFEval с мульти-композициями: 30 RU, 30 EN и 30 Code-Switching задач (по 2-3 строгих ограничения на промпт)",
28
+ entry: "./index.mjs",
29
+ samplingDefaults: { temperature: 0.0 },
30
+ capabilities: { tools: false, multiTurn: false, streamingProgress: false, verification: false }
31
+ };
32
+
33
+ export const manifest = readJsonSafe(manifestPath, defaultManifest);
34
+
35
+ function loadScenarios() {
36
+ return readJsonSafe(scenariosPath, []);
37
+ }
38
+
39
+ export function listScenarios() {
40
+ return loadScenarios().map(s => ({
41
+ id: s.id,
42
+ name: s.name,
43
+ category: s.category || "Общие",
44
+ description: s.name
45
+ }));
46
+ }
47
+
48
+ export function getScenarios() {
49
+ return listScenarios();
50
+ }
51
+
52
+ function validateSingleRule(type, args, text) {
53
+ const clean = (text || "").trim();
54
+ const lower = clean.toLowerCase();
55
+
56
+ switch (type) {
57
+ case "code_switching": {
58
+ const words = clean.split(/\s+/).filter(w => /[\p{L}\p{N}]/u.test(w));
59
+ const ruWords = words.filter(w => /[а-яА-ЯёЁ]/.test(w)).length;
60
+ const enWords = words.filter(w => /[a-zA-Z]/.test(w)).length;
61
+ const minRu = args.minRu || 5;
62
+ const minEn = args.minEn || 5;
63
+
64
+ if (ruWords < minRu) {
65
+ return { pass: false, reason: `Мало русских слов: ${ruWords} (надо минимум ${minRu})` };
66
+ }
67
+ if (enWords < minEn) {
68
+ return { pass: false, reason: `Мало английских слов: ${enWords} (надо минимум ${minEn})` };
69
+ }
70
+ return { pass: true, reason: `Code-switching OK (${ruWords} RU / ${enWords} EN слов)` };
71
+ }
72
+
73
+ case "include_keywords": {
74
+ const missing = (args.keywords || []).filter(kw => !lower.includes(kw.toLowerCase()));
75
+ return {
76
+ pass: missing.length === 0,
77
+ reason: missing.length === 0 ? "Все ключевые слова найдены" : `Нет слов: ${missing.join(", ")}`
78
+ };
79
+ }
80
+
81
+ case "forbidden_words": {
82
+ const found = (args.words || []).filter(w => lower.includes(w.toLowerCase()));
83
+ return {
84
+ pass: found.length === 0,
85
+ reason: found.length === 0 ? "Запрещенных слов нет" : `Найдены запрещенные слова: ${found.join(", ")}`
86
+ };
87
+ }
88
+
89
+ case "keyword_frequency": {
90
+ const escaped = args.word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ const re = new RegExp(`(?<!\\p{L})${escaped}(?!\\p{L})`, "gui");
92
+ const count = (lower.match(re) || []).length;
93
+ return {
94
+ pass: count === args.count,
95
+ reason: count === args.count ? `Слово '${args.word}' x${count}` : `Слово '${args.word}' x${count} (требовалось ${args.count})`
96
+ };
97
+ }
98
+
99
+ case "letter_frequency": {
100
+ const char = args.letter.toLowerCase();
101
+ const count = lower.split(char).length - 1;
102
+ return {
103
+ pass: count === args.count,
104
+ reason: count === args.count ? `Буква '${char}' x${count}` : `Буква '${char}' x${count} (требовалось ${args.count})`
105
+ };
106
+ }
107
+
108
+ case "response_language": {
109
+ if (args.lang === "ru") {
110
+ const pass = !/[a-zA-Z]/.test(clean) && /[а-яА-ЯёЁ]/.test(clean);
111
+ return { pass, reason: pass ? "Строго русский язык без латиницы" : "Обнаружена латиница" };
112
+ }
113
+ const samples = {
114
+ es: ["de", "la", "el", "en"],
115
+ fr: ["le", "la", "du", "est"],
116
+ de: ["und", "der", "die", "das"],
117
+ it: ["di", "per", "con", "il"]
118
+ }[args.lang] || [];
119
+ const pass = samples.some(s => lower.includes(s));
120
+ return { pass, reason: pass ? `Язык (${args.lang}) подтвержден` : `Маркеры (${args.lang}) не найдены` };
121
+ }
122
+
123
+ case "number_words": {
124
+ const words = clean.split(/\s+/).filter(w => /[\p{L}\p{N}]/u.test(w));
125
+ const count = words.length;
126
+ if (args.exact) return { pass: count === args.exact, reason: `Слов: ${count} (надо ровно ${args.exact})` };
127
+ if (args.min && count < args.min) return { pass: false, reason: `Слов: ${count} (мин. ${args.min})` };
128
+ if (args.max && count > args.max) return { pass: false, reason: `Слов: ${count} (макс. ${args.max})` };
129
+ return { pass: true, reason: `Слов в норме (${count})` };
130
+ }
131
+
132
+ case "number_sentences": {
133
+ const normalized = clean.replace(/P\.S\./gi, "PS");
134
+ const count = normalized.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
135
+ return { pass: count === args.count, reason: `Предложений: ${count} (надо ровно ${args.count})` };
136
+ }
137
+
138
+ case "number_paragraphs": {
139
+ const count = clean.split(/\n\s*\n/).filter(p => p.trim().length > 0).length;
140
+ return { pass: count === args.count, reason: `Абзацев: ${count} (надо ровно ${args.count})` };
141
+ }
142
+
143
+ case "paragraph_first_word": {
144
+ const paras = clean.split(/\n\s*\n/).filter(p => p.trim().length > 0);
145
+ const target = paras[args.paragraphIndex];
146
+ if (!target) return { pass: false, reason: `Абзац ${args.paragraphIndex + 1} отсутствует` };
147
+ const firstWord = target.split(/\s+/)[0].replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "");
148
+ const pass = firstWord.toLowerCase() === args.word.toLowerCase();
149
+ return { pass, reason: pass ? `Первое слово: '${firstWord}'` : `Первое слово: '${firstWord}' (ожидалось '${args.word}')` };
150
+ }
151
+
152
+ case "number_bullet_lists": {
153
+ const bullets = clean.split("\n").filter(line => /^\s*([-*•]|\d+[.)])\s+/.test(line));
154
+ return { pass: bullets.length === args.count, reason: `Пунктов списка: ${bullets.length} (надо ${args.count})` };
155
+ }
156
+
157
+ case "title": {
158
+ const pass = /<<[^>]+>>/.test(clean);
159
+ return { pass, reason: pass ? "Заголовок <<...>> найден" : "Заголовок <<...>> не найден" };
160
+ }
161
+
162
+ case "constrained_response": {
163
+ const pass = clean.toLowerCase() === (args.expected || "").toLowerCase();
164
+ return { pass, reason: pass ? "Точный ответ" : `Получено '${clean}' (ожидалось '${args.expected}')` };
165
+ }
166
+
167
+ case "highlighted_section": {
168
+ const matches = clean.match(/(?<!\*)\*(?!\*)([^*\n]+?)(?<!\*)\*(?!\*)/g) || [];
169
+ const count = matches.length;
170
+ if (args.exact) return { pass: count === args.exact, reason: `Выделений звездочками: ${count} (надо ${args.exact})` };
171
+ const min = args.min || 1;
172
+ return { pass: count >= min, reason: `Выделений звездочками: ${count} (мин. ${min})` };
173
+ }
174
+
175
+ case "multiple_sections": {
176
+ const matches = clean.match(/(?:^|\n)\s*(?:[#*_\->•]|\d+[.)]|\s)*\bSection\s+\d+\b/gi) || [];
177
+ return { pass: matches.length === args.count, reason: `Секций Section X: ${matches.length} (надо ${args.count})` };
178
+ }
179
+
180
+ case "json_format": {
181
+ try {
182
+ const c = clean.replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/```$/, "").trim();
183
+ const parsed = JSON.parse(c);
184
+ if (args.isArray && !Array.isArray(parsed)) return { pass: false, reason: "Не массив JSON" };
185
+ if (Array.isArray(args.requiredKeys)) {
186
+ const obj = Array.isArray(parsed) ? parsed[0] : parsed;
187
+ if (!obj || typeof obj !== "object") return { pass: false, reason: "Некорректный объект JSON" };
188
+ for (const k of args.requiredKeys) {
189
+ if (!(k in obj)) return { pass: false, reason: `Нет ключа '${k}' в JSON` };
190
+ }
191
+ }
192
+ return { pass: true, reason: "Валидный JSON с нужными ключами" };
193
+ } catch {
194
+ return { pass: false, reason: "Не является валидным JSON" };
195
+ }
196
+ }
197
+
198
+ case "number_of_placeholders": {
199
+ const matches = clean.match(/\[[^\]\n]+\]/g) || [];
200
+ const count = matches.length;
201
+ if (args.exact) return { pass: count === args.exact, reason: `Плейсхолдеров [...]: ${count} (надо ${args.exact})` };
202
+ const min = args.min || 1;
203
+ return { pass: count >= min, reason: `Плейсхолдеров [...]: ${count} (мин. ${min})` };
204
+ }
205
+
206
+ case "postscript": {
207
+ const pass = /(?:\n|^)\s*P\.S\..*$/is.test(clean);
208
+ return { pass, reason: pass ? "P.S. найден в конце" : "P.S. в конце отсутствует" };
209
+ }
210
+
211
+ case "repeat_prompt": {
212
+ const promptClean = (args.prompt || "").trim().toLowerCase();
213
+ const pass = lower.startsWith(promptClean);
214
+ return { pass, reason: pass ? "Запрос повторен дословно" : "Запрос не повторен дословно в начале" };
215
+ }
216
+
217
+ case "two_responses": {
218
+ const parts = clean.split("******");
219
+ const pass = parts.length === 2 && parts[0].trim().length > 0 && parts[1].trim().length > 0;
220
+ return { pass, reason: pass ? "Два ответа через ******" : "Разделитель ****** отсутствует" };
221
+ }
222
+
223
+ case "all_uppercase": {
224
+ const hasL = /\p{L}/u.test(clean);
225
+ const pass = hasL && clean === clean.toUpperCase();
226
+ return { pass, reason: pass ? "Все ЗАГЛАВНЫМИ БУКВАМИ" : "Найдены строчные буквы" };
227
+ }
228
+
229
+ case "all_lowercase": {
230
+ const hasL = /\p{L}/u.test(clean);
231
+ const pass = hasL && clean === clean.toLowerCase();
232
+ return { pass, reason: pass ? "Все строчными буквами" : "Найдены заглавные буквы" };
233
+ }
234
+
235
+ case "capital_word_frequency": {
236
+ const rawWords = clean.split(/\s+/).filter(Boolean);
237
+ const capWords = [];
238
+ for (let i = 0; i < rawWords.length; i++) {
239
+ const w = rawWords[i];
240
+ const letters = w.replace(/[^\p{L}]/gu, "");
241
+ if (!letters || letters !== letters.toUpperCase()) continue;
242
+ if (letters.length >= 2) capWords.push(letters);
243
+ else if (letters.length === 1) {
244
+ const prev = i > 0 ? rawWords[i - 1] : "";
245
+ const isStart = i === 0 || /[.!?][)"'»*]*$/.test(prev);
246
+ const next = i < rawWords.length - 1 ? rawWords[i + 1] : "";
247
+ if (isStart && /\p{Ll}/u.test(next)) continue;
248
+ capWords.push(letters);
249
+ }
250
+ }
251
+ return { pass: capWords.length === args.count, reason: `Слов капсом: ${capWords.length} (надо ${args.count})` };
252
+ }
253
+
254
+ case "end_checker": {
255
+ const pass = clean.endsWith(args.phrase);
256
+ return { pass, reason: pass ? `Фраза в конце: '${args.phrase}'` : `Нет фразы '${args.phrase}' в конце` };
257
+ }
258
+
259
+ case "quotation": {
260
+ const pass = (clean.startsWith('"') && clean.endsWith('"')) || (clean.startsWith("«") && clean.endsWith("»"));
261
+ return { pass, reason: pass ? "Весь текст в кавычках" : "Текст не заключен в кавычки целиком" };
262
+ }
263
+
264
+ case "no_comma": {
265
+ const pass = !clean.includes(",") && !clean.includes(",");
266
+ return { pass, reason: pass ? "Без запятых" : "Найдена запрещенная запятая" };
267
+ }
268
+
269
+ default:
270
+ return { pass: true, reason: "Неизвестный тип валидатора" };
271
+ }
272
+ }
273
+
274
+ function validateResponse(scenario, rawText) {
275
+ const validators = scenario.validators || (scenario.validatorType ? [{ type: scenario.validatorType, args: scenario.validatorArgs }] : []);
276
+ if (validators.length === 0) return { pass: true, reason: "Нет валидаторов" };
277
+
278
+ const results = validators.map(v => {
279
+ const vArgs = { ...(v.args || {}) };
280
+ if (v.type === "repeat_prompt") {
281
+ vArgs.prompt = scenario.prompt;
282
+ }
283
+ return {
284
+ type: v.type,
285
+ res: validateSingleRule(v.type, vArgs, rawText)
286
+ };
287
+ });
288
+
289
+ const allPass = results.every(r => r.res.pass);
290
+ const summary = results.map(r => `[${r.res.pass ? "OK" : "FAIL"}: ${r.type} -> ${r.res.reason}]`).join(" ");
291
+
292
+ return {
293
+ pass: allPass,
294
+ reason: summary
295
+ };
296
+ }
297
+
298
+ function resolveHelpers(context) {
299
+ if (context && typeof context.getRequiredProvider === "function") {
300
+ return context;
301
+ }
302
+ if (context?.helpers && typeof context.helpers.getRequiredProvider === "function") {
303
+ return context.helpers;
304
+ }
305
+ return {
306
+ getRequiredProvider(providerId) {
307
+ const providers = context?.providers || [];
308
+ const found = Array.isArray(providers)
309
+ ? providers.find((p) => p.id === providerId || p.provider === providerId)
310
+ : providers[providerId];
311
+ return found || { baseUrl: "http://127.0.0.1:11434/v1", apiKey: "none" };
312
+ },
313
+ getInferenceEndpoint(modelId) {
314
+ return context?.endpoints?.[modelId] || null;
315
+ }
316
+ };
317
+ }
318
+
319
+ export async function prepare(context) {
320
+ const helpers = resolveHelpers(context);
321
+
322
+ return {
323
+ async runScenario(input) {
324
+ const allScenarios = loadScenarios();
325
+ const scenario = allScenarios.find(s => s.id === input.scenario.id);
326
+ if (!scenario) {
327
+ return {
328
+ scenarioId: input.scenario.id,
329
+ status: "fail",
330
+ score: 0,
331
+ summary: "Сценарий не найден",
332
+ rawLog: "error=Scenario not found"
333
+ };
334
+ }
335
+
336
+ const provider = helpers.getRequiredProvider(input.model.provider, { enabledOnly: true });
337
+ const inference = helpers.getInferenceEndpoint ? helpers.getInferenceEndpoint(input.model.id) : null;
338
+
339
+ const rawBaseUrl = (inference && inference.status === "running" ? inference.baseUrl : provider?.baseUrl) || "http://127.0.0.1:11434/v1";
340
+ const baseUrl = rawBaseUrl.replace(/\/+$/, "");
341
+ const apiKey = provider?.apiKey || "none";
342
+
343
+ const requestBody = {
344
+ model: input.model.model,
345
+ messages: [{ role: "user", content: scenario.prompt }],
346
+ temperature: 0
347
+ };
348
+
349
+ const response = await fetch(`${baseUrl}/chat/completions`, {
350
+ method: "POST",
351
+ headers: {
352
+ "Content-Type": "application/json",
353
+ Authorization: `Bearer ${apiKey}`
354
+ },
355
+ body: JSON.stringify(requestBody)
356
+ });
357
+
358
+ const data = await response.json();
359
+ const text = (data.choices?.[0]?.message?.content || "").trim();
360
+ const result = validateResponse(scenario, text);
361
+
362
+ const rawLog = [
363
+ `model=${input.model.model}`,
364
+ `scenario=${scenario.id} ${scenario.name}`,
365
+ `user_turn_1=${scenario.prompt}`,
366
+ `assistant_turn_1=${text}`,
367
+ `final_answer=${text}`,
368
+ `verdict=${result.pass ? "pass" : "fail"}`,
369
+ `score=${result.pass ? 100 : 0}`,
370
+ `summary=${result.reason}`,
371
+ `\n--- RAW API RESPONSE ---`,
372
+ JSON.stringify(data, null, 2)
373
+ ].join("\n");
374
+
375
+ return {
376
+ scenarioId: scenario.id,
377
+ status: result.pass ? "pass" : "fail",
378
+ score: result.pass ? 100 : 0,
379
+ summary: result.reason,
380
+ rawLog: rawLog
381
+ };
382
+ },
383
+
384
+ async dispose() {}
385
+ };
386
+ }
387
+
388
+ export function scoreModelResults(results) {
389
+ const scored = results || [];
390
+ const total = scored.reduce((sum, r) => sum + (r.score || 0), 0);
391
+ const max = scored.length * 100;
392
+
393
+ return {
394
+ totalScore: max > 0 ? Math.round((total / max) * 100) : 0,
395
+ categories: []
396
+ };
397
+ }
398
+
399
+ export default {
400
+ manifest,
401
+ listScenarios,
402
+ getScenarios,
403
+ prepare,
404
+ createRuntime: prepare,
405
+ scoreModelResults
406
+ };
ifeval-mini/scenarios.json ADDED
The diff for this file is too large to render. See raw diff