openthai-systemone 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ from .types import Choice, Noul, Score, ChoiceAnswer, NoulAnswer, ScoreAnswer, SystemOneRequest, SystemOneResponse
2
+ from .configuration import OpenThaiSystemOneConfig
3
+ from .modeling import OpenThaiSystemOneForDecision
4
+ from .formatting import Formatter, SPECIAL_TOKENS, N_SLOTS, ABSTAIN_SLOT
5
+ from .client import SystemOneClient
6
+
7
+ from transformers import AutoConfig, AutoModel
8
+
9
+ try: # make AutoConfig/AutoModel aware of the architecture when the package is installed
10
+ AutoConfig.register("openthai_systemone", OpenThaiSystemOneConfig)
11
+ AutoModel.register(OpenThaiSystemOneConfig, OpenThaiSystemOneForDecision)
12
+ except ValueError: # already registered (e.g. remote code + package both imported)
13
+ pass
14
+
15
+ __version__ = "0.1.0"
16
+ __all__ = [
17
+ "Choice", "Noul", "Score", "ChoiceAnswer", "NoulAnswer", "ScoreAnswer",
18
+ "SystemOneRequest", "SystemOneResponse",
19
+ "OpenThaiSystemOneConfig", "OpenThaiSystemOneForDecision",
20
+ "Formatter", "SPECIAL_TOKENS", "N_SLOTS", "ABSTAIN_SLOT", "SystemOneClient",
21
+ ]
@@ -0,0 +1,109 @@
1
+ """Local inference: `client.system_one(state, questions)` -> answers, mirroring the TypeSafe SDK."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, List, Optional, Sequence, Union
5
+
6
+ import torch
7
+ from transformers import AutoTokenizer
8
+
9
+ from .formatting import Encoded, Formatter, collate
10
+ from .modeling import OpenThaiSystemOneForDecision, QTYPE_INDEX, confidence_from_probs, use_reference_kernels
11
+ from .types import (
12
+ Answer,
13
+ Choice,
14
+ ChoiceAnswer,
15
+ Noul,
16
+ NoulAnswer,
17
+ Question,
18
+ Score,
19
+ ScoreAnswer,
20
+ SystemOneResponse,
21
+ Usage,
22
+ parse_question,
23
+ )
24
+
25
+
26
+ class SystemOneClient:
27
+ def __init__(
28
+ self,
29
+ model_path: str,
30
+ *,
31
+ device: Optional[str] = None,
32
+ dtype: Optional[torch.dtype] = None,
33
+ max_total_tokens: int = 65536,
34
+ max_state_tokens: int = 32768,
35
+ model_name: str = "openthai-systemone",
36
+ ):
37
+ if device is None:
38
+ device = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")
39
+ if dtype is None:
40
+ dtype = torch.bfloat16 if device == "cuda" else torch.float32
41
+ self.device = device
42
+ if device != "cuda":
43
+ use_reference_kernels()
44
+ self.tok = AutoTokenizer.from_pretrained(model_path)
45
+ self.model = OpenThaiSystemOneForDecision.from_pretrained(model_path, dtype=dtype).to(device).eval()
46
+ self.fmt = Formatter(self.tok, max_total_tokens=max_total_tokens, max_state_tokens=max_state_tokens)
47
+ self.model_name = model_name
48
+ pad = self.tok.pad_token_id
49
+ self.pad_id = pad if pad is not None else (self.tok.eos_token_id or 0)
50
+
51
+ # ------------------------------------------------------------------ public API
52
+ @torch.no_grad()
53
+ def system_one(
54
+ self,
55
+ state: Union[str, Dict[str, Any], List[Any]],
56
+ questions: Dict[str, Union[Question, Dict[str, Any]]],
57
+ ) -> SystemOneResponse:
58
+ return self.system_one_batch([(state, questions)])[0]
59
+
60
+ @torch.no_grad()
61
+ def system_one_batch(
62
+ self,
63
+ items: Sequence[tuple],
64
+ ) -> List[SystemOneResponse]:
65
+ parsed = [(s, {k: parse_question(v) for k, v in qs.items()}) for s, qs in items]
66
+ encs = [self.fmt.encode(s, qs) for s, qs in parsed]
67
+ batch = collate(encs, self.pad_id)
68
+ qtypes = torch.full_like(batch["option_counts"], -1)
69
+ for b, e in enumerate(encs):
70
+ for qi, spec in enumerate(e.specs):
71
+ qtypes[b, qi] = QTYPE_INDEX[spec.qtype]
72
+ batch = {k: v.to(self.device) for k, v in batch.items()}
73
+ out = self.model(
74
+ input_ids=batch["input_ids"],
75
+ attention_mask=batch["attention_mask"],
76
+ answer_positions=batch["answer_positions"],
77
+ option_counts=batch["option_counts"],
78
+ qtypes=qtypes.to(self.device),
79
+ include_abstain=True,
80
+ )
81
+ probs = out.probs.float().cpu()
82
+ return [self._decode(e, probs[b]) for b, e in enumerate(encs)]
83
+
84
+ # ------------------------------------------------------------------ decoding
85
+ def _decode(self, enc: Encoded, probs: torch.Tensor) -> SystemOneResponse:
86
+ answers: Dict[str, Answer] = {}
87
+ for qi, spec in enumerate(enc.specs):
88
+ p = probs[qi]
89
+ k = len(spec.option_names)
90
+ abstain = float(p[self.model.config.abstain_slot])
91
+ pk = p[:k]
92
+ pk = pk / pk.sum().clamp(min=1e-12) # renormalise over the real options
93
+ conf = confidence_from_probs(pk, k)
94
+ if spec.qtype == "noul":
95
+ answers[spec.qid] = NoulAnswer(noul=float(pk[1]))
96
+ elif spec.qtype == "choice":
97
+ prob_map = {spec.option_names[i]: float(pk[i]) for i in range(k)}
98
+ best = max(prob_map, key=prob_map.get)
99
+ answers[spec.qid] = ChoiceAnswer(choice=best, probabilities=prob_map, confidence=conf, abstain=abstain)
100
+ else: # score
101
+ levels = torch.arange(k, dtype=torch.float32)
102
+ score = float((pk * levels).sum())
103
+ answers[spec.qid] = ScoreAnswer(
104
+ score=score,
105
+ legend={i: spec.option_descs[i] or str(i) for i in range(k)},
106
+ probabilities={str(i): float(pk[i]) for i in range(k)},
107
+ confidence=conf,
108
+ )
109
+ return SystemOneResponse(model=self.model_name, answers=answers, usage=Usage(input_tokens=enc.n_tokens))
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from transformers import AutoConfig, PretrainedConfig
6
+
7
+
8
+ class OpenThaiSystemOneConfig(PretrainedConfig):
9
+ """Config = a text-tower config (Qwen3.5 text by default) + slot-head settings."""
10
+
11
+ model_type = "openthai_systemone"
12
+ sub_configs = {"text_config": AutoConfig}
13
+
14
+ def __init__(
15
+ self,
16
+ text_config: Optional[Dict[str, Any] | PretrainedConfig] = None,
17
+ n_slots: int = 256,
18
+ abstain_slot: int = 255,
19
+ answer_token_id: Optional[int] = None,
20
+ head_bias: bool = True,
21
+ n_temperatures: int = 3, # per question type: choice / score / noul
22
+ **kwargs,
23
+ ):
24
+ if isinstance(text_config, dict):
25
+ text_config = AutoConfig.for_model(**text_config) if "model_type" in text_config else AutoConfig.for_model("qwen3_5_text", **text_config)
26
+ self.text_config = text_config
27
+ self.n_slots = n_slots
28
+ self.abstain_slot = abstain_slot
29
+ self.answer_token_id = answer_token_id
30
+ self.head_bias = head_bias
31
+ self.n_temperatures = n_temperatures
32
+ super().__init__(**kwargs)
33
+
34
+ @property
35
+ def hidden_size(self) -> int:
36
+ return self.text_config.hidden_size
37
+
38
+ @property
39
+ def vocab_size(self) -> int:
40
+ return self.text_config.vocab_size
File without changes
@@ -0,0 +1,95 @@
1
+ """Unified decision record format shared by all data builders and the SFT trainer.
2
+
3
+ {
4
+ "id": str, "source": str, "lang": "th"|"en"|"mixed"|..., "split": "train"|"eval",
5
+ "state": str | dict | list,
6
+ "questions": {qid: {"type": "choice"|"score"|"noul", "instructions": str, "criteria": ...}},
7
+ "labels": {qid: option-name | level-index | bool}
8
+ }
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import random
14
+ from dataclasses import dataclass, field
15
+ from typing import Any, Dict, Iterable, Iterator, List, Optional, Union
16
+
17
+ # Instruction paraphrases: the model must not overfit to one phrasing per dataset.
18
+ TH_CHOICE_TEMPLATES = [
19
+ "ข้อความนี้จัดอยู่ในหมวด{what}ใด",
20
+ "เลือก{what}ที่ตรงกับข้อความมากที่สุด",
21
+ "จาก state ด้านบน {what}ที่ถูกต้องคือข้อใด",
22
+ "ระบุ{what}ของข้อความนี้",
23
+ ]
24
+ EN_CHOICE_TEMPLATES = [
25
+ "Which {what} best matches this text?",
26
+ "Classify the {what} of the state above.",
27
+ "Select the correct {what}.",
28
+ "What is the {what}?",
29
+ ]
30
+ TH_NOUL_TEMPLATES = ["ข้อความนี้{what}หรือไม่", "จริงหรือไม่ว่า ข้อความนี้{what}", "state นี้{what}ใช่หรือไม่"]
31
+ EN_NOUL_TEMPLATES = ["Is this text {what}?", "Does the state {what}?", "True or false: the text {what}."]
32
+
33
+
34
+ @dataclass
35
+ class Record:
36
+ id: str
37
+ source: str
38
+ lang: str
39
+ state: Union[str, Dict[str, Any], List[Any]]
40
+ questions: Dict[str, Dict[str, Any]]
41
+ labels: Dict[str, Union[str, int, bool]]
42
+ split: str = "train"
43
+ meta: Dict[str, Any] = field(default_factory=dict)
44
+
45
+ def to_json(self) -> str:
46
+ return json.dumps(self.__dict__, ensure_ascii=False)
47
+
48
+ @staticmethod
49
+ def from_json(line: str) -> "Record":
50
+ d = json.loads(line)
51
+ known = {"id", "source", "lang", "state", "questions", "labels", "split", "meta"}
52
+ meta = dict(d.get("meta", {}))
53
+ meta.update({k: v for k, v in d.items() if k not in known})
54
+ return Record(**{k: d[k] for k in ("id", "source", "lang", "state", "questions", "labels")},
55
+ split=d.get("split", "train"), meta=meta)
56
+
57
+
58
+ def choice_q(instructions: str, options: Iterable[str], descs: Optional[Dict[str, Optional[str]]] = None) -> Dict[str, Any]:
59
+ descs = descs or {}
60
+ return {"type": "choice", "instructions": instructions, "criteria": {o: descs.get(o) for o in options}}
61
+
62
+
63
+ def score_q(instructions: str, levels: List[str]) -> Dict[str, Any]:
64
+ return {"type": "score", "instructions": instructions, "criteria": list(levels)}
65
+
66
+
67
+ def noul_q(instructions: str, true_desc: Optional[str] = None, false_desc: Optional[str] = None) -> Dict[str, Any]:
68
+ q: Dict[str, Any] = {"type": "noul", "instructions": instructions}
69
+ if true_desc or false_desc:
70
+ q["criteria"] = {"true": true_desc, "false": false_desc}
71
+ return q
72
+
73
+
74
+ def pick_template(rng: random.Random, lang: str, kind: str, what: str) -> str:
75
+ if kind == "choice":
76
+ t = TH_CHOICE_TEMPLATES if lang == "th" else EN_CHOICE_TEMPLATES
77
+ else:
78
+ t = TH_NOUL_TEMPLATES if lang == "th" else EN_NOUL_TEMPLATES
79
+ return rng.choice(t).format(what=what)
80
+
81
+
82
+ def write_jsonl(path, records: Iterable[Record]) -> int:
83
+ n = 0
84
+ with open(path, "w", encoding="utf-8") as f:
85
+ for r in records:
86
+ f.write(r.to_json() + "\n")
87
+ n += 1
88
+ return n
89
+
90
+
91
+ def read_jsonl(path) -> Iterator[Record]:
92
+ with open(path, encoding="utf-8") as f:
93
+ for line in f:
94
+ if line.strip():
95
+ yield Record.from_json(line)
@@ -0,0 +1,291 @@
1
+ """Turn (state, questions) into token ids + slot bookkeeping.
2
+
3
+ Layout (one sequence, causal):
4
+
5
+ <|ts_state|> {state text}
6
+ <|ts_q|><|ts_choice|> {instructions}
7
+ <|ts_opt_0|> {option name}: {description}
8
+ <|ts_opt_1|> {option name}
9
+ ...
10
+ <|ts_answer|> <- hidden state here -> SlotHead (256 logits)
11
+ <|ts_q|><|ts_noul|> {instructions}
12
+ <|ts_opt_0|> no
13
+ <|ts_opt_1|> yes
14
+ <|ts_answer|>
15
+ ...
16
+
17
+ Slot i (0..254) means "the option introduced by <|ts_opt_i|>"; slot 255 = abstain.
18
+ All answers for all questions are read out from one forward pass.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import random
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
26
+
27
+ from .types import Choice, Noul, Score, Question, MAX_OPTIONS
28
+
29
+ N_SLOTS = 256
30
+ ABSTAIN_SLOT = 255
31
+
32
+ TOK_STATE = "<|ts_state|>"
33
+ TOK_Q = "<|ts_q|>"
34
+ TOK_CHOICE = "<|ts_choice|>"
35
+ TOK_SCORE = "<|ts_score|>"
36
+ TOK_NOUL = "<|ts_noul|>"
37
+ TOK_ANSWER = "<|ts_answer|>"
38
+ TOK_OPT = [f"<|ts_opt_{i}|>" for i in range(N_SLOTS)]
39
+
40
+ SPECIAL_TOKENS: List[str] = [TOK_STATE, TOK_Q, TOK_CHOICE, TOK_SCORE, TOK_NOUL, TOK_ANSWER] + TOK_OPT
41
+
42
+ NOUL_OPTIONS = ("no", "yes") # slot 0 = no, slot 1 = yes -> noul = p(slot 1)
43
+
44
+ DEFAULT_MAX_TOTAL_TOKENS = 65536
45
+ DEFAULT_MAX_STATE_TOKENS = 32768
46
+
47
+
48
+ def add_special_tokens(tokenizer) -> int:
49
+ """Register the TypeSafe control tokens. Returns number of tokens added."""
50
+ existing = set(tokenizer.get_vocab())
51
+ new = [t for t in SPECIAL_TOKENS if t not in existing]
52
+ if not new:
53
+ return 0
54
+ return tokenizer.add_tokens(new, special_tokens=True)
55
+
56
+
57
+ def sanitize(text: str) -> str:
58
+ """Stop user content from smuggling control tokens into the sequence."""
59
+ return text.replace("<|ts_", "<​|ts_") if "<|ts_" in text else text
60
+
61
+
62
+ def state_to_text(state: Union[str, Dict[str, Any], List[Any]], *, indent: Optional[int] = None) -> str:
63
+ if isinstance(state, str):
64
+ return state
65
+ return json.dumps(state, ensure_ascii=False, indent=indent)
66
+
67
+
68
+ @dataclass
69
+ class QuestionSpec:
70
+ """A question flattened to option strings + slot bookkeeping."""
71
+
72
+ qid: str
73
+ qtype: str # choice | score | noul
74
+ instructions: str
75
+ option_names: List[str] # in slot order (after any permutation)
76
+ option_descs: List[Optional[str]]
77
+ perm: List[int] # perm[slot] = original index of the option at that slot
78
+ label_slot: Optional[int] = None # training only
79
+
80
+
81
+ def question_to_spec(
82
+ qid: str,
83
+ q: Question,
84
+ *,
85
+ label: Optional[Union[str, int, bool]] = None,
86
+ shuffle: bool = False,
87
+ rng: Optional[random.Random] = None,
88
+ drop_label: bool = False,
89
+ ) -> QuestionSpec:
90
+ """Flatten a typed question.
91
+
92
+ label: for training. Choice -> option name; Score -> level index (int); Noul -> bool.
93
+ shuffle: permute option order (Choice only; Score/Noul order is semantic).
94
+ drop_label: remove the correct option from a Choice so the target becomes ABSTAIN_SLOT.
95
+ """
96
+ if isinstance(q, Choice):
97
+ names = list(q.criteria.keys())
98
+ descs = [q.criteria[n] for n in names]
99
+ idx = list(range(len(names)))
100
+ label_idx = None
101
+ if label is not None:
102
+ if label not in q.criteria:
103
+ raise ValueError(f"label {label!r} is not one of the options")
104
+ label_idx = names.index(str(label))
105
+ if drop_label and label_idx is not None:
106
+ if len(idx) < 2:
107
+ raise ValueError("cannot drop the only option")
108
+ idx.remove(label_idx)
109
+ label_idx = None
110
+ if shuffle:
111
+ (rng or random).shuffle(idx)
112
+ names_p = [names[i] for i in idx]
113
+ descs_p = [descs[i] for i in idx]
114
+ if label is None:
115
+ slot = None
116
+ elif label_idx is None:
117
+ slot = ABSTAIN_SLOT
118
+ else:
119
+ slot = idx.index(label_idx)
120
+ return QuestionSpec(qid, "choice", q.instructions, names_p, descs_p, idx, slot)
121
+
122
+ if isinstance(q, Score):
123
+ names = [str(i) for i in range(len(q.criteria))]
124
+ descs = list(q.criteria)
125
+ slot = int(label) if label is not None else None
126
+ if slot is not None and not (0 <= slot < len(descs)):
127
+ raise ValueError("score label out of range")
128
+ return QuestionSpec(qid, "score", q.instructions, names, descs, list(range(len(names))), slot)
129
+
130
+ if isinstance(q, Noul):
131
+ c = q.criteria or {}
132
+ descs = [c.get("false"), c.get("true")]
133
+ slot = None if label is None else int(bool(label))
134
+ return QuestionSpec(qid, "noul", q.instructions, list(NOUL_OPTIONS), descs, [0, 1], slot)
135
+
136
+ raise TypeError(type(q))
137
+
138
+
139
+ def spec_to_text(spec: QuestionSpec) -> str:
140
+ head = {"choice": TOK_CHOICE, "score": TOK_SCORE, "noul": TOK_NOUL}[spec.qtype]
141
+ lines = [f"{TOK_Q}{head} {sanitize(spec.instructions).strip()}"]
142
+ for i, (name, desc) in enumerate(zip(spec.option_names, spec.option_descs)):
143
+ name = sanitize(str(name)).strip()
144
+ if desc:
145
+ lines.append(f"{TOK_OPT[i]} {name}: {sanitize(str(desc)).strip()}")
146
+ else:
147
+ lines.append(f"{TOK_OPT[i]} {name}")
148
+ lines.append(TOK_ANSWER)
149
+ return "\n".join(lines) + "\n"
150
+
151
+
152
+ @dataclass
153
+ class Encoded:
154
+ input_ids: List[int]
155
+ answer_positions: List[int] # index of each <|ts_answer|> token, question order
156
+ option_counts: List[int] # k per question (valid slots 0..k-1)
157
+ specs: List[QuestionSpec]
158
+ labels: List[int] = field(default_factory=list) # -100 if unknown
159
+ truncated_state: bool = False
160
+
161
+ @property
162
+ def n_tokens(self) -> int:
163
+ return len(self.input_ids)
164
+
165
+
166
+ class Formatter:
167
+ """Tokenizer-aware encoder shared by training and inference."""
168
+
169
+ def __init__(
170
+ self,
171
+ tokenizer,
172
+ *,
173
+ max_total_tokens: int = DEFAULT_MAX_TOTAL_TOKENS,
174
+ max_state_tokens: int = DEFAULT_MAX_STATE_TOKENS,
175
+ ):
176
+ self.tok = tokenizer
177
+ add_special_tokens(self.tok)
178
+ self.max_total_tokens = max_total_tokens
179
+ self.max_state_tokens = max_state_tokens
180
+ self.answer_id = self.tok.convert_tokens_to_ids(TOK_ANSWER)
181
+ self.state_id = self.tok.convert_tokens_to_ids(TOK_STATE)
182
+ self.opt_ids = self.tok.convert_tokens_to_ids(TOK_OPT)
183
+ assert self.answer_id is not None and self.answer_id != self.tok.unk_token_id
184
+
185
+ def _ids(self, text: str) -> List[int]:
186
+ return self.tok(text, add_special_tokens=False)["input_ids"]
187
+
188
+ def encode(
189
+ self,
190
+ state: Union[str, Dict[str, Any], List[Any]],
191
+ questions: Dict[str, Question],
192
+ *,
193
+ labels: Optional[Dict[str, Union[str, int, bool]]] = None,
194
+ shuffle_options: bool = False,
195
+ shuffle_questions: bool = False,
196
+ drop_label_for: Optional[Sequence[str]] = None,
197
+ rng: Optional[random.Random] = None,
198
+ state_indent: Optional[int] = None,
199
+ ) -> Encoded:
200
+ rng = rng or random.Random()
201
+ labels = labels or {}
202
+ drop = set(drop_label_for or [])
203
+ qids = list(questions.keys())
204
+ if shuffle_questions:
205
+ rng.shuffle(qids)
206
+
207
+ specs = [
208
+ question_to_spec(
209
+ qid,
210
+ questions[qid],
211
+ label=labels.get(qid),
212
+ shuffle=shuffle_options,
213
+ rng=rng,
214
+ drop_label=qid in drop,
215
+ )
216
+ for qid in qids
217
+ ]
218
+ q_texts = [spec_to_text(s) for s in specs]
219
+ q_ids = [self._ids(t) for t in q_texts]
220
+ q_total = sum(len(x) for x in q_ids)
221
+
222
+ state_text = sanitize(state_to_text(state, indent=state_indent))
223
+ state_ids = self._ids(TOK_STATE + " " + state_text.strip() + "\n")
224
+ budget = min(self.max_state_tokens, self.max_total_tokens - q_total)
225
+ truncated = False
226
+ if len(state_ids) > budget:
227
+ # keep the head (state token) and the tail of the state; the end is usually the most recent info
228
+ keep_tail = max(budget - 1, 0)
229
+ state_ids = state_ids[:1] + state_ids[len(state_ids) - keep_tail :]
230
+ truncated = True
231
+
232
+ ids: List[int] = list(state_ids)
233
+ answer_positions: List[int] = []
234
+ for qi in q_ids:
235
+ ids.extend(qi)
236
+ # the answer token is the last non-newline token of each question block
237
+ pos = len(ids) - 1
238
+ while ids[pos] != self.answer_id:
239
+ pos -= 1
240
+ answer_positions.append(pos)
241
+
242
+ return Encoded(
243
+ input_ids=ids,
244
+ answer_positions=answer_positions,
245
+ option_counts=[len(s.option_names) for s in specs],
246
+ specs=specs,
247
+ labels=[(-100 if s.label_slot is None else s.label_slot) for s in specs],
248
+ truncated_state=truncated,
249
+ )
250
+
251
+
252
+ def slot_mask(option_counts: Sequence[int], *, include_abstain: bool = True, n_slots: int = N_SLOTS):
253
+ """Boolean mask (Q, n_slots): True where a slot is valid for that question."""
254
+ import torch
255
+
256
+ k = torch.as_tensor(list(option_counts), dtype=torch.long)
257
+ ar = torch.arange(n_slots)
258
+ mask = ar[None, :] < k[:, None]
259
+ if include_abstain:
260
+ mask[:, ABSTAIN_SLOT] = True
261
+ return mask
262
+
263
+
264
+ def collate(encoded: Sequence[Encoded], pad_id: int, *, max_questions: Optional[int] = None):
265
+ """Right-pad a batch. Returns dict of tensors for OpenThaiSystemOneForDecision.forward."""
266
+ import torch
267
+
268
+ B = len(encoded)
269
+ T = max(e.n_tokens for e in encoded)
270
+ Q = max_questions or max(len(e.answer_positions) for e in encoded)
271
+ input_ids = torch.full((B, T), pad_id, dtype=torch.long)
272
+ attention_mask = torch.zeros((B, T), dtype=torch.long)
273
+ answer_positions = torch.zeros((B, Q), dtype=torch.long)
274
+ option_counts = torch.zeros((B, Q), dtype=torch.long)
275
+ labels = torch.full((B, Q), -100, dtype=torch.long)
276
+ for b, e in enumerate(encoded):
277
+ n = e.n_tokens
278
+ input_ids[b, :n] = torch.tensor(e.input_ids)
279
+ attention_mask[b, :n] = 1
280
+ q = len(e.answer_positions)
281
+ answer_positions[b, :q] = torch.tensor(e.answer_positions)
282
+ option_counts[b, :q] = torch.tensor(e.option_counts)
283
+ if e.labels:
284
+ labels[b, :q] = torch.tensor(e.labels)
285
+ return {
286
+ "input_ids": input_ids,
287
+ "attention_mask": attention_mask,
288
+ "answer_positions": answer_positions,
289
+ "option_counts": option_counts,
290
+ "labels": labels,
291
+ }