tensorcode 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,133 @@
1
+ """A small learned text classifier: TF-IDF features + multinomial logistic regression.
2
+
3
+ Requires the ``learned`` extra (scikit-learn, numpy). Abstention is governed by a
4
+ threshold chosen on held-out validation data to meet a target selective accuracy,
5
+ after temperature scaling on the same split. The fit report records what the
6
+ threshold is based on; nothing here is claimed beyond that split.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import enum
12
+ import time
13
+ from dataclasses import dataclass, field
14
+ from typing import Sequence
15
+
16
+ import numpy as np
17
+ from sklearn.feature_extraction.text import TfidfVectorizer
18
+ from sklearn.linear_model import LogisticRegression
19
+ from sklearn.pipeline import FeatureUnion
20
+
21
+ from ..outcomes import Score, Unknown
22
+ from ..runtime import Output, Profile, Request, Traits
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class FitReport:
27
+ train_size: int
28
+ validation_size: int
29
+ temperature: float
30
+ threshold: float
31
+ target_accuracy: float
32
+ validation_coverage: float
33
+ validation_selective_accuracy: float
34
+ validation_accuracy: float
35
+ fit_seconds: float
36
+ basis: str
37
+
38
+
39
+ def _softmax(z: np.ndarray) -> np.ndarray:
40
+ z = z - z.max(axis=1, keepdims=True)
41
+ e = np.exp(z)
42
+ return e / e.sum(axis=1, keepdims=True)
43
+
44
+
45
+ @dataclass
46
+ class LinearTextClassifier:
47
+ labels: type[enum.Enum]
48
+ features: FeatureUnion
49
+ model: LogisticRegression
50
+ temperature: float
51
+ threshold: float
52
+ basis: str
53
+ name: str = "tfidf-logreg"
54
+ version: str = "1"
55
+ op: str = "classify"
56
+ traits: Traits = Traits(locality="in_process", egress=False, deterministic=True, requires=frozenset({"sklearn"}))
57
+ profile: Profile = field(default_factory=lambda: Profile(source="declared: in-process, no metered spend; latency unmeasured", usd_per_call=0.0))
58
+
59
+ @classmethod
60
+ def fit(
61
+ cls,
62
+ texts: Sequence[str],
63
+ labels: Sequence[enum.Enum],
64
+ *,
65
+ label_type: type[enum.Enum],
66
+ validation: tuple[Sequence[str], Sequence[enum.Enum]],
67
+ target_accuracy: float,
68
+ basis: str,
69
+ seed: int = 0,
70
+ ) -> tuple[LinearTextClassifier, FitReport]:
71
+ t0 = time.perf_counter()
72
+ features = FeatureUnion(
73
+ [
74
+ ("word", TfidfVectorizer(ngram_range=(1, 2), min_df=1, sublinear_tf=True)),
75
+ ("char", TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5), min_df=2, sublinear_tf=True)),
76
+ ]
77
+ )
78
+ x = features.fit_transform(texts)
79
+ y = [label.value for label in labels]
80
+ model = LogisticRegression(C=20.0, max_iter=2000, random_state=seed)
81
+ model.fit(x, y)
82
+
83
+ val_texts, val_labels = validation
84
+ logits = model.decision_function(features.transform(val_texts))
85
+ val_y = np.array([model.classes_.tolist().index(label.value) for label in val_labels])
86
+
87
+ # temperature scaling: minimize validation NLL over a log grid
88
+ grid = np.exp(np.linspace(np.log(0.05), np.log(5.0), 200))
89
+ nll = [-np.log(_softmax(logits / t)[np.arange(len(val_y)), val_y] + 1e-12).mean() for t in grid]
90
+ temperature = float(grid[int(np.argmin(nll))])
91
+ probs = _softmax(logits / temperature)
92
+ conf, pred = probs.max(axis=1), probs.argmax(axis=1)
93
+ correct = pred == val_y
94
+
95
+ # lowest threshold whose selective accuracy on validation meets the target
96
+ order = np.argsort(-conf)
97
+ cum_acc = np.cumsum(correct[order]) / np.arange(1, len(order) + 1)
98
+ ok = np.nonzero(cum_acc >= target_accuracy)[0]
99
+ k = int(ok.max()) + 1 if len(ok) else 0
100
+ threshold = float(conf[order][k - 1]) if k else 1.0
101
+ impl = cls(label_type, features, model, temperature, threshold, basis)
102
+ report = FitReport(
103
+ train_size=len(texts),
104
+ validation_size=len(val_texts),
105
+ temperature=temperature,
106
+ threshold=threshold,
107
+ target_accuracy=target_accuracy,
108
+ validation_coverage=k / len(order),
109
+ validation_selective_accuracy=float(cum_acc[k - 1]) if k else float("nan"),
110
+ validation_accuracy=float(correct.mean()),
111
+ fit_seconds=time.perf_counter() - t0,
112
+ basis=basis,
113
+ )
114
+ return impl, report
115
+
116
+ def accepts(self, request: Request) -> bool:
117
+ return request.op == "classify" and request.target is self.labels and isinstance(request.subject, str)
118
+
119
+ def probabilities(self, texts: Sequence[str]) -> np.ndarray:
120
+ return _softmax(self.model.decision_function(self.features.transform(texts)) / self.temperature)
121
+
122
+ def run(self, requests: Sequence[Request]) -> list[Output]:
123
+ probs = self.probabilities([r.subject for r in requests])
124
+ classes = self.model.classes_
125
+ outs = []
126
+ for row in probs:
127
+ top = np.argsort(-row)[:3]
128
+ cands = tuple((self.labels(classes[i]), Score(float(row[i]), "probability", self.basis)) for i in top)
129
+ if row[top[0]] < self.threshold:
130
+ outs.append(Output(Unknown("below_threshold", f"p={row[top[0]]:.3f} < {self.threshold:.3f}", cands)))
131
+ else:
132
+ outs.append(Output(cands[0][0], cands[0][1]))
133
+ return outs
@@ -0,0 +1,361 @@
1
+ """Learned implementations: a trained request parser and a trained extractive answerer.
2
+
3
+ Requires the ``learned-neural`` extra (torch, transformers). Both are registered the same
4
+ way every other implementation is, so the runtime routes to them by declared traits and
5
+ the trace records which one answered.
6
+
7
+ The parser turns an utterance into ``ParsedRequest(act, slots)``; the answerer turns a
8
+ question plus passages into ``Answer(text, evidence)``. Each loads an artifact directory
9
+ that carries its own label space and its calibrated abstention threshold, so the code
10
+ here cannot drift from what was trained. Confidence is the calibrated probability that
11
+ the output is correct, measured on a held-out split named in the artifact; below the
12
+ threshold the implementation abstains with ``Unknown`` rather than guessing.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any, Mapping, Sequence
21
+
22
+ import torch
23
+ from torch import nn
24
+ from transformers import AutoModel, AutoTokenizer
25
+
26
+ from ..outcomes import Score, Unknown
27
+ from ..runtime import Output, Profile, Request, Traits
28
+
29
+
30
+ # --------------------------------------------------------------- what they answer
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ParsedRequest:
35
+ """What an utterance asks for: an act and its slots, as the assistant's procedures expect."""
36
+
37
+ act: str
38
+ slots: Mapping[str, Any] = field(default_factory=dict)
39
+ confidence: float | None = None
40
+ speech_act: str | None = None # command / question / self_disclosure / world_statement / other
41
+
42
+ def __repr__(self) -> str:
43
+ return f"{self.act}({', '.join(f'{k}={v!r}' for k, v in sorted(self.slots.items()))})"
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class QuestionOverPassages:
48
+ """A question and the passages that may contain its answer."""
49
+
50
+ question: str
51
+ passages: tuple[tuple[str, str], ...] # (title, sentence)
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Answer:
56
+ text: str
57
+ evidence: tuple[tuple[str, str], ...] = () # the passages the span came from
58
+ confidence: float | None = None
59
+
60
+
61
+ # ------------------------------------------------------------------- the models
62
+
63
+
64
+ class RequestParserModel(nn.Module):
65
+ """One encoder, four kinds of head: the act, a BIO tagger for span slots, closed choices, flags."""
66
+
67
+ def __init__(self, encoder_id: str, n_acts: int, n_tags: int, closed_sizes: Sequence[int], n_flags: int) -> None:
68
+ super().__init__()
69
+ self.encoder = AutoModel.from_pretrained(encoder_id)
70
+ h = self.encoder.config.hidden_size
71
+ self.act = nn.Linear(h, n_acts)
72
+ self.tags = nn.Linear(h, n_tags)
73
+ self.closed = nn.ModuleList([nn.Linear(h, n) for n in closed_sizes])
74
+ self.flags = nn.Linear(h, n_flags)
75
+
76
+ def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> dict[str, torch.Tensor]:
77
+ hidden = self.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
78
+ pooled = hidden[:, 0]
79
+ return {
80
+ "act": self.act(pooled),
81
+ "tags": self.tags(hidden),
82
+ "closed": [head(pooled) for head in self.closed],
83
+ "flags": self.flags(pooled),
84
+ "pooled": pooled,
85
+ }
86
+
87
+
88
+ class SpanAnswererModel(nn.Module):
89
+ """Start/end over the passage, plus one head for "the passage does not answer this"."""
90
+
91
+ def __init__(self, encoder_id: str) -> None:
92
+ super().__init__()
93
+ self.encoder = AutoModel.from_pretrained(encoder_id)
94
+ h = self.encoder.config.hidden_size
95
+ self.span = nn.Linear(h, 2)
96
+ self.answerable = nn.Linear(h, 2)
97
+
98
+ def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor, token_type_ids: torch.Tensor | None = None) -> dict[str, torch.Tensor]:
99
+ kw = {"token_type_ids": token_type_ids} if token_type_ids is not None else {}
100
+ hidden = self.encoder(input_ids=input_ids, attention_mask=attention_mask, **kw).last_hidden_state
101
+ start, end = self.span(hidden).split(1, dim=-1)
102
+ return {"start": start.squeeze(-1), "end": end.squeeze(-1), "answerable": self.answerable(hidden[:, 0])}
103
+
104
+
105
+ # ------------------------------------------------------------------ inference
106
+
107
+
108
+ def _device(prefer: str | None = None) -> str:
109
+ if prefer:
110
+ return prefer
111
+ return "cuda" if torch.cuda.is_available() else "cpu"
112
+
113
+
114
+ @dataclass
115
+ class NeuralRequestParser:
116
+ """A trained utterance -> (act, slots) parser, as a ``parse`` implementation."""
117
+
118
+ artifact: Path
119
+ device: str | None = None
120
+ batch_size: int = 64
121
+ threshold: float | None = None # overrides the artifact's calibrated threshold
122
+ name: str = "neural-request-parser"
123
+ op: str = "parse"
124
+
125
+ def __post_init__(self) -> None:
126
+ self.artifact = Path(self.artifact)
127
+ self.config = json.loads((self.artifact / "config.json").read_text())
128
+ self.device = _device(self.device)
129
+ self.tokenizer = AutoTokenizer.from_pretrained(str(self.artifact / "tokenizer"))
130
+ closed = self.config["closed_heads"]
131
+ self.model = RequestParserModel(
132
+ self.config["encoder"], len(self.config["acts"]), len(self.config["tags"]),
133
+ [len(v) for _, v in closed], len(self.config["flags"]),
134
+ )
135
+ state = torch.load(self.artifact / "weights.pt", map_location="cpu", weights_only=True)
136
+ self.model.load_state_dict(state)
137
+ self.model.to(self.device).eval()
138
+ self.version = str(self.config.get("version", "1"))
139
+ self._threshold = self.threshold if self.threshold is not None else float(self.config.get("threshold", 0.0))
140
+ self.traits = Traits(locality="in_process", egress=False, deterministic=True,
141
+ requires=frozenset({"cuda"} if self.device.startswith("cuda") else set()))
142
+ q = self.config.get("quality", {})
143
+ self.profile = Profile(source=self.config.get("measured_on", "unmeasured"), quality=q,
144
+ latency_ms_p50=self.config.get("latency_ms_p50"), latency_ms_p95=self.config.get("latency_ms_p95"),
145
+ usd_per_call=0.0, peak_memory_mb=self.config.get("peak_memory_mb"))
146
+
147
+ # -- runtime contract
148
+
149
+ def accepts(self, request: Request) -> bool:
150
+ return request.op == "parse" and request.target is ParsedRequest and isinstance(request.subject, str)
151
+
152
+ def run(self, requests: Sequence[Request]) -> list[Output]:
153
+ texts = [r.subject for r in requests]
154
+ parsed = self.parse(texts)
155
+ outs: list[Output] = []
156
+ for got in parsed:
157
+ score = Score(got.confidence or 0.0, "probability", self.profile.source or "unmeasured")
158
+ if got.act == "unknown":
159
+ reason = "not_a_request" if got.speech_act == "world_statement" else "not_a_known_request"
160
+ detail = ("this states something rather than asking for anything"
161
+ if got.speech_act == "world_statement" else f"no act fits this utterance (p={got.confidence:.2f})")
162
+ outs.append(Output(Unknown(reason, detail), score))
163
+ elif (got.confidence or 0.0) < self._threshold:
164
+ outs.append(Output(Unknown("below_threshold", f"p={got.confidence:.3f} < {self._threshold:.3f}", (got,)), score))
165
+ else:
166
+ outs.append(Output(got, score))
167
+ return outs
168
+
169
+ # -- the work
170
+
171
+ @torch.inference_mode()
172
+ def parse(self, texts: Sequence[str]) -> list[ParsedRequest]:
173
+ out: list[ParsedRequest] = []
174
+ for i in range(0, len(texts), self.batch_size):
175
+ out.extend(self._parse_batch(list(texts[i : i + self.batch_size])))
176
+ return out
177
+
178
+ @torch.inference_mode()
179
+ def embed(self, texts: Sequence[str]) -> torch.Tensor:
180
+ """Sentence embeddings from the same encoder, for associative recall over claims."""
181
+ vecs = []
182
+ for i in range(0, len(texts), self.batch_size):
183
+ batch = self.tokenizer(list(texts[i : i + self.batch_size]), return_tensors="pt", padding=True,
184
+ truncation=True, max_length=self.config["max_length"]).to(self.device)
185
+ vecs.append(self.model(batch["input_ids"], batch["attention_mask"])["pooled"].float().cpu())
186
+ return torch.cat(vecs) if vecs else torch.zeros(0, self.model.encoder.config.hidden_size)
187
+
188
+ def _parse_batch(self, texts: list[str]) -> list[ParsedRequest]:
189
+ cfg = self.config
190
+ enc = self.tokenizer(texts, return_tensors="pt", padding=True, truncation=True,
191
+ max_length=cfg["max_length"], return_offsets_mapping=True)
192
+ offsets = enc.pop("offset_mapping")
193
+ enc = {k: v.to(self.device) for k, v in enc.items()}
194
+ got = self.model(enc["input_ids"], enc["attention_mask"])
195
+ act_p = got["act"].softmax(-1).cpu()
196
+ tag_id = got["tags"].argmax(-1).cpu()
197
+ closed_id = [c.argmax(-1).cpu() for c in got["closed"]]
198
+ flag_on = (got["flags"] > 0).cpu()
199
+
200
+ whole_input = frozenset(cfg.get("whole_input_slots", ()))
201
+ results = []
202
+ for row, text in enumerate(texts):
203
+ act = cfg["acts"][int(act_p[row].argmax())]
204
+ confidence = float(act_p[row].max())
205
+ spans = _decode_spans(cfg["tags"], tag_id[row].tolist(), offsets[row].tolist(), text,
206
+ enc["attention_mask"][row].cpu().tolist())
207
+ slots: dict[str, Any] = {}
208
+ allowed = frozenset(cfg["act_slots"].get(act, []))
209
+ for slot, value in spans.items():
210
+ if slot in allowed and slot != "place":
211
+ slots[slot] = value
212
+ closed = {name: vocab[int(closed_id[k][row])] for k, (name, vocab) in enumerate(cfg["closed_heads"])}
213
+ if "place" in allowed:
214
+ kind = closed.get("place_kind", "none")
215
+ if kind == "span" and "place" in spans:
216
+ slots["place"] = spans["place"]
217
+ elif kind not in ("none", "span"):
218
+ slots["place"] = kind
219
+ if "target" in allowed and closed.get("target_kind", "none") == "@it":
220
+ slots["target"] = "@it" # "read it": the thing acted on is whatever was last touched
221
+ if "name" in allowed and closed.get("name_canon", "none") != "none" and "name" not in slots:
222
+ slots["name"] = closed["name_canon"] # "make a readme": a name supplied by convention
223
+ if act == "info" and closed.get("info_topic", "none") != "none":
224
+ slots["topic"] = closed["info_topic"]
225
+ if "unit" in allowed and closed.get("unit", "none") != "none":
226
+ slots["unit"] = closed["unit"]
227
+ for slot in allowed & whole_input:
228
+ slots[slot] = text # nothing to tag: the procedure reads the whole request back out
229
+ if "aspect" in allowed:
230
+ slots["aspect"] = closed.get("aspect", "none")
231
+ for k, flag in enumerate(cfg["flags"]):
232
+ if flag in allowed:
233
+ slots[flag] = bool(flag_on[row][k])
234
+ speech_act = closed.get("speech_act")
235
+ if speech_act == "world_statement":
236
+ # a remark about the world is not a request; acting on it would be a wrong action,
237
+ # so the speech act overrules whatever the act head preferred
238
+ act, slots = "unknown", {}
239
+ results.append(ParsedRequest(act, slots, confidence, speech_act))
240
+ return results
241
+
242
+
243
+ def _decode_spans(tags: Sequence[str], ids: Sequence[int], offsets: Sequence[Sequence[int]], text: str,
244
+ mask: Sequence[int]) -> dict[str, str]:
245
+ """First contiguous B/I run per slot, mapped back to the original characters."""
246
+ spans: dict[str, tuple[int, int]] = {}
247
+ current: tuple[str, int, int] | None = None
248
+ for position, tag_id in enumerate(ids):
249
+ if position >= len(offsets) or not mask[position]:
250
+ continue
251
+ start, end = offsets[position]
252
+ if start == end: # special token
253
+ continue
254
+ tag = tags[tag_id]
255
+ if tag == "O":
256
+ if current:
257
+ spans.setdefault(current[0], (current[1], current[2]))
258
+ current = None
259
+ continue
260
+ prefix, slot = tag.split("-", 1)
261
+ if current and current[0] == slot and prefix == "I":
262
+ current = (slot, current[1], end)
263
+ else:
264
+ if current:
265
+ spans.setdefault(current[0], (current[1], current[2]))
266
+ current = (slot, start, end)
267
+ if current:
268
+ spans.setdefault(current[0], (current[1], current[2]))
269
+ return {slot: text[a:b].strip() for slot, (a, b) in spans.items() if text[a:b].strip()}
270
+
271
+
272
+ @dataclass
273
+ class NeuralAnswerer:
274
+ """A trained extractive answerer over passages, as a ``parse`` implementation."""
275
+
276
+ artifact: Path
277
+ device: str | None = None
278
+ batch_size: int = 16
279
+ threshold: float | None = None
280
+ max_length: int = 384
281
+ name: str = "neural-span-answerer"
282
+ op: str = "parse"
283
+
284
+ def __post_init__(self) -> None:
285
+ self.artifact = Path(self.artifact)
286
+ self.config = json.loads((self.artifact / "config.json").read_text())
287
+ self.device = _device(self.device)
288
+ self.tokenizer = AutoTokenizer.from_pretrained(str(self.artifact / "tokenizer"))
289
+ self.model = SpanAnswererModel(self.config["encoder"])
290
+ self.model.load_state_dict(torch.load(self.artifact / "weights.pt", map_location="cpu", weights_only=True))
291
+ self.model.to(self.device).eval()
292
+ self.version = str(self.config.get("version", "1"))
293
+ self._threshold = self.threshold if self.threshold is not None else float(self.config.get("threshold", 0.0))
294
+ self.traits = Traits(locality="in_process", egress=False, deterministic=True,
295
+ requires=frozenset({"cuda"} if self.device.startswith("cuda") else set()))
296
+ self.profile = Profile(source=self.config.get("measured_on", "unmeasured"), quality=self.config.get("quality", {}),
297
+ latency_ms_p50=self.config.get("latency_ms_p50"), latency_ms_p95=self.config.get("latency_ms_p95"),
298
+ usd_per_call=0.0, peak_memory_mb=self.config.get("peak_memory_mb"))
299
+
300
+ def accepts(self, request: Request) -> bool:
301
+ return request.op == "parse" and request.target is Answer and isinstance(request.subject, QuestionOverPassages)
302
+
303
+ def run(self, requests: Sequence[Request]) -> list[Output]:
304
+ answers = self.answer([r.subject for r in requests])
305
+ outs = []
306
+ for got in answers:
307
+ score = Score(got.confidence or 0.0, "probability", self.profile.source or "unmeasured")
308
+ if not got.text:
309
+ outs.append(Output(Unknown("not_in_passage", f"no span answers this (p={got.confidence:.2f})"), score))
310
+ elif (got.confidence or 0.0) < self._threshold:
311
+ outs.append(Output(Unknown("below_threshold", f"p={got.confidence:.3f} < {self._threshold:.3f}", (got,)), score))
312
+ else:
313
+ outs.append(Output(got, score))
314
+ return outs
315
+
316
+ @torch.inference_mode()
317
+ def answer(self, items: Sequence[QuestionOverPassages]) -> list[Answer]:
318
+ out: list[Answer] = []
319
+ for i in range(0, len(items), self.batch_size):
320
+ out.extend(self._answer_batch(list(items[i : i + self.batch_size])))
321
+ return out
322
+
323
+ @torch.inference_mode()
324
+ def _answer_batch(self, items: list[QuestionOverPassages]) -> list[Answer]:
325
+ contexts = [" ".join(s for _, s in it.passages) for it in items]
326
+ enc = self.tokenizer([it.question for it in items], contexts, return_tensors="pt", padding=True,
327
+ truncation="only_second", max_length=self.max_length, return_offsets_mapping=True)
328
+ offsets = enc.pop("offset_mapping")
329
+ seq_ids = [enc.sequence_ids(i) for i in range(len(items))]
330
+ enc = {k: v.to(self.device) for k, v in enc.items()}
331
+ got = self.model(**{k: v for k, v in enc.items() if k in ("input_ids", "attention_mask", "token_type_ids")})
332
+ answerable = got["answerable"].softmax(-1)[:, 1].cpu()
333
+ starts, ends = got["start"].cpu(), got["end"].cpu()
334
+
335
+ results = []
336
+ for row, item in enumerate(items):
337
+ allowed = [i for i, s in enumerate(seq_ids[row]) if s == 1]
338
+ if not allowed:
339
+ results.append(Answer("", (), float(answerable[row])))
340
+ continue
341
+ s_log, e_log = starts[row][allowed], ends[row][allowed]
342
+ s_p, e_p = s_log.softmax(-1), e_log.softmax(-1)
343
+ best, span = 0.0, None
344
+ top_s = torch.topk(s_p, k=min(20, len(allowed))).indices.tolist()
345
+ top_e = torch.topk(e_p, k=min(20, len(allowed))).indices.tolist()
346
+ for a in top_s:
347
+ for b in top_e:
348
+ if b < a or b - a > 30:
349
+ continue
350
+ p = float(s_p[a] * e_p[b])
351
+ if p > best:
352
+ best, span = p, (allowed[a], allowed[b])
353
+ if span is None:
354
+ results.append(Answer("", (), float(answerable[row])))
355
+ continue
356
+ a, b = offsets[row][span[0]][0].item(), offsets[row][span[1]][1].item()
357
+ text = contexts[row][a:b].strip()
358
+ confidence = float(answerable[row]) * best ** 0.5
359
+ evidence = tuple(p for p in item.passages if p[1] and text and p[1].find(text) >= 0)
360
+ results.append(Answer(text if float(answerable[row]) >= 0.5 else "", evidence, confidence))
361
+ return results