fengxr93 commited on
Commit
9dbdae6
·
1 Parent(s): cb894ce

fix: normalize explicit TSQA choice labels

Browse files
Benchmark_eval/TSQA/_build/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/4level_grpo_min2/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/ChatTS-14B/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/GPT-4o/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/Qwen2.5-7B-Instruct/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/Time-MQA-Mistral-7B/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/Time-MQA-Qwen-2.5-7B/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/joint_grpo_min2/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/joint_sft/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/joint_sft_14B/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/ts-align-bk-joint-grpo-min2/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296
 
results/ts-align/TSQA/_infer/score/schema.py CHANGED
@@ -162,7 +162,20 @@ _BOXED_RE = re.compile(r"\\boxed\s*\{([^{}]*)\}")
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
- _LETTER_RE = re.compile(r"\b([A-Ea-e])\b") # MCQ up to 5 options (EngineMT uses a-e)
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  def answer_block(text: str) -> str:
@@ -233,10 +246,12 @@ def parse_number(text: str) -> float | None:
233
 
234
 
235
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
236
- """Strict choice extraction: \\boxed{} or <answer> tag only (no prose scraping).
237
 
238
- Order: \\boxed{} letter/option -> letter/option inside <answer>..</answer>.
239
- Returns normalized letter (upper) or matched option text, else None.
 
 
240
  """
241
  boxed = extract_boxed(text)
242
  if boxed:
@@ -255,6 +270,27 @@ def parse_choice(text: str, options: list[str] | None = None) -> str | None:
255
  for opt in options:
256
  if opt and opt.lower() in low:
257
  return opt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  return None
259
 
260
 
 
162
  _ANSWER_RE = re.compile(r"<answer>\s*(.*?)\s*</answer>", re.DOTALL | re.IGNORECASE)
163
  _THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
164
  _NUM_RE = re.compile(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?")
165
+ _LETTER_RE = re.compile(r"\b([A-Ha-h])\b")
166
+ _LEADING_CHOICE_RE = re.compile(
167
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s*(?=$|\n))"
168
+ )
169
+ _EXPLICIT_CHOICE_RE = re.compile(
170
+ r"(?is)(?:\*{0,2})(?:(?:final|correct)\s+)?"
171
+ r"(?:answer|option|choice|conclusion)(?:\*{0,2})\s*"
172
+ r"(?:is\s*[::]?|[::])\s*(?:\*{0,2})(?:option\s+)?"
173
+ r"\(?\s*([A-Ha-h])\b"
174
+ )
175
+ _EXPLICIT_TRUE_FALSE_RE = re.compile(
176
+ r"(?is)(?:answer|conclusion|statement)\s*(?:is\s*[::]?|[::])\s*"
177
+ r"(true|false)\b"
178
+ )
179
 
180
 
181
  def answer_block(text: str) -> str:
 
246
 
247
 
248
  def parse_choice(text: str, options: list[str] | None = None) -> str | None:
249
+ """Extract an explicit choice without scraping arbitrary prose.
250
 
251
+ Order: \\boxed{} -> <answer> -> leading option label -> explicit answer cue.
252
+ The two fallbacks accept unambiguous forms such as ``B) False``, a leading
253
+ bare ``B``, ``Final answer: B``, or ``Conclusion: B``. They deliberately do
254
+ not search for isolated letters elsewhere in prose.
255
  """
256
  boxed = extract_boxed(text)
257
  if boxed:
 
270
  for opt in options:
271
  if opt and opt.lower() in low:
272
  return opt
273
+ leading = _LEADING_CHOICE_RE.match(text)
274
+ if leading:
275
+ return leading.group(1).upper()
276
+ explicit = list(_EXPLICIT_CHOICE_RE.finditer(text))
277
+ if explicit:
278
+ return explicit[-1].group(1).upper()
279
+ if options:
280
+ true_false_labels = {}
281
+ for option in options:
282
+ match = re.match(
283
+ r"^\s*\(?\s*([A-Ha-h])(?:\s*\)|\s*[.:\-]|\s+)\s*"
284
+ r"(true|false)\b",
285
+ option,
286
+ re.IGNORECASE,
287
+ )
288
+ if match:
289
+ true_false_labels[match.group(2).lower()] = match.group(1).upper()
290
+ if len(true_false_labels) == 2:
291
+ tf_matches = list(_EXPLICIT_TRUE_FALSE_RE.finditer(text))
292
+ if tf_matches:
293
+ return true_false_labels[tf_matches[-1].group(1).lower()]
294
  return None
295
 
296