divaspoudel commited on
Commit
d4958fe
·
verified ·
1 Parent(s): 05691dc

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. script.py +358 -93
script.py CHANGED
@@ -1,4 +1,5 @@
1
 
 
2
  import os
3
  os.environ["HF_HUB_OFFLINE"] = "1"
4
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
@@ -20,10 +21,6 @@ try:
20
  except ImportError:
21
  _run_pkg_cmd(["install", "-q", "bitsandbytes"])
22
 
23
- # torchvision is not needed for a text-only LLM, and if the sandbox's
24
- # preinstalled torchvision doesn't match its torch build, transformers can
25
- # crash on import trying to load an unused image-utils path. Drop it
26
- # defensively before importing transformers.
27
  try:
28
  import torchvision # noqa: F401
29
  _run_pkg_cmd(["uninstall", "-y", "-q", "torchvision"])
@@ -33,14 +30,18 @@ except ImportError:
33
  import re
34
  import json
35
  import time
 
36
  import pandas as pd
37
  import torch
 
38
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
39
 
40
  MODEL_ID = "."
41
  TIME_LIMIT = 30 * 60 # hard competition limit, seconds
42
  SAFETY_BUFFER = 90 # stop issuing new generations this many seconds before the limit
43
- MAX_NEW_TOKENS = 900
 
 
44
 
45
  START = time.time()
46
 
@@ -53,7 +54,7 @@ tok = AutoTokenizer.from_pretrained(MODEL_ID)
53
  bnb_config = BitsAndBytesConfig(
54
  load_in_4bit=True,
55
  bnb_4bit_quant_type="nf4",
56
- bnb_4bit_compute_dtype=torch.float16, # T4 = Turing, no native bf16
57
  bnb_4bit_use_double_quant=True,
58
  )
59
 
@@ -69,43 +70,226 @@ df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
69
 
70
  ITEM_RE = re.compile(r"(?m)^\s*(\d+)\.\s")
71
 
72
- TASK_HINTS = {
73
- "translation": "Answer with the translated text only, in the language the item asks for.",
74
- "match_letters": "Answer with only the option letter (e.g. A, B, C) for each item.",
75
- "fill_blanks": "Answer with only the missing form for each blank, using the same spelling/diacritic conventions as the data.",
76
- "text_to_num": "Answer with the value written in digits (e.g. 285), no words.",
77
- "num_to_text": "Answer with the number written out in words in the task language, matching the data's conventions.",
78
- }
79
 
