Download upstream/vela3_encoder.py from FluidInference/vela-3.0-coreml: direct link, hf CLI and curl.
- Browser
- Download file 25.3 kB
-
https://huggingface.co/FluidInference/vela-3.0-coreml/resolve/main/upstream/vela3_encoder.py
- Command line
-
hf download hf://FluidInference/vela-3.0-coreml/upstream/vela3_encoder.py
-
curl -L -o vela3_encoder.py https://huggingface.co/FluidInference/vela-3.0-coreml/resolve/main/upstream/vela3_encoder.py
25.3 kB
| """vela3_encoder.py: inference for vela3-fast (UltraBERT-380M encoder). | |
| Three networks on one frozen backbone: | |
| DecisionEncoder the decision backbone (UBD3 stage 1b, soup a+c) and its heads: every choice / noul decision that is not a router | |
| question. This class and model.safetensors are unchanged from the decision-only preview (8667a7bb), so these | |
| answers are the preview's, bit for bit. | |
| span adapter LoRA r64 on all 24 layers + the top 4 layers + span head (span_adapter.safetensors): every span question. | |
| router adapter LoRA r64 on all 24 layers + choice / set head (router_adapter.safetensors): choice / set questions whose | |
| instructions are one of the router texts in router_questions.json. | |
| Decision rendering (one encoder input per question, exactly as in training, ub_d3_common.to_row): | |
| [BOS] [Q] instructions [O] key_1: description_1 [O] key_2 ... [SEP_SCHEMA] [SEG_user] state [EOS] | |
| state / instructions: strings as is, JSON objects as compact JSON; noul = the 2-option choice {no, yes}, answer = p(yes). | |
| Readout (both, as trained): cosine(w_o(norm(H[O_i])) + w_q(norm(H[Q])), w_p(norm(mean H over [SEG_user] .. state))) / tau | |
| + MLP(H[O_i]); softmax over the question's options. Nothing is truncated: an input over max_len raises Unsupported. | |
| Span and router passes: vela3_schema.py (the Vela 2.0 layout, 8,192-token windows). Evidence questions (EV-FIX, 18 Oct 2026, | |
| DIAG_04B_EV_HALU.md): one window up to span.evidence.max_len (32,768) on the same span adapter, and a threshold by target length | |
| (span.evidence.buckets: <= 2,048 tokens the short-dev pick, longer the QASPER-validation pick). | |
| Task adapters (round 1, 19 Oct 2026; vela3_config.json span.task_adapters): relation questions ("Which spans are the <relation> of <head>?", | |
| "Which spans are related to <head> by these relations?", ...) -> the relation adapter, per-form thresholds (pair = one relation, multi = | |
| several); open-label entity questions (names only) stay on the span adapter. | |
| from vela3_encoder import Vela3Encoder | |
| m = Vela3Encoder.load("path/to/this/folder") # cuda if available | |
| m.system_one(state, {"q": {"type": "choice", "instructions": "...", "criteria": {"a": "...", "b": None}}, | |
| "ents": {"type": "span", "instructions": "Which spans are named entities of these types?", | |
| "criteria": {"person": None, "company": None}}}) | |
| m.predict(state, questions) # decisions only, the preview's API and answers (the Decision Index engine uses it) | |
| """ | |
| import json, math | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| from ub_d3_common import to_row | |
| MARKERS = ["[Q]", "[O]", "[ABS]", "[SEP_SCHEMA]", "[SEG_user]", "[SEG_answer]", "[E]", "[SEG_context]"] | |
| class Unsupported(ValueError): | |
| pass | |
| def opt_text(name, desc): | |
| return name if not desc else f"{name}: {desc}" | |
| class DecisionEncoder(nn.Module): | |
| def __init__(self, cfg, proj_dim=256): | |
| super().__init__() | |
| from transformers import AutoModel | |
| self.encoder = AutoModel.from_config(cfg, trust_remote_code=True, attn_implementation="sdpa", dtype=torch.float32) | |
| h = cfg.hidden_size | |
| self.norm = nn.LayerNorm(h) | |
| self.w_o, self.w_q, self.w_p = nn.Linear(h, proj_dim), nn.Linear(h, proj_dim), nn.Linear(h, proj_dim) | |
| self.log_tau = nn.Parameter(torch.tensor(math.log(0.05))) | |
| self.cls_mlp = nn.Sequential(nn.Linear(h, 2 * h), nn.ReLU(), nn.Dropout(0.1), nn.Linear(2 * h, 1)) | |
| def load(cls, path, device=None, max_len=32768, token_budget=65536): | |
| from safetensors.torch import load_file | |
| from transformers import AutoConfig, AutoTokenizer | |
| path = Path(path); device = device or ("cuda" if torch.cuda.is_available() else "cpu") | |
| cfg = AutoConfig.from_pretrained(str(path), trust_remote_code=True) | |
| m = cls(cfg, cfg.decision["proj_dim"]) | |
| m.load_state_dict(load_file(str(path / "model.safetensors")), strict=True) | |
| m = m.to(device).eval() | |
| # text is tokenised with the base UltraBERT tokenizer (as in training); the 8 marker tokens are the rows appended after it | |
| m.tok = AutoTokenizer.from_pretrained(str(path), trust_remote_code=True) | |
| m.mk = {t: int(cfg.decision["marker_ids"][t]) for t in MARKERS} | |
| assert len(m.tok) == cfg.decision["n_vocab_base"] == min(m.mk.values()), "tokenizer / marker mismatch" | |
| m.device, m.max_len, m.budget = device, int(max_len), int(token_budget) | |
| return m | |
| def _ids(self, s): | |
| return self.tok(s, add_special_tokens=False)["input_ids"] if s else [] | |
| def encode(self, state, q, state_ids=None): | |
| """-> dict(ids, q_pos, opt_pos, names, pool) for one question, or raise Unsupported.""" | |
| qq = to_row("q", state, q)["questions"][0]; st = to_row("q", state, q)["parts"][0]["text"] | |
| ids = [self.tok.bos_token_id, self.mk["[Q]"]] + self._ids(qq["text"]); opt_pos, names = [], [] | |
| for n, d in qq["options"].items(): | |
| opt_pos.append(len(ids)); names.append(n); ids += [self.mk["[O]"]] + self._ids(opt_text(n, d)) | |
| ids.append(self.mk["[SEP_SCHEMA]"]); ps = len(ids) | |
| ids += [self.mk["[SEG_user]"]] + (state_ids if state_ids is not None else self._ids(st)); pe = len(ids); ids.append(self.tok.eos_token_id) | |
| if len(ids) > self.max_len: | |
| raise Unsupported(f"input longer than the {self.max_len}-token context") | |
| return dict(ids=ids, q_pos=1, opt_pos=opt_pos, names=names, pool=(ps, pe)) | |
| def _logits(self, H, recs): | |
| H = H.float(); dev = H.device | |
| ob = torch.tensor([b for b, r in enumerate(recs) for _ in r["opt_pos"]], device=dev) | |
| op = torch.tensor([p for r in recs for p in r["opt_pos"]], device=dev) | |
| own = torch.tensor([k for k, r in enumerate(recs) for _ in r["opt_pos"]], device=dev) | |
| b = torch.arange(len(recs), device=dev) | |
| ps = torch.tensor([r["pool"][0] for r in recs], device=dev); pe = torch.tensor([r["pool"][1] for r in recs], device=dev) | |
| cs = nn.functional.pad(H.cumsum(1), (0, 0, 1, 0)) | |
| pooled = (cs[b, pe] - cs[b, ps]) / (pe - ps).clamp(min=1).unsqueeze(-1).float() | |
| v = nn.functional.normalize(self.w_p(self.norm(pooled)), dim=-1) | |
| uq = self.w_q(self.norm(H[b, torch.tensor([r["q_pos"] for r in recs], device=dev)])) | |
| u = nn.functional.normalize(self.w_o(self.norm(H[ob, op])) + uq[own], dim=-1) | |
| lg = (u * v[own]).sum(-1) / self.log_tau.exp() + self.cls_mlp(H[ob, op]).squeeze(-1) | |
| return list(torch.split(lg, [len(r["opt_pos"]) for r in recs])) | |
| def forward_recs(self, recs): | |
| """Batched by length under a token budget (as the evaluated engine); bf16 autocast on CUDA. -> list of prob arrays.""" | |
| out = [None] * len(recs); order = sorted(range(len(recs)), key=lambda i: len(recs[i]["ids"])); i = 0 | |
| amp = torch.autocast("cuda", dtype=torch.bfloat16, enabled=self.device == "cuda") | |
| while i < len(order): | |
| j = i + 1 | |
| while j < len(order) and len(recs[order[j]]["ids"]) * (j - i + 1) <= self.budget: | |
| j += 1 | |
| grp = [recs[k] for k in order[i:j]]; w = max(len(r["ids"]) for r in grp) | |
| x = torch.full((len(grp), w), self.tok.pad_token_id, dtype=torch.long); msk = torch.zeros_like(x) | |
| for r_, r in enumerate(grp): | |
| x[r_, :len(r["ids"])] = torch.tensor(r["ids"]); msk[r_, :len(r["ids"])] = 1 | |
| with amp: | |
| H = self.encoder(input_ids=x.to(self.device), attention_mask=msk.to(self.device)).last_hidden_state | |
| for k, lg in zip(order[i:j], self._logits(H, grp)): | |
| z = lg.double().cpu().numpy(); p = np.exp(z - z.max()); out[k] = p / p.sum() | |
| i = j | |
| return out | |
| def predict(self, state, questions): | |
| """questions: {key: {type: choice | noul, instructions, criteria}} -> {key: answer} (choice: choice + probabilities; noul: p(yes)).""" | |
| recs, keys, st = [], [], None | |
| for k, q in questions.items(): | |
| if q["type"] not in ("choice", "noul"): | |
| raise Unsupported("Unsupported question type " + str(q["type"])) | |
| if st is None: | |
| st = self._ids(to_row("q", state, q)["parts"][0]["text"]) | |
| recs.append(self.encode(state, q, st)); keys.append(k) | |
| ans = {} | |
| for k, r, p in zip(keys, recs, self.forward_recs(recs)): | |
| if questions[k]["type"] == "noul": | |
| ans[k] = {"type": "noul", "noul": float(p[r["names"].index("yes")])} | |
| else: | |
| pr = {n: float(v) for n, v in zip(r["names"], p)} | |
| ans[k] = {"type": "choice", "choice": max(pr, key=pr.get), "probabilities": pr} | |
| return ans, sum(len(r["ids"]) for r in recs) | |
| # ===================================================================================================================== | |
| # vela3-fast: decisions (the preview's DecisionEncoder, unchanged) + gated span / router adapters | |
| # ===================================================================================================================== | |
| import re as _re | |
| ROLES = ("user", "context", "answer") | |
| _REL_Q = _re.compile(r"\bspans? (?:are|is) the .+ (?:of|for)\b|\brelat(?:ed|ion|ions|es|ing)?\b|\b(?:head|tail)\b|\bstands? in\b" | |
| r"|\bwith respect to\b|\bcounterpart\b|\bsuch that\b|\brole of\b") | |
| _EV_Q = _re.compile(r"\b(?:evidence|support(?:s|ing|ed)?|cite[sd]?|citations?)\b") | |
| def _norm(s): | |
| return " ".join(str(s or "").split()) | |
| def _parts(state): | |
| """System One state -> [(role, text)]: a string is the user part; an object of text parts uses the roles user / context / answer.""" | |
| if isinstance(state, str): | |
| return [("user", state)] | |
| if isinstance(state, dict) and state and all(isinstance(v, str) for v in state.values()): | |
| bad = [k for k in state if k not in ROLES] | |
| if bad: | |
| raise ValueError(f"span / router state parts must be named {ROLES}, got {bad}") | |
| return list(state.items()) | |
| raise ValueError("span and router questions need a text state or an object of text parts {user | context | answer: text}") | |
| class Vela3Encoder: | |
| """vela3-fast. system_one(state, questions): choice / noul (decision backbone), router choice / set (router adapter), span | |
| (span adapter). predict(state, questions): the preview's decision API (decision backbone only).""" | |
| def __init__(self, path, device=None, max_len=32768, token_budget=65536, adapter_bf16=False, entity_route=None): | |
| """adapter_bf16: run the span / router passes under bf16 autocast (default fp32, as evaluated). Decisions: unchanged (bf16 autocast on GPU).""" | |
| import vela3_schema as VS | |
| path = Path(path) | |
| self.decision = DecisionEncoder.load(path, device=device, max_len=max_len, token_budget=token_budget) | |
| dev = self.decision.device | |
| self.cfg = json.loads((path / "vela3_config.json").read_text()) | |
| rq = json.loads((path / "router_questions.json").read_text())["questions"] | |
| self.router_q = {_norm(t): s for t, s in rq.items()} | |
| span_net = VS.build_adapter(self.decision, path / "span_adapter.safetensors", dev) | |
| self.span = VS.SchemaEngine(path, span_net, dev, amp_bf16=adapter_bf16) | |
| ev_len = int(self.cfg["span"]["evidence"].get("max_len", self.span.max_len)) # EV-FIX: evidence engine, same net (no extra weights) | |
| evf = self.cfg["span"]["evidence"].get("adapter") # EVFAST: optional dedicated evidence adapter (evidence questions only; missing file = off) | |
| ev_net = VS.build_adapter(self.decision, path / evf, dev) if evf and (path / evf).exists() else span_net | |
| self.span_ev = self.span if (ev_len == self.span.max_len and ev_net is span_net) else VS.SchemaEngine(path, ev_net, dev, tokenizer=self.span.tok, max_len=ev_len, | |
| token_budget=max(ev_len, 32768), amp_bf16=adapter_bf16) | |
| self.task = {} # per-task span adapters (span.task_adapters: {task: file}); a missing file = route off | |
| for t, f in (self.cfg["span"].get("task_adapters") or {}).items(): | |
| if (path / f).exists(): | |
| self.task[t] = VS.SchemaEngine(path, VS.build_adapter(self.decision, path / f, dev), dev, tokenizer=self.span.tok, amp_bf16=adapter_bf16) | |
| # entity route for names-only open-label entity questions: "ner" (the NER adapter, default) or "span" (the span adapter). The | |
| # end-to-end relation pipeline (entity step, then relation questions) runs with "span" (span.entity_route_relation_pipeline): | |
| # set VELA3_ENTITY_ROUTE=span, or Vela3Encoder.load(..., entity_route="span"), for that pipeline. | |
| import os as _os | |
| self.entity_route = entity_route or _os.environ.get("VELA3_ENTITY_ROUTE") or self.cfg["span"].get("entity_route", "ner") | |
| # R2c: a second span adapter for the router label sets (span.set_adapter: {file, label_sets, router_set_temperature, | |
| # thresholds_by_set, pii_buckets}): halu and PII questions are answered by it with its own dev-picked temperature / thresholds | |
| ss = self.cfg["span"].get("set_adapter"); self.span_set = None | |
| if ss and (path / ss["file"]).exists(): | |
| self.span_set = VS.SchemaEngine(path, VS.build_adapter(self.decision, path / ss["file"], dev), dev, tokenizer=self.span.tok, amp_bf16=adapter_bf16) | |
| self.router = VS.SchemaEngine(path, VS.build_adapter(self.decision, path / "router_adapter.safetensors", dev), dev, tokenizer=self.span.tok, amp_bf16=adapter_bf16) | |
| # DOM04B: optional per-task router adapters (vela3_config.json router.task_adapters: {task: {file, ids}}): a router question whose | |
| # trained schema id is in ids is answered by that adapter; a missing file = route off (the shared router adapter answers) | |
| self.router_task = {} | |
| for t, ta in (self.cfg["router"].get("task_adapters") or {}).items(): | |
| files = ta.get("files") or [ta["file"]] # several files = logit average of the adapters (an ensemble entry) | |
| if all((path / f).exists() for f in files): | |
| engs = [VS.SchemaEngine(path, VS.build_adapter(self.decision, path / f, dev), dev, tokenizer=self.span.tok, amp_bf16=adapter_bf16) for f in files] | |
| for i in ta["ids"]: | |
| self.router_task[i] = (t, engs) | |
| self._VS = VS | |
| self.name = self.cfg["name"] | |
| def load(cls, path, **kw): | |
| return cls(path, **kw) | |
| # ------------------------------------------------------------------ decisions (the preview, unchanged) | |
| def predict(self, state, questions): | |
| return self.decision.predict(state, questions) | |
| # ------------------------------------------------------------------ router questions | |
| def _router_answers(self, state, qs): | |
| textual = isinstance(state, str) or (isinstance(state, dict) and state and all(isinstance(v, str) for v in state.values()) and set(state) <= set(ROLES)) | |
| parts = _parts(state) if textual else [("user", json.dumps(state, ensure_ascii=False, separators=(",", ":")))] # JSON state: compact JSON | |
| roles = [r for r, _ in parts]; rows = {} | |
| for k, q in qs.items(): | |
| s = self.router_q[_norm(q.get("instructions"))] | |
| crit = q.get("criteria") or {} | |
| if isinstance(crit, (list, tuple)): | |
| crit = {str(c): None for c in crit} | |
| opts = {str(n): (str(d) if d else (s["options"] or {}).get(str(n), "")) for n, d in crit.items()} or dict(s["options"]) | |
| over = q.get("over") or (s["over"] if s["over"] in roles else roles[-1]) | |
| rows.setdefault((over, s["id"] if s["id"] in self.router_task else None), []).append(dict(id=k, type=s["type"], over=over, text=q["instructions"], options=opts, abstain=bool(s["abstain"]))) | |
| out, n_tok = {}, 0 | |
| for (over, task), gq in rows.items(): | |
| engs = self.router_task[task][1] if task else [self.router]; head = f"router:{self.router_task[task][0]}" if task else "router" | |
| raws = [e.predict_raw([dict(parts=[dict(role=r, text=t) for r, t in parts], questions=gq)])[0] for e in engs]; raw = raws[0] | |
| if len(raws) > 1: # logit average over the ensemble's adapters (same questions, same option order) | |
| for qi, q in enumerate(raw["qs"]): | |
| q["logits"] = list(np.mean([np.asarray(r_["qs"][qi]["logits"], np.float64) for r_ in raws], axis=0)) | |
| n_tok += raw["n_input"] | |
| for q in raw["qs"]: | |
| lg = np.asarray(q["logits"], np.float64); n = len(q["names"]) | |
| if q["qtype"] == "set": | |
| p = 1 / (1 + np.exp(-lg[:n])); thr = float(self.cfg["router"]["set_threshold"]) | |
| pr = {nm: float(v) for nm, v in zip(q["names"], p)} | |
| out[q["qid"]] = {"type": "set", "selected": [nm for nm, v in pr.items() if v > thr], "probabilities": pr, "threshold": thr, "head": head} | |
| else: | |
| z = lg[:n]; p = np.exp(z - z.max()); p /= p.sum(); pr = {nm: float(v) for nm, v in zip(q["names"], p)} | |
| out[q["qid"]] = {"type": "choice", "choice": max(pr, key=pr.get), "probabilities": pr, "head": head} | |
| return out, n_tok | |
| # ------------------------------------------------------------------ span questions | |
| def _label_set(self, names): | |
| sp = self.cfg["span"] | |
| for k, ls in sp["router_label_sets"].items(): | |
| if names and set(names) <= set(ls): | |
| return k | |
| if names and {n.strip().lower() for n in names} <= {x.lower() for x in sp["evidence"]["labels"]}: | |
| return "evidence" | |
| return None | |
| def span_answer(self, state, q): | |
| VS = self._VS; sp = self.cfg["span"] | |
| parts = _parts(state); roles = [r for r, _ in parts] | |
| target = q.get("target") or q.get("over") or ("answer" if "answer" in roles else roles[-1]) | |
| if target not in roles: | |
| raise ValueError(f"target {target!r} is not a part of the state") | |
| crit = q.get("criteria") or q.get("labels") | |
| if isinstance(crit, dict): | |
| labels = [(str(k), "" if v is None else str(v)) for k, v in crit.items()] | |
| elif isinstance(crit, (list, tuple)): | |
| labels = [(str(k), "") for k in crit] | |
| else: | |
| raise ValueError("span questions need criteria: {label: description or null} or a list of labels") | |
| names = [n for n, _ in labels] | |
| if not names or len(set(names)) != len(names): | |
| raise ValueError("span labels must be distinct and at least one") | |
| lset = self._label_set(names); question = q.get("instructions") | |
| if lset in sp["schemas"]: # a trained router label set: its question text and label descriptions fill what the request leaves empty | |
| sch = sp["schemas"][lset]; labels = [(n, d or sch["labels"].get(n, "")) for n, d in labels]; question = question or sch["question"] | |
| if lset == "evidence": | |
| question = question or sp["evidence"]["question"] | |
| question = question or sp["default_question"] | |
| row = dict(parts=[dict(role=r, text=t) for r, t in parts], questions=[dict(id="q", type="span", over=target, text=question, labels=dict(labels))]) | |
| evq = lset == "evidence" or (lset not in ("pii", "halu", "toxic") and bool(_EV_Q.search(_norm(question).lower()))) | |
| eng, head = (self.span_ev if evq else self.span), "span" # EV-FIX: evidence questions in one long window | |
| nq = _norm(question).lower(); names_only = not any(d for _, d in labels) | |
| if lset is None and not evq: # task routes: relation questions -> the relation adapter; names-only entity questions -> ner (when shipped) | |
| if _REL_Q.search(nq): | |
| if "relation" in self.task: | |
| eng, head = self.task["relation"], "relation" | |
| elif names_only and "ner" in self.task and self.entity_route == "ner": | |
| eng, head = self.task["ner"], "ner" | |
| ss = sp.get("set_adapter") | |
| if ss and self.span_set is not None and lset in ss["label_sets"]: # R2c: router label sets on the set adapter, with its thresholds | |
| eng, head = self.span_set, "span_set" | |
| sp = dict(sp, router_set_temperature=ss["router_set_temperature"], thresholds_by_set=dict(sp["thresholds_by_set"], **ss["thresholds_by_set"]), pii_buckets=ss["pii_buckets"]) | |
| raw = eng.predict_raw([row])[0] | |
| n_tok = raw["n_tokens"][target] | |
| self.span.last = eng.last = dict(target_tokens=int(n_tok), windows=max(1, raw["windows"]), prompt_tokens=[int(raw["n_input"])]) | |
| # threshold and temperature: the dev picks of this checkpoint (vela3_config.json span.threshold_sources) | |
| if lset == "pii": | |
| b = next(b for b in sp["pii_buckets"] if n_tok <= b["max_tokens"]); thr0, T = b["threshold"], sp["router_set_temperature"] | |
| why = f"pii label set, {b['name']} bucket ({n_tok} target tokens)" | |
| elif lset in ("halu", "toxic"): | |
| thr0, T, why = sp["thresholds_by_set"][lset], sp["router_set_temperature"], f"{lset} label set" | |
| elif evq: | |
| bs = sp["evidence"].get("buckets") | |
| if bs: # EV-FIX: threshold by target length | |
| b = next(b for b in bs if n_tok <= b["max_tokens"]); thr0, T, why = b["threshold"], 1.0, f"evidence question, {b['name']} bucket ({n_tok} target tokens)" | |
| else: | |
| thr0, T, why = sp["evidence"]["threshold"], 1.0, "evidence question" | |
| elif head == "relation": # per-form dev picks: one relation (pair) / several (multi) | |
| thr0 = sp["relation"]["threshold_pair" if len(labels) == 1 else "threshold_multi"]; T, why = 1.0, f"relation question ({'pair' if len(labels) == 1 else 'multi'})" | |
| elif _REL_Q.search(nq): | |
| thr0, T, why = sp["relation_threshold"], 1.0, "relation question" | |
| elif head == "ner": | |
| thr0, T, why = sp["ner"]["threshold"], 1.0, "open labels, names only" | |
| else: | |
| thr0, T, why = sp["threshold"], 1.0, "open labels" | |
| thr = float(q.get("threshold", thr0)); text = dict(parts)[target] | |
| S = raw["span"]; spans = [] | |
| if S is not None and len(S["offs"]): | |
| p = VS._sigmoid(S["logits"] / T); wo = S["offs"] | |
| spans = VS.decode_spans(p, wo, S["names"], text, thr) | |
| for s in spans: | |
| k = S["names"].index(s["label"]); m = (wo[:, 0] < s["end"]) & (wo[:, 1] > s["start"]) | |
| s["text"] = text[s["start"]:s["end"]]; s["score"] = float(p[m, k].mean()) if m.any() else 0.0 | |
| return {"type": "span", "target": target, "spans": spans, "head": head, "head_reason": f"{head} adapter; {why}", "threshold": thr}, raw["n_input"] | |
| # ------------------------------------------------------------------ System One | |
| def system_one(self, state, questions, model=None): | |
| """questions {qid: question} -> {"model", "answers": {qid: answer}, "usage"}. Choice / noul: the decision backbone (the | |
| preview's answers); choice / set whose instructions are a router text: the router adapter; span: the span adapter. | |
| A question that cannot be answered gets {"type", "error", "message"}; the others are still answered.""" | |
| dec, rout, span, answers, n = {}, {}, {}, {}, 0 | |
| for k, q in questions.items(): | |
| t = q.get("type") if isinstance(q, dict) else None | |
| if t == "span": | |
| span[k] = q | |
| elif t in ("choice", "set") and _norm(q.get("instructions")) in self.router_q: | |
| if self.router_q[_norm(q.get("instructions"))]["type"] != t: | |
| answers[k] = {"type": t, "error": "invalid_question", "message": "router question asked with another type than trained"} | |
| else: | |
| rout[k] = q | |
| elif t in ("choice", "noul"): | |
| dec[k] = q | |
| else: | |
| answers[k] = {"type": t, "error": "invalid_question", "message": f"unsupported question type {t!r} (choice, noul, span; set for router questions)"} | |
| if dec: | |
| try: | |
| a, m = self.decision.predict(state, dec); answers.update(a); n += m | |
| except Unsupported as e: | |
| answers.update({k: {"type": q["type"], "error": "unsupported", "message": str(e)} for k, q in dec.items()}) | |
| if rout: | |
| try: | |
| a, m = self._router_answers(state, rout); answers.update(a); n += m | |
| except ValueError as e: | |
| answers.update({k: {"type": q["type"], "error": "invalid_question", "message": str(e)} for k, q in rout.items()}) | |
| for k, q in span.items(): | |
| try: | |
| a, m = self.span_answer(state, q); answers[k] = a; n += m | |
| except ValueError as e: | |
| answers[k] = {"type": "span", "error": "invalid_question", "message": str(e)} | |
| return {"model": "vllm-sr/" + self.name, "answers": {k: answers[k] for k in questions}, "usage": {"input_tokens": int(n), "output_tokens": 0}} | |