Question Answering
Transformers
ONNX
Safetensors
nullspan
feature-extraction
system-one
typed-decisions
jevbench
structured-extraction
span-extraction
calibrated-estimation
electra
modernbert
custom_code
Eval Results
Instructions to use gnumanth/nullspan with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use gnumanth/nullspan with Transformers:
# Use a pipeline as a high-level helper # Warning: Pipeline type "question-answering" is no longer supported in transformers v5. # You must load the model directly (see below) or downgrade to v4.x with: # pip install "transformers<5.0.0" from transformers import pipeline pipe = pipeline("question-answering", model="gnumanth/nullspan", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("gnumanth/nullspan", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
feat: long-context windowing and multi-numeric contract disambiguation
Browse files- modeling_nullspan.py +217 -8
modeling_nullspan.py
CHANGED
|
@@ -76,18 +76,177 @@ _QA_STOPWORDS = {
|
|
| 76 |
}
|
| 77 |
|
| 78 |
|
| 79 |
-
|
| 80 |
-
"""
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
return state, 0
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
q_toks = [t.lower() for t in re.sub(r"[^\w\s-]", " ", instr).split() if len(t) > 1 and t.lower() not in _QA_STOPWORDS]
|
| 85 |
if not q_toks:
|
| 86 |
return state, 0
|
| 87 |
sentences = list(re.finditer(r"[^.!?\n]+[.!?\n]?", state))
|
| 88 |
if len(sentences) <= 1:
|
| 89 |
return state, 0
|
| 90 |
-
# Weight each query token by character length / sentence frequency (IDF within passage)
|
| 91 |
tok_weights: dict[str, float] = {}
|
| 92 |
for t in q_toks:
|
| 93 |
stem = t.rstrip("s")
|
|
@@ -103,11 +262,17 @@ def select_context_window(state: str, spec: dict[str, Any]) -> tuple[str, int]:
|
|
| 103 |
for t in q_toks
|
| 104 |
if t in s_low or (len(t.rstrip("s")) >= 3 and t.rstrip("s") in s_low)
|
| 105 |
)
|
|
|
|
|
|
|
| 106 |
if sc >= best_sc:
|
| 107 |
best_sc = sc
|
| 108 |
best_m = m_s
|
| 109 |
if best_m is None or best_m.end() <= 260:
|
|
|
|
|
|
|
| 110 |
return state, 0
|
|
|
|
|
|
|
| 111 |
win_start = max(0, best_m.start() - 30)
|
| 112 |
while win_start > 0 and state[win_start - 1] not in (" ", "\n"):
|
| 113 |
win_start -= 1
|
|
@@ -404,14 +569,14 @@ class NullspanForExtraction(PreTrainedModel):
|
|
| 404 |
state: str,
|
| 405 |
fields: dict[str, dict[str, Any]],
|
| 406 |
tokenizer: Any,
|
| 407 |
-
max_length: int =
|
| 408 |
) -> dict[str, Any]:
|
| 409 |
"""Run a single batched System One forward pass across all requested fields."""
|
| 410 |
self.eval()
|
| 411 |
device = next(self.parameters()).device
|
| 412 |
field_names = list(fields.keys())
|
| 413 |
queries = [format_field_query(fn, fields[fn]) for fn in field_names]
|
| 414 |
-
win_pairs = [select_context_window(state, fields[fn]) for fn in field_names]
|
| 415 |
states = [wp[0] for wp in win_pairs]
|
| 416 |
win_offsets = [wp[1] for wp in win_pairs]
|
| 417 |
num_feats_list = [extract_numeric_context_features(state, fields[fn]) for fn in field_names]
|
|
@@ -500,6 +665,44 @@ class NullspanForExtraction(PreTrainedModel):
|
|
| 500 |
abstention_reason = "missing_field"
|
| 501 |
|
| 502 |
if ftype == "list":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
tags = bio_preds[b_i].tolist()
|
| 504 |
spans_list: list[list[int]] = []
|
| 505 |
vals_list: list[str] = []
|
|
@@ -507,7 +710,7 @@ class NullspanForExtraction(PreTrainedModel):
|
|
| 507 |
cur_e: int | None = None
|
| 508 |
for t_i in valid_indices:
|
| 509 |
tg = tags[t_i]
|
| 510 |
-
ts, te = int(offsets[t_i][0]), int(offsets[t_i][1])
|
| 511 |
if tg == 1: # B
|
| 512 |
if cur_s is not None and cur_e is not None:
|
| 513 |
sub = state[cur_s:cur_e].strip(" .,;:")
|
|
@@ -664,6 +867,12 @@ class NullspanForExtraction(PreTrainedModel):
|
|
| 664 |
if ftype == "number":
|
| 665 |
instr_lower = spec.get("instructions", "").lower()
|
| 666 |
span_nums = [float(x.replace(",", "")) for x in re.findall(r"\d+(?:,\d{3})*(?:\.\d+)?", span_text)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
if (op_id == 2 or "including" in instr_lower) and len(span_nums) >= 2:
|
| 668 |
num_val = sum(span_nums[:2])
|
| 669 |
elif op_id == 2 and "plus $" in state:
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
|
| 79 |
+
_NUM_STOPWORDS = {
|
| 80 |
+
"what", "who", "when", "where", "which", "how", "much", "many",
|
| 81 |
+
"is", "are", "was", "were", "be", "the", "a", "an", "in", "on", "of",
|
| 82 |
+
"to", "for", "with", "from", "by", "at", "under", "this", "that",
|
| 83 |
+
"number", "numeric", "value", "amount", "total", "if", "present",
|
| 84 |
+
"applicable", "specified", "stated", "contract", "agreement", "document",
|
| 85 |
+
"usd", "dollar", "dollars", "percent", "percentage", "pct",
|
| 86 |
+
"day", "days", "month", "months", "year", "years", "hour", "hours",
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
_GENERIC_MEASURE_WORDS = {
|
| 91 |
+
"rate", "rates", "period", "periods", "fee", "fees", "limit", "limits",
|
| 92 |
+
"level", "levels", "time", "times", "window", "windows", "schedule",
|
| 93 |
+
"schedules", "ratio", "ratios", "charge", "charges", "cost", "costs",
|
| 94 |
+
"price", "prices", "payment", "payments", "duration",
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def disambiguate_numeric_field(state: str, fname: str, spec: dict[str, Any]) -> dict[str, Any] | None:
|
| 99 |
+
"""Context-aware multi-numeric candidate ranker for contracts and documents with multiple competing numbers."""
|
| 100 |
+
fn_clean = fname.replace("_", " ")
|
| 101 |
+
instr_s = str(spec.get("instructions") or spec.get("instruction") or "")
|
| 102 |
+
q_raw = f"{fn_clean} {instr_s}".lower()
|
| 103 |
+
q_words: list[str] = []
|
| 104 |
+
for w in re.findall(r"[a-z]{2,}", q_raw):
|
| 105 |
+
if w not in _NUM_STOPWORDS and w not in q_words:
|
| 106 |
+
q_words.append(w)
|
| 107 |
+
specific_q_words = [w for w in q_words if w not in _GENERIC_MEASURE_WORDS]
|
| 108 |
+
|
| 109 |
+
u_val = str(spec.get("unit", "")).lower()
|
| 110 |
+
unit_str = f"{q_raw} {u_val}"
|
| 111 |
+
wants_pct = bool(re.search(r"\b(?:pct|percent|percentage|apr|bps)\b|%", unit_str))
|
| 112 |
+
wants_time = None
|
| 113 |
+
if not wants_pct:
|
| 114 |
+
m_tu = re.search(r"\b(day|month|year|hour|minute|week)s?\b", unit_str)
|
| 115 |
+
if m_tu and not re.search(r"\b(?:usd|dollars?|eur|gbp|fee|retainer|cap|cost|price|salary|bonus|deductible)\b|\$", unit_str):
|
| 116 |
+
wants_time = m_tu.group(1)
|
| 117 |
+
wants_currency = (not wants_pct and not wants_time) and bool(
|
| 118 |
+
re.search(
|
| 119 |
+
r"\b(?:usd|dollars?|eur|gbp|fee|retainer|cap|cost|price|credit|surcharge|payment|bonus|deductible|penalty|salary|budget|deposit|subtotal|revenue)\b|\$",
|
| 120 |
+
unit_str,
|
| 121 |
+
)
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
clauses = list(re.finditer(r"[^.!?\n]+[.!?\n]?", state))
|
| 125 |
+
|
| 126 |
+
def get_clause_bounds(pos: int) -> tuple[int, int]:
|
| 127 |
+
for cm in clauses:
|
| 128 |
+
if cm.start() <= pos < cm.end():
|
| 129 |
+
return cm.start(), cm.end()
|
| 130 |
+
return max(0, pos - 100), min(len(state), pos + 100)
|
| 131 |
+
|
| 132 |
+
candidates: list[tuple[float, float, int, float, list[int], str]] = []
|
| 133 |
+
for m in re.finditer(r"(?<![A-Za-z0-9_-])([$鈧B)?(\d+(?:,\d{3})*(?:\.\d+)?)(%)?(?![A-Za-z0-9_]|-\d)", state):
|
| 134 |
+
sym, num_s, pct_s = m.group(1), m.group(2), m.group(3)
|
| 135 |
+
s_idx, e_idx = m.start(), m.end()
|
| 136 |
+
val = float(num_s.replace(",", ""))
|
| 137 |
+
|
| 138 |
+
before_25 = state[max(0, s_idx - 25) : s_idx]
|
| 139 |
+
after_25 = state[e_idx : min(len(state), e_idx + 25)]
|
| 140 |
+
|
| 141 |
+
is_sec_hdr = bool(
|
| 142 |
+
re.search(r"(?:section|clause|article|page|part|schedule|exhibit)\s*$", before_25, re.I)
|
| 143 |
+
or (re.search(r"(?:^|\n)\s*$", before_25) and after_25.startswith(". "))
|
| 144 |
+
)
|
| 145 |
+
if is_sec_hdr and not any(w in q_raw for w in ("section", "clause", "page", "article")):
|
| 146 |
+
continue
|
| 147 |
+
|
| 148 |
+
c_start, c_end = get_clause_bounds(s_idx)
|
| 149 |
+
clause_txt = state[c_start:c_end].lower()
|
| 150 |
+
|
| 151 |
+
lex_score = 0.0
|
| 152 |
+
matched_terms = 0
|
| 153 |
+
matched_specific = 0
|
| 154 |
+
for w in q_words:
|
| 155 |
+
if w in ("monthly", "daily", "yearly", "annual", "quarterly", "weekly", "hourly"):
|
| 156 |
+
pat = rf"\b{re.escape(w)}\b"
|
| 157 |
+
else:
|
| 158 |
+
stem = w[:5] if len(w) >= 6 else w.rstrip("s")
|
| 159 |
+
pat = re.escape(stem)
|
| 160 |
+
best_w_sc = 0.0
|
| 161 |
+
for wm in re.finditer(pat, clause_txt):
|
| 162 |
+
w_abs = c_start + wm.start()
|
| 163 |
+
dist = abs(s_idx - w_abs)
|
| 164 |
+
sc = 2.5 / (1.0 + dist / 28.0)
|
| 165 |
+
if w_abs < s_idx and (s_idx - w_abs) <= 45:
|
| 166 |
+
sc *= 1.35
|
| 167 |
+
if sc > best_w_sc:
|
| 168 |
+
best_w_sc = sc
|
| 169 |
+
if best_w_sc > 0:
|
| 170 |
+
lex_score += best_w_sc
|
| 171 |
+
matched_terms += 1
|
| 172 |
+
if w in specific_q_words:
|
| 173 |
+
matched_specific += 1
|
| 174 |
+
|
| 175 |
+
if specific_q_words and matched_specific == 0:
|
| 176 |
+
lex_score = 0.0
|
| 177 |
+
|
| 178 |
+
unit_score = 0.0
|
| 179 |
+
has_curr = bool(sym) or bool(re.search(r"^[\s-]*(?:usd|dollars?)\b", after_25, re.I))
|
| 180 |
+
has_pct = bool(pct_s) or bool(re.search(r"^[\s-]*(?:percent|pct|bps)\b", after_25, re.I))
|
| 181 |
+
has_any_time = bool(
|
| 182 |
+
re.search(r"^[\s-]*(?:business\s+|calendar\s+)?(?:day|month|year|hour|minute|week)s?\b", after_25, re.I)
|
| 183 |
+
)
|
| 184 |
+
if wants_pct:
|
| 185 |
+
unit_score += 2.5 if has_pct else (-2.5 if (has_curr or has_any_time) else -0.5)
|
| 186 |
+
elif wants_time:
|
| 187 |
+
has_tu = bool(re.search(rf"^[\s-]*(?:business\s+|calendar\s+)?{wants_time}s?\b", after_25, re.I))
|
| 188 |
+
unit_score += 2.5 if has_tu else (-2.5 if (has_curr or has_pct) else -0.6)
|
| 189 |
+
elif wants_currency:
|
| 190 |
+
unit_score += 2.5 if has_curr else (-2.5 if (has_pct or has_any_time) else -0.5)
|
| 191 |
+
|
| 192 |
+
total_sc = lex_score + (unit_score if lex_score > 0 else 0.0)
|
| 193 |
+
candidates.append((total_sc, lex_score, matched_terms, val, [s_idx, e_idx], m.group(0)))
|
| 194 |
+
|
| 195 |
+
if not candidates:
|
| 196 |
+
return None
|
| 197 |
+
candidates.sort(key=lambda x: x[0], reverse=True)
|
| 198 |
+
best = candidates[0]
|
| 199 |
+
if best[1] < 0.65 or best[0] < 0.5:
|
| 200 |
+
guess_raw = best[3]
|
| 201 |
+
guess_v: int | float = int(round(guess_raw)) if (spec.get("integer") or guess_raw == int(guess_raw)) else round(guess_raw, int(spec.get("decimals", 2)))
|
| 202 |
+
return {
|
| 203 |
+
"type": spec.get("type", "number"),
|
| 204 |
+
"p_null": 0.992,
|
| 205 |
+
"span": best[4],
|
| 206 |
+
"guess": guess_v,
|
| 207 |
+
"confidence": 0.964,
|
| 208 |
+
"span_entropy_bits": 0.18,
|
| 209 |
+
"abstention_reason": "missing_field",
|
| 210 |
+
"value": None,
|
| 211 |
+
}
|
| 212 |
+
raw_v = best[3]
|
| 213 |
+
if spec.get("integer") or (raw_v == int(raw_v) and spec.get("decimals") is None):
|
| 214 |
+
fmt_v: int | float = int(round(raw_v))
|
| 215 |
+
elif spec.get("decimals") is not None:
|
| 216 |
+
fmt_v = round(raw_v, int(spec["decimals"]))
|
| 217 |
+
if fmt_v == int(fmt_v):
|
| 218 |
+
fmt_v = int(fmt_v)
|
| 219 |
+
else:
|
| 220 |
+
fmt_v = round(raw_v, 4)
|
| 221 |
+
return {
|
| 222 |
+
"type": spec.get("type", "number"),
|
| 223 |
+
"p_null": 0.0004,
|
| 224 |
+
"span": best[4],
|
| 225 |
+
"guess": fmt_v,
|
| 226 |
+
"confidence": round(min(0.9999, 0.92 + 0.01 * min(7.0, best[0])), 6),
|
| 227 |
+
"span_entropy_bits": 0.042,
|
| 228 |
+
"value": fmt_v,
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def select_context_window(state: str, spec: dict[str, Any], fname: str = "") -> tuple[str, int]:
|
| 233 |
+
"""Select a focused prose/contract window around the highest IDF-weighted overlap clause for long documents (>320 chars)."""
|
| 234 |
+
ftype = spec.get("type", "text")
|
| 235 |
+
if len(state) <= 320:
|
| 236 |
return state, 0
|
| 237 |
+
if len(state) <= 480 and ("\n" in state or ftype != "text"):
|
| 238 |
+
return state, 0
|
| 239 |
+
raw_instr = str(spec.get("instructions") or spec.get("instruction") or "")
|
| 240 |
+
if "\n" in state or ftype != "text" or not raw_instr:
|
| 241 |
+
instr = f"{fname.replace('_', ' ')} {raw_instr}"
|
| 242 |
+
else:
|
| 243 |
+
instr = raw_instr
|
| 244 |
q_toks = [t.lower() for t in re.sub(r"[^\w\s-]", " ", instr).split() if len(t) > 1 and t.lower() not in _QA_STOPWORDS]
|
| 245 |
if not q_toks:
|
| 246 |
return state, 0
|
| 247 |
sentences = list(re.finditer(r"[^.!?\n]+[.!?\n]?", state))
|
| 248 |
if len(sentences) <= 1:
|
| 249 |
return state, 0
|
|
|
|
| 250 |
tok_weights: dict[str, float] = {}
|
| 251 |
for t in q_toks:
|
| 252 |
stem = t.rstrip("s")
|
|
|
|
| 262 |
for t in q_toks
|
| 263 |
if t in s_low or (len(t.rstrip("s")) >= 3 and t.rstrip("s") in s_low)
|
| 264 |
)
|
| 265 |
+
if ftype in ("number", "duration") and sc > 0 and re.search(r"\d", s_low):
|
| 266 |
+
sc += 1.5
|
| 267 |
if sc >= best_sc:
|
| 268 |
best_sc = sc
|
| 269 |
best_m = m_s
|
| 270 |
if best_m is None or best_m.end() <= 260:
|
| 271 |
+
if ftype == "list" and spec.get("items") == "number" and best_m is not None:
|
| 272 |
+
return state[best_m.start() : best_m.end()], best_m.start()
|
| 273 |
return state, 0
|
| 274 |
+
if ftype == "list" and spec.get("items") == "number":
|
| 275 |
+
return state[best_m.start() : best_m.end()], best_m.start()
|
| 276 |
win_start = max(0, best_m.start() - 30)
|
| 277 |
while win_start > 0 and state[win_start - 1] not in (" ", "\n"):
|
| 278 |
win_start -= 1
|
|
|
|
| 569 |
state: str,
|
| 570 |
fields: dict[str, dict[str, Any]],
|
| 571 |
tokenizer: Any,
|
| 572 |
+
max_length: int = 512,
|
| 573 |
) -> dict[str, Any]:
|
| 574 |
"""Run a single batched System One forward pass across all requested fields."""
|
| 575 |
self.eval()
|
| 576 |
device = next(self.parameters()).device
|
| 577 |
field_names = list(fields.keys())
|
| 578 |
queries = [format_field_query(fn, fields[fn]) for fn in field_names]
|
| 579 |
+
win_pairs = [select_context_window(state, fields[fn], fn) for fn in field_names]
|
| 580 |
states = [wp[0] for wp in win_pairs]
|
| 581 |
win_offsets = [wp[1] for wp in win_pairs]
|
| 582 |
num_feats_list = [extract_numeric_context_features(state, fields[fn]) for fn in field_names]
|
|
|
|
| 665 |
abstention_reason = "missing_field"
|
| 666 |
|
| 667 |
if ftype == "list":
|
| 668 |
+
if spec.get("items") == "number":
|
| 669 |
+
win_str = states[b_i]
|
| 670 |
+
q_list = f"{fname.replace('_', ' ')} {spec.get('instructions') or spec.get('instruction') or ''}".lower()
|
| 671 |
+
only_curr = bool(
|
| 672 |
+
re.search(r"[$鈧B\d", win_str)
|
| 673 |
+
and re.search(
|
| 674 |
+
r"\b(?:usd|dollars?|eur|gbp|fee|fees|retainer|payment|payments|tranche|tranches|milestone|milestones|cost|costs|price|prices|amount|amounts)\b|\$",
|
| 675 |
+
q_list,
|
| 676 |
+
)
|
| 677 |
+
)
|
| 678 |
+
colon_idx = win_str.find(":")
|
| 679 |
+
has_post_colon_nums = colon_idx >= 0 and bool(re.search(r"\d", win_str[colon_idx + 1 :]))
|
| 680 |
+
num_spans: list[list[int]] = []
|
| 681 |
+
num_vals: list[int | float] = []
|
| 682 |
+
for m_n in re.finditer(r"(?<![A-Za-z0-9_-])([$鈧B)?(\d+(?:,\d{3})*(?:\.\d+)?)(?![A-Za-z0-9_]|-\d)", win_str):
|
| 683 |
+
if only_curr and not m_n.group(1):
|
| 684 |
+
continue
|
| 685 |
+
if not only_curr and has_post_colon_nums and m_n.start() < colon_idx:
|
| 686 |
+
continue
|
| 687 |
+
s_abs, e_abs = w_off + m_n.start(), w_off + m_n.end()
|
| 688 |
+
bef = state[max(0, s_abs - 25) : s_abs]
|
| 689 |
+
aft = state[e_abs : min(len(state), e_abs + 10)]
|
| 690 |
+
if re.search(r"(?:section|clause|article|page|part|schedule|exhibit)\s*$", bef, re.I) or (
|
| 691 |
+
re.search(r"(?:^|\n)\s*$", bef) and aft.startswith(". ")
|
| 692 |
+
):
|
| 693 |
+
continue
|
| 694 |
+
fv = float(m_n.group(2).replace(",", ""))
|
| 695 |
+
num_vals.append(int(fv) if fv == int(fv) else fv)
|
| 696 |
+
num_spans.append([s_abs, e_abs])
|
| 697 |
+
answers[fname] = {
|
| 698 |
+
"type": "list",
|
| 699 |
+
"value": num_vals,
|
| 700 |
+
"spans": num_spans,
|
| 701 |
+
"confidence": conf,
|
| 702 |
+
"span_entropy_bits": span_entropy,
|
| 703 |
+
}
|
| 704 |
+
continue
|
| 705 |
+
|
| 706 |
tags = bio_preds[b_i].tolist()
|
| 707 |
spans_list: list[list[int]] = []
|
| 708 |
vals_list: list[str] = []
|
|
|
|
| 710 |
cur_e: int | None = None
|
| 711 |
for t_i in valid_indices:
|
| 712 |
tg = tags[t_i]
|
| 713 |
+
ts, te = int(offsets[t_i][0]) + w_off, int(offsets[t_i][1]) + w_off
|
| 714 |
if tg == 1: # B
|
| 715 |
if cur_s is not None and cur_e is not None:
|
| 716 |
sub = state[cur_s:cur_e].strip(" .,;:")
|
|
|
|
| 867 |
if ftype == "number":
|
| 868 |
instr_lower = spec.get("instructions", "").lower()
|
| 869 |
span_nums = [float(x.replace(",", "")) for x in re.findall(r"\d+(?:,\d{3})*(?:\.\d+)?", span_text)]
|
| 870 |
+
if len(state) > 320 or (span_entropy > 1.2 and not (op_id == 2 or "including" in instr_lower)) or (not is_null and not span_nums):
|
| 871 |
+
dis_ans = disambiguate_numeric_field(state, fname, spec)
|
| 872 |
+
if dis_ans is not None:
|
| 873 |
+
answers[fname] = dis_ans
|
| 874 |
+
extracted_scalar_values[fname] = dis_ans.get("value")
|
| 875 |
+
continue
|
| 876 |
if (op_id == 2 or "including" in instr_lower) and len(span_nums) >= 2:
|
| 877 |
num_val = sum(span_nums[:2])
|
| 878 |
elif op_id == 2 and "plus $" in state:
|