80
- SYSTEM_PROMPT = (
81
- "You are a world-class contestant at the International Linguistics Olympiad (IOL). "
82
- "You will be given a self-contained problem: some linguistic data plus a query with "
83
- "numbered items. Work out the pattern rigorously from the data given -- do not rely on "
84
- "any outside knowledge of the language, only on what is in the context. Think step by "
85
- "step internally, but keep your visible reasoning compact.\n\n"
86
- "You MUST end your reply with exactly two blocks, in this order:\n\n"
87
- "EXPLANATION:\n"
88
- "<a short, human-readable explanation of your reasoning and the rule(s) you found; "
89
- "a few bullet points or a couple of short paragraphs, NOT a raw token-by-token trace>\n\n"
90
- "FINAL:\n"
91
- "<a JSON list of strings, one answer per numbered item, in the same order as the query, "
92
- "and nothing else on that line>"
93
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- def n_items(query: str) -> int:
 
96
  nums = ITEM_RE.findall(query)
97
  if nums:
98
  return len(set(nums))
99
- # fallback: count non-empty lines as a rough guess
100
  return max(1, len([l for l in query.splitlines() if l.strip()]))
101
 
102
- def build_prompt(context: str, query: str, task_type: str) -> str:
103
- hint = TASK_HINTS.get(task_type, "")
104
- extra = f"\n\nFormat hint: {hint}" if hint else ""
105
- return f"{context.strip()}\n\n{query.strip()}{extra}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- def extract_final_list(text: str, expected_n: int):
108
- m = re.search(r"FINAL:\s*(\[.*?\])", text, re.DOTALL)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  if m:
110
  try:
111
  parsed = json.loads(m.group(1))
@@ -113,7 +297,8 @@ def extract_final_list(text: str, expected_n: int):
113
  return [str(x) for x in parsed]
114
  except Exception:
115
  pass
116
- # fallback 1: any JSON array anywhere in the text
 
117
  for m2 in re.finditer(r"\[.*?\]", text, re.DOTALL):
118
  try:
119
  parsed = json.loads(m2.group(0))
@@ -121,86 +306,166 @@ def extract_final_list(text: str, expected_n: int):
121
  return [str(x) for x in parsed]
122
  except Exception:
123
  continue
124
- # fallback 2: split remaining non-empty lines after FINAL: (or whole text)
 
125
  tail = text.split("FINAL:")[-1]
126
- lines = [re.sub(r"^\s*\d+[\.\)]\s*", "", l).strip(" \t\"'") for l in tail.splitlines() if l.strip()]
 
127
  lines = [l for l in lines if l]
128
- if lines:
129
- return lines
130
- return []
131
 
132
  def extract_explanation(text: str) -> str:
133
- m = re.search(r"EXPLANATION:\s*(.*?)\s*FINAL:", text, re.DOTALL)
134
- expl = m.group(1).strip() if m else ""
135
- return expl[:1200]
136
-
137
- def fit_to_length(items, n):
138
- items = list(items)
139
- if len(items) < n:
140
- items = items + [""] * (n - len(items))
141
- elif len(items) > n:
142
- items = items[:n]
143
- return items
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  rows_out = []
146
  n_rows = len(df)
147
 
148
  for i, r in df.iterrows():
149
- expected_n = n_items(r["query"])
150
  remaining_rows = n_rows - i
151
  per_row_budget = (time_left() - SAFETY_BUFFER) / max(1, remaining_rows)
152
 
153
- if time_left() < SAFETY_BUFFER or per_row_budget < 5:
154
- # out of time: best-effort blank guesses so every row still has a row
155
  rows_out.append({
156
  "id": r["id"],
157
- "prediction": json.dumps([""] * expected_n, ensure_ascii=False),
158
- "explanation": "",
159
  })
160
  continue
161
 
162
  try:
163
- prompt = build_prompt(r["context"], r["query"], r.get("task_type", ""))
164
- messages = [
165
- {"role": "system", "content": SYSTEM_PROMPT},
166
- {"role": "user", "content": prompt},
167
- ]
168
- model_inputs = tok.apply_chat_template(
169
- messages, add_generation_prompt=True, return_tensors="pt"
170
- )
171
- ids = model_inputs['input_ids'].to(model.device)
172
-
173
- gen_tokens = min(MAX_NEW_TOKENS, MAX_NEW_TOKENS)
174
- with torch.no_grad():
175
- out = model.generate(
176
- ids,
177
- max_new_tokens=gen_tokens,
178
- do_sample=False,
179
- temperature=None,
180
- top_p=None,
181
- pad_token_id=tok.eos_token_id,
182
- )
183
- text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
184
-
185
- preds = extract_final_list(text, expected_n)
186
- preds = fit_to_length(preds, expected_n)
187
- explanation = extract_explanation(text)
188
-
189
- rows_out.append({
190
- "id": r["id"],
191
- "prediction": json.dumps(preds, ensure_ascii=False),
192
- "explanation": explanation,
193
- })
194
  except Exception as e:
 
195
  rows_out.append({
196
  "id": r["id"],
197
- "prediction": json.dumps([""] * expected_n, ensure_ascii=False),
198
- "explanation": f"(error during generation: {e})"[:300],
199
  })
200
 
201
- # write after every row so a partial file is always valid on disk
202
  pd.DataFrame(rows_out).to_csv("submission.csv", index=False)
203
- print(f"{len(rows_out)}/{n_rows} done, {time_left():.0f}s left", flush=True)
204
 
 
205
  pd.DataFrame(rows_out).to_csv("submission.csv", index=False)
206
- print("wrote submission.csv", flush=True)
 
 
1
 
2
+
3
  import os
4
  os.environ["HF_HUB_OFFLINE"] = "1"
5
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
 
21
  except ImportError:
22
  _run_pkg_cmd(["install", "-q", "bitsandbytes"])
23
 
 
 
 
 
24
  try:
25
  import torchvision # noqa: F401
26
  _run_pkg_cmd(["uninstall", "-y", "-q", "torchvision"])
 
30
  import re
31
  import json
32
  import time
33
+ import math
34
  import pandas as pd
35
  import torch
36
+ from collections import Counter
37
  from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
38
 
39
  MODEL_ID = "."
40
  TIME_LIMIT = 30 * 60 # hard competition limit, seconds
41
  SAFETY_BUFFER = 90 # stop issuing new generations this many seconds before the limit
42
+ MAX_NEW_TOKENS = 1200 # increased for grammar scratchpad
43
+ NUM_PATHS = 3 # N=3 for test-time scaling
44
+ TEMPERATURE = 0.3 # T=0.3 for diverse but coherent paths
45
 
46
  START = time.time()
47
 
 
54
  bnb_config = BitsAndBytesConfig(
55
  load_in_4bit=True,
56
  bnb_4bit_quant_type="nf4",
57
+ bnb_4bit_compute_dtype=torch.float16,
58
  bnb_4bit_use_double_quant=True,
59
  )
60
 
 
70
 
71
  ITEM_RE = re.compile(r"(?m)^\s*(\d+)\.\s")
72
 
73
+ # =============================================================================
74
+ # 1. DYNAMIC TASK ROUTER
75
+ # Analyzes: task_type, eval_type, item count (K)
76
+ # =============================================================================
 
 
 
77
 
78
+ TASK_PROFILES = {
79
+ "translation": {
80
+ "eval_metric": "chrF",
81
+ "format_hint": "Translate to the target language preserving diacritics and orthography.",
82
+ "grammar_focus": "morphophonology, tense, case, agreement",
83
+ },
84
+ "match_letters": {
85
+ "eval_metric": "exact",
86
+ "format_hint": "Answer with only the option letter (e.g. A, B, C) for each item.",
87
+ "grammar_focus": "phonological rules, allophony, orthographic patterns",
88
+ },
89
+ "fill_blanks": {
90
+ "eval_metric": "chrF",
91
+ "format_hint": "Fill the blank with the correct inflected/corrected form.",
92
+ "grammar_focus": "inflection, derivation, agreement, sandhi",
93
+ },
94
+ "text_to_num": {
95
+ "eval_metric": "exact",
96
+ "format_hint": "Answer with the value written in digits (e.g. 285).",
97
+ "grammar_focus": "numeral systems, base systems, place value",
98
+ },
99
+ "num_to_text": {
100
+ "eval_metric": "chrF",
101
+ "format_hint": "Answer with the number written out in words in the task language.",
102
+ "grammar_focus": "numeral morphology, ordinals, cardinals",
103
+ },
104
+ }
105
 
106
+ def count_items(query: str) -> int:
107
+ """Extract K = number of items in the query."""
108
  nums = ITEM_RE.findall(query)
109
  if nums:
110
  return len(set(nums))
111
+ # fallback: count non-empty lines
112
  return max(1, len([l for l in query.splitlines() if l.strip()]))
113
 
114
+ def get_task_profile(task_type: str) -> dict:
115
+ """Route to task-specific configuration."""
116
+ return TASK_PROFILES.get(task_type, TASK_PROFILES["translation"])
117
+
118
+ # =============================================================================
119
+ # 2. THE GRAMMAR SCRATCHPAD (System Prompt Injection)
120
+ # Forces syntax deduction before answering
121
+ # =============================================================================
122
+
123
+ GRAMMAR_SCRATCHPAD = """Before answering, you MUST work through this deduction process in your scratchpad:
124
+
125
+ 【GRAMMAR SCRATCHPAD】
126
+ 1. DATA INVENTORY: List the forms and glosses explicitly shown in the data
127
+ 2. MORPHEME SEGMENTATION: Break forms into plausible morpheme boundaries
128
+ 3. PARADIGM MAPPING: If there are inflections, sketch the paradigm (person, number, tense, case...)
129
+ 4. CONSTRAINT IDENTIFICATION: What rules govern the forms? (sandhi, vowel harmony, consonant mutation...)
130
+ 5. VERIFICATION: Test your hypothesis against any exceptions in the data
131
+ 6. APPLICATION: Apply the deduced rules to the query items
132
+
133
+ After completing the scratchpad, provide your FINAL answer.
134
+ """
135
+
136
+ BASE_SYSTEM_PROMPT = (
137
+ "You are an expert linguist competing in the International Linguistics Olympiad. "
138
+ "You will analyze linguistic data, deduce the underlying grammar rules, and solve problems.\n\n"
139
+ + GRAMMAR_SCRATCHPAD + "\n\n"
140
+ "Respond with:\n"
141
+ "1. EXPLANATION: A clear summary of your reasoning and the rules you found\n"
142
+ "2. FINAL: A JSON array with exactly one answer string per numbered item\n"
143
+ " - Preserve diacritics and special characters exactly\n"
144
+ " - Match the orthography seen in the examples\n"
145
+ " - Do not include any markdown formatting or extra text"
146
+ )
147
+
148
+ def build_router_aware_prompt(context: str, query: str, task_type: str, eval_type: str, k: int) -> str:
149
+ """Build a prompt enriched with task routing information."""
150
+ profile = get_task_profile(task_type)
151
+
152
+ header = f"""【TASK PROFILE】
153
+ Task Type: {task_type}
154
+ Evaluation: {eval_type} (using {profile['eval_metric']})
155
+ Number of Items: {k}
156
+
157
+ Focus: {profile['grammar_focus']}
158
+ """
159
+
160
+ hint = profile["format_hint"]
161
+ return f"{header}\n---\nDATA:\n{context.strip()}\n\nQUERY ({k} items):\n{query.strip()}\n\nFormat: {hint}"
162
+
163
+ # =============================================================================
164
+ # 3. TEST-TIME SCALING (Self-Consistency Engine)
165
+ # Generates N=3 paths (T=0.3) -> Consensus Voting
166
+ # =============================================================================
167
+
168
+ def generate_n_paths(prompt: str, n: int = NUM_PATHS, temperature: float = TEMPERATURE) -> list:
169
+ """Generate N diverse reasoning paths using sampling."""
170
+ messages = [
171
+ {"role": "system", "content": BASE_SYSTEM_PROMPT},
172
+ {"role": "user", "content": prompt},
173
+ ]
174
+ model_inputs = tok.apply_chat_template(
175
+ messages, add_generation_prompt=True, return_tensors="pt"
176
+ )
177
+ ids = model_inputs['input_ids'].to(model.device)
178
+
179
+ paths = []
180
+ for path_idx in range(n):
181
+ try:
182
+ with torch.no_grad():
183
+ out = model.generate(
184
+ ids,
185
+ max_new_tokens=MAX_NEW_TOKENS,
186
+ do_sample=True,
187
+ temperature=temperature,
188
+ top_p=0.9,
189
+ pad_token_id=tok.eos_token_id,
190
+ )
191
+ text = tok.decode(out[0][ids.shape[-1]:], skip_special_tokens=True).strip()
192
+ paths.append(text)
193
+ print(f" Path {path_idx+1}/{n} generated", flush=True)
194
+ except Exception as e:
195
+ print(f" Path {path_idx+1} failed: {e}", flush=True)
196
+ paths.append("") # placeholder for failed generation
197
+ return paths
198
+
199
+ def chrF_score(candidate: str, reference: str) -> float:
200
+ """Compute character n-gram F-score (simplified chrF)."""
201
+ if not candidate or not reference:
202
+ return 0.0
203
+
204
+ def get_ngrams(s, n):
205
+ s = s.lower()
206
+ return [s[i:i+n] for i in range(len(s) - n + 1)]
207
+
208
+ # Use n-grams of size 2, 3, 4, 5, 6 (chrF default)
209
+ total_f = 0.0
210
+ for n in range(2, 7):
211
+ cand_ngrams = Counter(get_ngrams(candidate, n))
212
+ ref_ngrams = Counter(get_ngrams(reference, n))
213
+
214
+ overlapping = sum((cand_ngrams & ref_ngrams).values())
215
+ precision = overlapping / max(sum(cand_ngrams.values()), 1)
216
+ recall = overlapping / max(sum(ref_ngrams.values()), 1)
217
+
218
+ if precision + recall > 0:
219
+ f = 2 * precision * recall / (precision + recall)
220
+ total_f += f
221
+
222
+ return total_f / 6.0 # average over n-gram sizes
223
+
224
+ def consensus_vote(path_answers: list, eval_metric: str) -> tuple:
225
+ """
226
+ Aggregate N paths using consensus voting.
227
+ - For exact match: majority vote (string equality)
228
+ - For chrF: fuzzy clustering and highest-confidence cluster
229
+ Returns (consensus_answer, confidence, explanation)
230
+ """
231
+ if not path_answers or len(path_answers) == 0:
232
+ return [], 0.0, "No paths generated"
233
+
234
+ path_answers = [ans for ans in path_answers if ans] # filter empty
235
+ if not path_answers:
236
+ return [], 0.0, "All paths failed"
237
+
238
+ k = len(path_answers[0])
239
+ consensus = []
240
+ confidences = []
241
+
242
+ for item_idx in range(k):
243
+ # Extract item answer from each path
244
+ item_answers = []
245
+ for path in path_answers:
246
+ if item_idx < len(path):
247
+ item_answers.append(path[item_idx])
248
+
249
+ if not item_answers:
250
+ consensus.append("")
251
+ confidences.append(0.0)
252
+ continue
253
+
254
+ if eval_metric == "exact":
255
+ # Exact match majority voting
256
+ answer_counts = Counter(item_answers)
257
+ best_answer, count = answer_counts.most_common(1)[0]
258
+ confidence = count / len(item_answers)
259
+ consensus.append(best_answer)
260
+ confidences.append(confidence)
261
+ else:
262
+ # chrF-based clustering (fuzzy voting)
263
+ # Find the answer that maximizes average similarity to others
264
+ best_answer = item_answers[0]
265
+ best_score = 0.0
266
 
267
+ for candidate in item_answers:
268
+ score = sum(chrF_score(candidate, other) for other in item_answers) / len(item_answers)
269
+ if score > best_score:
270
+ best_score = score
271
+ best_answer = candidate
272
+
273
+ # Confidence based on agreement
274
+ agreeing = sum(1 for ans in item_answers if chrF_score(best_answer, ans) > 0.8)
275
+ confidence = agreeing / len(item_answers)
276
+ consensus.append(best_answer)
277
+ confidences.append(confidence)
278
+
279
+ avg_confidence = sum(confidences) / len(confidences) if confidences else 0.0
280
+
281
+ # Build explanation showing voting outcome
282
+ explanation_parts = [
283
+ f"Generated {len(path_answers)} reasoning paths with T={TEMPERATURE}.",
284
+ f"Consensus voting ({eval_metric} metric) selected answers with {avg_confidence:.0%} avg confidence.",
285
+ ]
286
+
287
+ return consensus, avg_confidence, " | ".join(explanation_parts)
288
+
289
+ def extract_answers_from_text(text: str, expected_k: int) -> list:
290
+ """Extract the JSON answer array from model output."""
291
+ # Try FINAL: block first
292
+ m = re.search(r"FINAL:\s*(\[.*?\])", text, re.DOTALL | re.IGNORECASE)
293
  if m:
294
  try:
295
  parsed = json.loads(m.group(1))
 
297
  return [str(x) for x in parsed]
298
  except Exception:
299
  pass
300
+
301
+ # Look for any JSON array
302
  for m2 in re.finditer(r"\[.*?\]", text, re.DOTALL):
303
  try:
304
  parsed = json.loads(m2.group(0))
 
306
  return [str(x) for x in parsed]
307
  except Exception:
308
  continue
309
+
310
+ # Fallback: extract lines after FINAL:
311
  tail = text.split("FINAL:")[-1]
312
+ lines = [re.sub(r"^\s*\d+[\.\)]\s*", "", l).strip(' \t\"\'')
313
+ for l in tail.splitlines() if l.strip()]
314
  lines = [l for l in lines if l]
315
+ return lines
 
 
316
 
317
  def extract_explanation(text: str) -> str:
318
+ """Extract the explanation portion from output."""
319
+ m = re.search(r"EXPLANATION:\s*(.*?)\s*(?:FINAL:|$)", text, re.DOTALL | re.IGNORECASE)
320
+ if m:
321
+ return m.group(1).strip()[:1000]
322
+ return ""
323
+
324
+ # =============================================================================
325
+ # 4. DETERMINISTIC ALIGNMENT & FALLBACK GUARDRAIL
326
+ # Enforces exact K-length JSON array & chrF recovery
327
+ # =============================================================================
328
+
329
+ def enforce_k_length(answers: list, k: int, context: str = "", query: str = "") -> list:
330
+ """
331
+ Deterministically ensure output is exactly K items.
332
+ - Pad with empty strings if too few
333
+ - Truncate if too many
334
+ """
335
+ answers = list(answers)[:k] # Take first k
336
+ while len(answers) < k:
337
+ answers.append("") # Pad to k
338
+ return answers
339
+
340
+ def chrF_fallback_recovery(paths: list, expected_k: int, target_item_idx: int,
341
+ reference: str = "") -> str:
342
+ """
343
+ When consensus fails, try chrF-based selection from all paths.
344
+ """
345
+ candidates = []
346
+ for path in paths:
347
+ answers = extract_answers_from_text(path, expected_k)
348
+ if target_item_idx < len(answers) and answers[target_item_idx]:
349
+ candidates.append(answers[target_item_idx])
350
+
351
+ if not candidates:
352
+ return ""
353
+
354
+ # Return the answer that appears most frequently (fallback voting)
355
+ counts = Counter(candidates)
356
+ return counts.most_common(1)[0][0]
357
+
358
+ def process_row_with_architecture(row: pd.Series) -> dict:
359
+ """
360
+ Main processing function implementing the 4-stage architecture.
361
+ """
362
+ # STAGE 1: Dynamic Task Router
363
+ task_type = row.get("task_type", "translation")
364
+ eval_type = row.get("eval_type", "chr_f1")
365
+ context = row["context"]
366
+ query = row["query"]
367
+ k = count_items(query)
368
+
369
+ profile = get_task_profile(task_type)
370
+ eval_metric = "exact" if eval_type.startswith("exact") else "chrF"
371
+
372
+ print(f"\n[{row['id']}] Task={task_type}, Eval={eval_metric}, K={k}", flush=True)
373
+
374
+ # Build router-aware prompt with grammar scratchpad hint
375
+ prompt = build_router_aware_prompt(context, query, task_type, eval_type, k)
376
+
377
+ # STAGE 2 & 3: Grammar Scratchpad + Test-Time Scaling
378
+ print(f" Generating N={NUM_PATHS} paths with Grammar Scratchpad...", flush=True)
379
+ paths = generate_n_paths(prompt, n=NUM_PATHS, temperature=TEMPERATURE)
380
+
381
+ # Extract answers from each path
382
+ path_answers = []
383
+ for i, path in enumerate(paths):
384
+ if path:
385
+ answers = extract_answers_from_text(path, k)
386
+ answers = enforce_k_length(answers, k)
387
+ path_answers.append(answers)
388
+ print(f" Path {i+1}: {answers}", flush=True)
389
+
390
+ # STAGE 3: Consensus Voting
391
+ if path_answers:
392
+ consensus, confidence, voting_explanation = consensus_vote(path_answers, eval_metric)
393
+ consensus = enforce_k_length(consensus, k)
394
+ print(f" Consensus: {consensus} (confidence={confidence:.2f})", flush=True)
395
+ else:
396
+ consensus = [""] * k
397
+ confidence = 0.0
398
+ voting_explanation = "No valid paths generated"
399
+
400
+ # STAGE 4: Deterministic Alignment & Fallback
401
+ final_answers = enforce_k_length(consensus, k)
402
+
403
+ # chrF fallback: if confidence is low, try to recover
404
+ if confidence < 0.5 and path_answers:
405
+ for i in range(k):
406
+ if not final_answers[i] or final_answers[i].strip() == "":
407
+ recovered = chrF_fallback_recovery(paths, k, i)
408
+ if recovered:
409
+ final_answers[i] = recovered
410
+ print(f" chrF recovery for item {i+1}: {recovered}", flush=True)
411
+
412
+ # Final alignment check
413
+ final_answers = enforce_k_length(final_answers, k)
414
+
415
+ # Extract explanation from best path (highest confidence answer source)
416
+ best_explanation = ""
417
+ for path in paths:
418
+ if path:
419
+ best_explanation = extract_explanation(path)
420
+ if best_explanation:
421
+ break
422
+
423
+ # Combine with voting explanation
424
+ full_explanation = f"{voting_explanation} | Grammar Analysis: {best_explanation[:500]}"
425
+
426
+ return {
427
+ "id": row["id"],
428
+ "prediction": json.dumps(final_answers, ensure_ascii=False),
429
+ "explanation": full_explanation[:1200],
430
+ }
431
+
432
+ # =============================================================================
433
+ # MAIN PROCESSING LOOP
434
+ # =============================================================================
435
 
436
  rows_out = []
437
  n_rows = len(df)
438
 
439
  for i, r in df.iterrows():
440
+ expected_k = count_items(r["query"])
441
  remaining_rows = n_rows - i
442
  per_row_budget = (time_left() - SAFETY_BUFFER) / max(1, remaining_rows)
443
 
444
+ if time_left() < SAFETY_BUFFER or per_row_budget < 10:
445
+ # Out of time: use fast fallback
446
  rows_out.append({
447
  "id": r["id"],
448
+ "prediction": json.dumps([""] * expected_k, ensure_ascii=False),
449
+ "explanation": "Timeout fallback - no computation time remaining",
450
  })
451
  continue
452
 
453
  try:
454
+ result = process_row_with_architecture(r)
455
+ rows_out.append(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  except Exception as e:
457
+ print(f"Error processing {r['id']}: {e}", flush=True)
458
  rows_out.append({
459
  "id": r["id"],
460
+ "prediction": json.dumps([""] * expected_k, ensure_ascii=False),
461
+ "explanation": f"Error: {str(e)[:200]}",
462
  })
463
 
464
+ # Write after every row for persistence
465
  pd.DataFrame(rows_out).to_csv("submission.csv", index=False)
466
+ print(f"Progress: {len(rows_out)}/{n_rows} rows, {time_left():.0f}s left", flush=True)
467
 
468
+ # Final write
469
  pd.DataFrame(rows_out).to_csv("submission.csv", index=False)
470
+ print("\n=== submission.csv written successfully ===", flush=True)
471
+ print(f"Total time: {time.time() - START:.1f}s", flush=True)