"""The prompt and the readout. A System One decision is a single forward pass that stops at the answer slot: everything the model sees is built here, and the answer is a softmax over its options' tokens. """ from __future__ import annotations import json import math import re from dataclasses import dataclass from functools import cached_property, lru_cache from typing import Any, Mapping, Sequence import numpy as np from .media import SPOKEN, Media, as_media # The system turn: none by default. SYSTEMS: dict[str, str | None] = {"none": None, "d1": "You are d1, a decision model trained by Liquid AI."} DEFAULT_SYSTEM = "none" IM_START = "<|im_start|>" IM_END = "<|im_end|>" # --------------------------------------------------------------------------- # # question types # --------------------------------------------------------------------------- # @dataclass(frozen=True) class Choice: instructions: str | Media # a spoken question's clip (`media`) criteria: Mapping[str, str | None] type: str = "choice" multiple: bool = False # each option its own yes or no @dataclass(frozen=True) class Noul: instructions: str | Media criteria: Mapping[str, Any] | None = None type: str = "noul" @dataclass(frozen=True) class Score: instructions: str | Media criteria: Sequence[str] type: str = "score" @dataclass(frozen=True) class Read: """A span read from the state: text, or a number or a date written there; `unit`, a number's.""" instructions: str | Media type: str = "text" unit: str | None = None @dataclass(frozen=True) class Estimate: """A number estimated on a grid over the declared range: `step` apart, at most 128 values (a finer step is read on 128).""" instructions: str | Media low: float high: float step: float | None = None unit: str | None = None type: str = "number" @cached_property def grid(self): from .grid import Grid return Grid(self.low, self.high, 128 if self.step is None else min(128, round((self.high - self.low) / self.step) + 1)) @dataclass(frozen=True) class Box: """Where something is in the picture: its four edges, or nothing.""" instructions: str | Media type: str = "box" @dataclass(frozen=True) class Boxes: """Every instance of something in the picture, in reading order, none if absent: `max` blocks read, else `box.INSTANCES`. A readout choice: the question's text never says it.""" instructions: str | Media type: str = "boxes" max: int | None = None @property def blocks(self) -> int: from .box import INSTANCES return INSTANCES if self.max is None else self.max @dataclass(frozen=True) class Position: name: str @dataclass(frozen=True) class Number: """A slot over a grid's values as the model writes them: value i times `scale`, an integer, is one token of the tokenizer's own (`number_ids`). A checkpoint that records `d1_numbers` reads its grids so.""" instructions: str grid: Any scale: int = 1 type: str = "grid" Question = Choice | Noul | Score | Read | Estimate | Box | Boxes TYPED = ("text", "number", "date", "box", "boxes") # the typed answers' kinds; a choice is one when `multiple` def typed(q: Question) -> bool: """A typed answer, read at its own slots or state positions and answered in its own shape (api.py): a read, an estimate, a box, a multiple choice.""" return q.type in TYPED or isinstance(q, Choice) and q.multiple def cardinality(q: Question) -> int: if isinstance(q, (Estimate, Number)): return q.grid.size return 2 if isinstance(q, Noul) else len(q.criteria) # --------------------------------------------------------------------------- # # verbalizer # --------------------------------------------------------------------------- # PROMPTS = (1, 2) def version(tokenizer) -> int: """The prompt version the checkpoint behind `tokenizer` trained with (`PROMPTS`).""" return getattr(tokenizer, "d1_prompt", 1) def most_blocks(tokenizer) -> int: """The most blocks a `boxes` question's `max` asks of this checkpoint: the 16 prompt 1 trained, else `box.MAX_INSTANCES`.""" from .box import INSTANCES, MAX_INSTANCES return INSTANCES if version(tokenizer) == 1 else MAX_INSTANCES LETTERS = tuple(chr(ord("A") + i) for i in range(26)) def option_codes(labels: Sequence[str], scheme: int = 1) -> list[str]: """Native letters when the labels already are letters, else A..Z, else the `scheme`'s codes: 00.. (1), A..Z then AA, AB.. (2); one rule for every cardinality.""" labs = [str(x).strip() for x in labels] if labs and all(len(k) == 1 and k.isalpha() for k in labs): return labs if len(labs) <= 26: return list(LETTERS[: len(labs)]) if scheme == 2: return (list(LETTERS) + [a + b for a in LETTERS for b in LETTERS])[: len(labs)] return [f"{i:02d}" for i in range(len(labs))] _FALLBACK_POOL = ( [chr(c) for c in range(ord("A"), ord("Z") + 1)] + [f"{i:02d}" for i in range(100)] + [chr(c) for c in range(ord("a"), ord("z") + 1)] + [f"#{i}" for i in range(200)] # Where a tokenizer splits digits, "00".."99" and "#i" are two tokens, and two capital letters are # often one. Last in the pool, so a tokenizer with digit pairs never reaches it. + [chr(a) + chr(b) for a in range(ord("A"), ord("Z") + 1) for b in range(ord("A"), ord("Z") + 1)] ) @lru_cache(maxsize=4096) def _aliases_cached(tokenizer_key: int, codes: tuple[str, ...]) -> tuple[tuple[str, int], ...]: tokenizer = _TOKENIZERS[tokenizer_key] used: set[int] = set() out: list[tuple[str, int]] = [] def take(raw: str) -> bool: enc = tokenizer.encode(raw, add_special_tokens=False) if len(enc) != 1 or enc[0] in used: return False out.append((raw, enc[0])) used.add(enc[0]) return True for code in codes: if take(code): continue if not any(take(raw) for raw in _FALLBACK_POOL): raise RuntimeError(f"no single-token alias left for {len(codes)} options") return tuple(out) _TOKENIZERS: dict[int, object] = {} def aliases(tokenizer, labels: Sequence[str]) -> list[tuple[str, int]]: """Every label's distinct single-token code: [(code, token_id)]. Memoised on the codes, which are positional unless the labels are letters, so every ordering of one option list shares an entry.""" _TOKENIZERS.setdefault(id(tokenizer), tokenizer) codes = tuple(option_codes(labels, getattr(tokenizer, "d1_codes", 1))) return list(_aliases_cached(id(tokenizer), codes)) @lru_cache(maxsize=8192) def _ids_cached(tokenizer_key: int, texts: tuple[str, ...]) -> tuple[int, ...]: tokenizer = _TOKENIZERS[tokenizer_key] out, seen = [], set() for t in texts: enc = tokenizer.encode(t, add_special_tokens=False) if len(enc) == 1 and enc[0] not in seen: out.append(enc[0]) seen.add(enc[0]) return tuple(out) def _ids(tokenizer, texts: Sequence[str]) -> list[int]: _TOKENIZERS.setdefault(id(tokenizer), tokenizer) return list(_ids_cached(id(tokenizer), tuple(texts))) QUESTION_TYPES = ("noul", "choice", "score", "text", "number", "date", "box", "boxes") CHOICE = '"criteria" names the options: {"billing": "Charges and refunds", "shipping": "Delivery"}' SCORE = '"criteria" lists 2 to 10 levels, lowest first: ["Can wait", "Today", "Now"]' def check_question(q: Any, tokenizer=None) -> None: """A caller's question in the API's JSON, refused with what to change; a class passes. `tokenizer` carries the checkpoint's prompt version, which bounds a `boxes` question's `max` (`most_blocks`; prompt 1 without it).""" if isinstance(q, (Choice, Noul, Score, Read, Estimate, Box, Boxes)): return if not isinstance(q, Mapping): raise ValueError(f'a question is a dict such as {{"type": "noul", "instructions": "Is it urgent?"}}, not ' f"{type(q).__name__}") kind, said, criteria = q.get("type"), q.get("instructions"), q.get("criteria") if kind not in QUESTION_TYPES: raise ValueError(f'"type" is one of {", ".join(QUESTION_TYPES)}, not {kind!r}') if not (isinstance(said, str) and said.strip() or isinstance(said, Mapping)): raise ValueError('"instructions" is the question as text (or a spoken one, {"type": "audio", "path": ...})') if kind == "choice" and not (isinstance(criteria, Mapping) and criteria and all(isinstance(k, str) and (v is None or isinstance(v, str)) for k, v in criteria.items())): raise ValueError(f"a choice's {CHOICE}" + ("; a list of levels is a score" if isinstance(criteria, list) else "")) if kind == "score" and not (isinstance(criteria, (list, tuple)) and 2 <= len(criteria) <= 10 and all(isinstance(c, str) for c in criteria)): raise ValueError(f"a score's {SCORE}") if kind == "noul" and criteria is not None and not isinstance(criteria, Mapping): raise ValueError('a noul\'s "criteria", when given, is {"true": "what yes means", "false": "what no means"}') if q.get("multiple") and kind != "choice": raise ValueError('"multiple" belongs to a choice: {"type": "choice", "multiple": true, "criteria": {...}}') if "max" in q and not (kind == "boxes" and isinstance(q["max"], int) and not isinstance(q["max"], bool) and 1 <= q["max"] <= most_blocks(tokenizer)): raise ValueError(f'"max" belongs to boxes, a whole number from 1 to {most_blocks(tokenizer)}: ' '{"type": "boxes", "instructions": "Every cup.", "max": 4}') if "step" in q and "range" not in q: raise ValueError('"step" needs a "range": {"type": "number", "range": [0, 10], "step": 1}') if "range" in q: bounds = q["range"] if kind != "number" or not (isinstance(bounds, (list, tuple)) and len(bounds) == 2 and all(isinstance(b, (int, float)) and not isinstance(b, bool) for b in bounds)): raise ValueError('an estimate is {"type": "number", "range": [low, high]}, with an optional "step"') def as_question(q: Mapping | Question) -> Question: """A question in the API's JSON (`type`, `instructions`, `criteria`; `multiple`, `range`, `step`, `unit` for the typed answers; `instructions` a media dict for a spoken question), as one of the classes above; a class passes through.""" if not isinstance(q, Mapping): return q if isinstance(q.get("instructions"), Mapping): q = {**q, "instructions": as_media(q["instructions"])} kind = q.get("type", "choice") if kind == "number" and "range" in q: low, high = q["range"] step = q.get("step") if not low < high or step is not None and not (step > 0 and math.isclose((high - low) / step, round((high - low) / step))): raise ValueError(f"an estimate's range runs low to high and its step, above 0, divides it: not " f"{[low, high]} in steps of {step}") return Estimate(q["instructions"], low, high, step, q.get("unit")) if kind in ("text", "number", "date"): return Read(q["instructions"], kind, q.get("unit")) if kind == "box": return Box(q["instructions"]) if kind == "boxes": return Boxes(q["instructions"], max=q.get("max")) if kind == "noul": return Noul(q["instructions"], q.get("criteria")) if kind == "score": return Score(q["instructions"], list(q["criteria"])) return Choice(q["instructions"], q["criteria"], multiple=bool(q.get("multiple", False))) def question_slots(q: Question, tokenizer=None) -> tuple[Question, ...]: """Each answer position's readout, in question order; a grid's through the tokenizer's numbers where the checkpoint reads them so (`grid_slot`).""" if isinstance(q, Read): return Position("start"), Position("end") if isinstance(q, Estimate): return (grid_slot(tokenizer, q.instructions, q.grid),) if isinstance(q, Box): from .box import EDGES, edge_grid grid, scale = edge_grid(tokenizer) return (Noul(q.instructions), *(grid_slot(tokenizer, q.instructions, grid, scale) for _ in EDGES)) if isinstance(q, Boxes): return question_slots(Box(q.instructions), tokenizer) * q.blocks if isinstance(q, Choice) and q.multiple: if not q.criteria: raise ValueError("choice with no options") return tuple(Noul(q.instructions) for _ in q.criteria) return (q,) YES_FORMS = ("yes", "Yes", "YES") NO_FORMS = ("no", "No", "NO") def readout_ids(tokenizer, q: Question) -> list[list[int]]: """Token ids to score, one group per option, max-pooled: the answer is a softmax over these and nothing else.""" if isinstance(q, Estimate): return readout_ids(tokenizer, question_slots(q, tokenizer)[0]) if isinstance(q, Number): return [list(group) for group in number_ids(tokenizer, q.grid, q.scale)] if isinstance(q, (Read, Position)): raise ValueError("a pointer reads state positions, not vocabulary codes") if isinstance(q, Choice) and q.multiple: raise ValueError("a multiple choice reads one yes/no distribution per option") if isinstance(q, Noul): yes, no = _ids(tokenizer, YES_FORMS), _ids(tokenizer, NO_FORMS) if not yes or not no: raise RuntimeError("tokenizer has no single-token yes/no") return [yes, no] if isinstance(q, Score): groups = [_ids(tokenizer, [str(i)]) for i in range(len(q.criteria))] if any(not g for g in groups): raise RuntimeError(f"score with {len(q.criteria)} levels needs single-token digits; the primitive is defined " "for 2 to 10") return groups groups = [] for code, tid in aliases(tokenizer, list(q.criteria.keys())): extra = _ids(tokenizer, [f" {code}"]) groups.append([tid] + [i for i in extra if i != tid]) if not groups: raise RuntimeError("choice with no options") return groups def readout(tokenizer, q: Question, logz, calibration=None) -> list[float]: """Option probabilities from the log-probabilities at the answer slot, `logz` (a vocabulary tensor, or a dict holding at least the option tokens). Each option scores its best form.""" if not hasattr(logz, "tolist"): return probabilities(q, [max(float(logz[i]) for i in g) for g in readout_ids(tokenizer, q)], calibration) tokens, forms, _ = readout_table(tokenizer, q) values = np.asarray(logz[list(tokens)].tolist()) # one device transfer return probabilities(q, values[forms].max(-1).tolist(), calibration) def readouts(tokenizer, qs: Sequence[Question], logz, calibrations: Sequence) -> list[list[float]]: """`readout` for each row of `logz` ([slots, vocabulary], a torch tensor) and its question, as one gather for the rows sharing a readout table (a box's edges) and one transfer for all.""" import torch tables = [readout_table(tokenizer, q) for q in qs] shared: dict[int, list[int]] = {} for j, table in enumerate(tables): shared.setdefault(id(table), []).append(j) picked = [logz[rows][:, torch.from_numpy(tables[rows[0]][2]).to(logz.device)] for rows in shared.values()] host = torch.cat([p.flatten() for p in picked]).cpu().numpy() if picked else None out, at = [None] * len(qs), 0 for rows, part in zip(shared.values(), picked): values = host[at:at + part.numel()].reshape(part.shape) at += part.numel() scores = values[:, tables[rows[0]][1]].max(-1).astype(np.float64) scores /= np.array([1.0 if calibrations[j] is None else calibrations[j].temperature(qs[j]) for j in rows])[:, None] exps = np.exp(scores - scores.max(-1, keepdims=True)) # the softmax of `probabilities`, a block at once for j, p in zip(rows, (exps / exps.sum(-1, keepdims=True)).tolist()): out[j] = p return out def probabilities(q: Question, scores: list[float], calibration=None) -> list[float]: """Each option's best score, through the calibration's temperature, as a softmax.""" if calibration is not None: scores = calibration.apply(q, scores) m = max(scores) exps = [math.exp(s - m) for s in scores] total = sum(exps) return [e / total for e in exps] def readout_table(tokenizer, q: Question) -> tuple[tuple[int, ...], np.ndarray, np.ndarray]: """`readout_ids` as one gather: every token once, each group's forms as positions among them (a short group repeats its first form, which leaves its best as it is), and the tokens as an array. Cached: a box edge scores 1,000 numbers.""" if isinstance(q, Number): _TOKENIZERS.setdefault(id(tokenizer), tokenizer) return _number_table(id(tokenizer), q.grid.low, q.grid.high, q.grid.size, q.scale) return _table(tuple(map(tuple, readout_ids(tokenizer, q)))) @lru_cache(maxsize=4096) def _table(groups: tuple[tuple[int, ...], ...]) -> tuple[tuple[int, ...], np.ndarray, np.ndarray]: tokens = tuple(dict.fromkeys(i for g in groups for i in g)) at = {t: k for k, t in enumerate(tokens)} width = max(map(len, groups)) return tokens, np.array([[at[i] for i in g] + [at[g[0]]] * (width - len(g)) for g in groups]), np.array(tokens) @lru_cache(maxsize=256) def _number_table(tokenizer_key: int, low: float, high: float, size: int, scale: int): from .grid import Grid return _table(tuple(map(tuple, readout_ids(_TOKENIZERS[tokenizer_key], Number("", Grid(low, high, size), scale))))) # --------------------------------------------------------------------------- # # state and question rendering # --------------------------------------------------------------------------- # # How a state is rendered: `json_only`, the default, writes every state as the object it is; `json` keeps # three shortcuts (`Message:`, `Passage:` / `Asked:`, a lone question's text); `sections` writes nested # states as labelled blocks. DEFAULT_STATE_STYLE = "json_only" def _is_scalar(v: Any) -> bool: return v is None or isinstance(v, (str, int, float, bool)) and "\n" not in str(v) def _sections(obj: Any, path: str, out: list[str]) -> None: """Flatten a nested state into labelled blocks, keeping the real newlines `json.dumps` would escape.""" head = f"[{path}]\n" if path else "" if isinstance(obj, dict): scalars = [(k, v) for k, v in obj.items() if _is_scalar(v)] rest = [(k, v) for k, v in obj.items() if not _is_scalar(v)] if scalars: body = "\n".join(f"{k}: {'' if v is None else v}" for k, v in scalars) out.append(f"{head}{body}") for k, v in rest: _sections(v, f"{path}.{k}" if path else str(k), out) return if isinstance(obj, (list, tuple)): if obj and all(_is_scalar(v) for v in obj): body = "\n".join(f"- {'' if v is None else v}" for v in obj) out.append(f"{head}{body}") return for i, v in enumerate(obj, start=1): _sections(v, f"{path} {i}/{len(obj)}" if path else f"{i}/{len(obj)}", out) return out.append(f"{head}{'' if obj is None else obj}") def render_state(state: Any) -> str: if isinstance(state, str): return state out: list[str] = [] _sections(state, "", out) return "\n\n".join(out) def state_text(state: Any, style: str = DEFAULT_STATE_STYLE) -> str: """The state as the prompt renders it, without the blank line after it: what a read's span offsets index.""" return state_block(state, style)[:-2] def state_block(state: Any, style: str = DEFAULT_STATE_STYLE) -> str: """The state as the block before `QUESTION:`. The `_only` styles (the default) write every state as the object it is; the others give three shapes a shortcut (`Message:`, `Passage:` / `Asked:`, a lone question's text).""" if isinstance(state, dict) and not style.endswith("_only"): keys = set(state.keys()) if keys == {"text"}: return f"Message: {state['text']}\n\n" if {"passage", "question"} <= keys and len(keys) == 2: return f"Passage: {state['passage']}\n\nAsked: {state['question']}\n\n" if keys == {"question"}: return f"{state['question']}\n\n" if style.startswith("json"): if isinstance(state, str): return f"{state}\n\n" return json.dumps(state, ensure_ascii=False, indent=2) + "\n\n" return f"{render_state(state)}\n\n" # A zero-width space after the first character of a special token's string: it reads the same and is no longer the # token. Without it a state holding `<|im_end|>` closed the user turn, and `` became an image token # with no picture behind it (the processor then refused the row). BREAK = "\u200b" @lru_cache(maxsize=64) def _specials_cached(tokenizer_key: int) -> re.Pattern | None: tok = _TOKENIZERS[tokenizer_key] tok = getattr(tok, "tokenizer", tok) # a processor's own names = set(getattr(tok, "all_special_tokens", ())) names |= {t.content for t in getattr(tok, "added_tokens_decoder", {}).values() if t.special} names = sorted((n for n in names if len(n) > 1 and BREAK not in n), key=len, reverse=True) return re.compile("|".join(map(re.escape, names))) if names else None def plain(tokenizer, text: str) -> str: """`text` as the caller's text: no special token of `tokenizer` in it once encoded. Text without one is returned unchanged.""" return _plain(tokenizer, text, False)[0] def plain_offsets(tokenizer, text: str) -> tuple[str, list[int]]: """The same escaped text, with every character boundary mapped to the original text.""" return _plain(tokenizer, text, True) def _plain(tokenizer, text: str, offsets: bool) -> tuple[str, list[int] | None]: _TOKENIZERS.setdefault(id(tokenizer), tokenizer) boundaries = list(range(len(text) + 1)) if offsets else None pattern = _specials_cached(id(tokenizer)) if pattern is None: return text, boundaries n = 1 while n: # again for a match that overlapped the one broken before it if offsets: insertions = [m.start() + 1 for m in pattern.finditer(text)] for at in reversed(insertions): boundaries.insert(at, boundaries[at]) text, n = pattern.subn(lambda m: m[0][0] + BREAK + m[0][1:], text) return text, boundaries _PLACEHOLDER = re.compile(r"^opt\d+$") def _option_line(code: str, label: str, desc: str | None, style: str) -> str: text = desc or label.replace("_", " ") if style == "name_desc" and not _PLACEHOLDER.match(label) and label != text: return f"{code} {label}: {text}" return f"{code} {text}" def said(q: Question) -> str: """A question's instructions as its text writes them: their place (`media.SPOKEN`) when they are spoken.""" return SPOKEN if isinstance(q.instructions, Media) else q.instructions def edge_legend(tokenizer) -> str: """How a box's edges are written: number tokens on a checkpoint that reads numbers, else codes.""" from .box import edge_grid grid, scale = edge_grid(tokenizer) if scale > 1: return ("Each edge is a number from 0 to 999, in thousandths of the picture: top and bottom from its top edge, " "left and right from its left edge.") codes = [code for code, _ in aliases(tokenizer, list(grid_question("", grid).criteria))] return (f"Each edge is a code on a {grid.size}-value scale: {codes[0]} is 0 (top or left), {codes[-1]} is 1 " "(bottom or right). Values are equally spaced.") def question_block(tokenizer, q: Question, option_style: str = "desc") -> str: if isinstance(q, Box): one = "one number" if isinstance(question_slots(q, tokenizer)[-1], Number) else "one code" return (f"{said(q)}\n\nPresence is yes or no. {edge_legend(tokenizer)} Reply with yes or no at present, " f"then {one} at top, bottom, left and right.") if isinstance(q, Boxes): from .box import INSTANCES most = f"{INSTANCES} at most, " if version(tokenizer) == 1 else "" return (f"{said(q)}\n\nEvery one of them, {most}in reading order (left edge, then top). " f"Present k is yes when there are at least k. {edge_legend(tokenizer)} Reply with yes or no at each " "present, then its top, bottom, left and right.") if isinstance(q, Estimate) and isinstance(question_slots(q, tokenizer)[0], Number): unit = f" {q.unit}" if q.unit is not None else "" return (f"{said(q)}\n\nEstimate from {q.grid.low:g} to {q.grid.high:g}{unit}, in steps of " f"{q.grid.step:g}. Reply with the number only.") if isinstance(q, Estimate): bins = grid_question("", q.grid) codes = ([str(i) for i in range(q.grid.size)] if isinstance(bins, Score) else [code for code, _ in aliases(tokenizer, list(bins.criteria))]) unit = f" {q.unit}" if q.unit is not None else "" return (f"{said(q)}\n\nEstimate from {q.grid.low:g} to {q.grid.high:g}{unit} on " f"{q.grid.size} equally spaced values, step {q.grid.step:g}: " f"{codes[0]} is {q.grid.low:g}, {codes[-1]} is {q.grid.high:g}. Reply with the code only.") if isinstance(q, Read): unit = f" (unit: {q.unit})" if q.type == "number" and q.unit else "" return f"{said(q)}\n\nRead the {q.type}{unit} from the state, or nothing if it is absent." if isinstance(q, Choice): labels = list(q.criteria.keys()) if q.multiple: lines = "\n".join(f"{json.dumps(name, ensure_ascii=False)}: {q.criteria[name] or name}" for name in labels) return f"{said(q)}\n\nOptions:\n{lines}\n\nReply with yes or no for each option." lines = "\n".join(_option_line(code, lab, q.criteria[lab], option_style) for (code, _), lab in zip(aliases(tokenizer, labels), labels)) return f"{said(q)}\n\nOptions:\n{lines}\n\nReply with the option code only." if isinstance(q, Noul): extra = "" if q.criteria: extra = f"\nYes: {q.criteria.get('true')}\nNo: {q.criteria.get('false')}" return f"{said(q)}{extra}\n\nReply with yes or no only." if isinstance(q, Score): legend = "\n".join(f"{i} {name}" for i, name in enumerate(q.criteria)) return f"{said(q)}\n\n{legend}\n\nReply with a single digit 0-{len(q.criteria) - 1} only." raise TypeError(f"unknown question type {type(q)}") def prefix_text(tokenizer, state: Any, bos: str = "", style: str = DEFAULT_STATE_STYLE, system: str = DEFAULT_SYSTEM, media: str = "") -> str: """Everything before the question, shared by all questions on one state: the media's markup (`media`, as `media.Reader` writes it) at the head of the user turn, then the state. With no state (`None`) the question follows the media directly. The state is the caller's text (`plain`).""" text = SYSTEMS[system] turn = "" if text is None else f"{IM_START}system\n{text}{IM_END}\n" if state is None: body = "" else: body = f"{plain(tokenizer, state_block(state, style))}\n{question_header(tokenizer, state)}" return f"{bos}{turn}{IM_START}user\n{media}{body}" def question_header(tokenizer, state: Any) -> str: """What `prefix_text` ends with to open a question. `set` attention repeats it before each further question packed after the state (`attention.pack_set`).""" return "" if state is None else "QUESTION:\n" # What sits between the assistant header and the answer slot. A family module registers its measured one # (`LEADS`); any other family's is read off its chat template (`template_lead`). DEFAULT_LEAD = "" LEADS: dict[str, str] = {} def default_lead(model_type: str | None, tokenizer=None) -> str: """What a checkpoint's own template writes before a non-thinking answer: `LEADS` for the families measured, else read off the tokenizer's chat template when one is given.""" if model_type in LEADS or tokenizer is None: return LEADS.get(model_type, DEFAULT_LEAD) return template_lead(tokenizer) def template_lead(tokenizer) -> str: """What a ChatML template writes after the assistant header for a non-thinking reply. The prompt is ChatML (`IM_START`, `IM_END`), so a model with another template is refused rather than mis-prompted.""" text = tokenizer.apply_chat_template([{"role": "user", "content": "x"}], add_generation_prompt=True, tokenize=False, enable_thinking=False) head = f"{IM_START}assistant\n" if head not in text: raise ValueError("not a ChatML chat template: d1 renders ChatML prompts only") return text[text.rindex(head) + len(head):] def suffix_text(tokenizer, q: Question, lead: str = DEFAULT_LEAD, option_style: str = "desc") -> str: """The question and the assistant header, up to the answer slot. The question is the caller's text (`plain`).""" return suffix_layout(tokenizer, q, lead, option_style)[0] def suffix_layout(tokenizer, q: Question, lead: str = DEFAULT_LEAD, option_style: str = "desc") -> tuple[str, list[int]]: """The suffix and each slot's token offset from its last token; old questions have one slot at zero.""" body = question_block(tokenizer, q, option_style) if isinstance(q, Read): return marked_suffix(tokenizer, body, ["start", "end"], lead) if isinstance(q, Box): from .box import EDGES return marked_suffix(tokenizer, body, ["present", *EDGES], lead) if isinstance(q, Boxes): from .box import EDGES, INSTANCES # the last block ends on the newline a next block would follow, where the colon and it are one token, so every # slot is the token the whole read has there: prompt 1 when `max` reads fewer than its 16, prompt 2 always return marked_suffix(tokenizer, body, [f"{name} {k}" for k in range(1, q.blocks + 1) for name in ("present", *EDGES)], lead, "\n" if q.blocks < INSTANCES or version(tokenizer) > 1 else "") if isinstance(q, Choice) and q.multiple: names = [json.dumps(name, ensure_ascii=False) for name in q.criteria] return marked_suffix(tokenizer, body, names, lead) return f"{plain(tokenizer, body)}{IM_END}\n{IM_START}assistant\n{lead}", [0] def marked_suffix(tokenizer, body: str, names: Sequence[str], lead: str = DEFAULT_LEAD, after: str = "") -> tuple[str, list[int]]: """A question's named answer markers and their token offsets from its last token; `after`, whitespace the last marker's colon token takes where the tokenizer writes the two as one token.""" if not names or len(set(names)) != len(names) or any(not isinstance(n, str) or not n or "\n" in n for n in names): raise ValueError("slot names must be distinct nonempty single lines") text = f"{plain(tokenizer, body)}{IM_END}\n{IM_START}assistant\n{lead}" ends = [] for i, name in enumerate(names): text += ("\n" if i else "") + plain(tokenizer, name) + ":" ends.append(len(text)) if after and tokenizer.decode(tokenizer.encode(text + after, add_special_tokens=False)[-1:]).rstrip().endswith(":"): text += after encoded = tokenizer(text, add_special_tokens=False, return_offsets_mapping=True) ids = encoded["input_ids"] slots = [] for chars in ends: matching = [(i, stop) for i, (start, stop) in enumerate(encoded["offset_mapping"]) if start <= chars - 1 < stop] if len(matching) != 1: raise ValueError("a slot marker must have an exact colon token position") end, stop = matching[0] # The colon may share its token with following whitespace, never with the next marker's text. if text[chars:stop].strip() or not tokenizer.decode([ids[end]]).rstrip().endswith(":"): raise ValueError("a slot marker token must end with its colon and optional whitespace") slots.append(end - len(ids) + 1) return text, slots def number_ids(tokenizer, grid, scale: int = 1) -> tuple[tuple[int, ...], ...] | None: """Each grid value times `scale` as the tokenizer writes it, plain and after a space, one token a form: None when a value is no integer or takes more than one token (a tokenizer of digits).""" _TOKENIZERS.setdefault(id(tokenizer), tokenizer) return _number_ids_cached(id(tokenizer), grid.low, grid.high, grid.size, scale) @lru_cache(maxsize=256) def _number_ids_cached(tokenizer_key: int, low: float, high: float, size: int, scale: int): from .grid import Grid groups = [] for value in Grid(low, high, size).values: n = round(value * scale) if not math.isclose(n, value * scale, abs_tol=1e-6): return None group = _ids_cached(tokenizer_key, (str(n), f" {n}")) if not group or len(_TOKENIZERS[tokenizer_key].encode(str(n), add_special_tokens=False)) != 1: return None groups.append(group) return tuple(groups) def grid_slot(tokenizer, instructions: str, grid, scale: int = 1) -> Number | Choice | Score: """A grid's slot: the tokenizer's own numbers where the checkpoint reads them so (`d1_numbers`) and every value is one (`number_ids`), else the grid's codes (`grid_question`).""" if getattr(tokenizer, "d1_numbers", False) and number_ids(tokenizer, grid, scale) is not None: return Number(instructions, grid, scale) return grid_question(instructions, grid) def grid_question(instructions: str, grid) -> Choice | Score: """The grid's ordered bins on the existing digit or option-code readout.""" labels = [f"{v:g}" for v in grid.values] return (Score(instructions, labels) if grid.size <= 10 else Choice(instructions, {str(i): v for i, v in enumerate(labels)})) def render(tokenizer, state: Any, q: Question, bos: str = "", lead: str = DEFAULT_LEAD, style: str = DEFAULT_STATE_STYLE, system: str = DEFAULT_SYSTEM, option_style: str = "desc", media: str = "") -> str: return prefix_text(tokenizer, state, bos, style, system, media) + suffix_text(tokenizer, q, lead, option_style)