"""Dinah-1 with two experts (code and calculator). For every request the gate looks only at the content, the same way for every request: 1. code expert: "what does this function return" questions are settled by running the function (`tools.py`); 2. calculator expert: numeric multiple-choice word problems are solved by a calculator loop in which a Dinah trained on calculator decisions (`calculator/model.safetensors`, a second 150M encoder) picks each calculation (which number, which operation, when to stop), a beam search keeps several paths, and the option that collects the most probability among finished paths wins; 3. everything else, and whatever the experts leave open, is answered by the Dinah-1 encoder at the root of this repository (PyTorch, or int8 ONNX on CPU). An expert answers only what it settles; nothing is truncated and every option gets a probability. from experts import Dinah01 model = Dinah01.from_pretrained("path/or/repo") model.predict(state, {"answer": {"type": "choice", "criteria": {...}, "instructions": "..."}}) """ from __future__ import annotations import copy import math import operator import re from collections import OrderedDict, defaultdict from pathlib import Path from typing import Any import numpy as np from dinah import QUESTION_TYPES, Dinah, DinahONNX, _resolve from tools import TOOLS # ---------------------------------------------------------------- calculator expert NUM = re.compile(r"(? self.max_cache: self.cache.popitem(last=False) return hs def _search(self, problem): alive, done = [(0.0, _Scratchpad(problem))], [] for _ in range(MAX_STEPS): if not alive: break opts1 = [sp.first_options() for _, sp in alive] l1 = self._logprobs([(sp.state(), I_FIRST, o) for (_, sp), o in zip(alive, opts1)]) second = [] for (lp, sp), o, l in zip(alive, opts1, l1): for c in np.argsort(-l)[:self.k_first]: if o[c] == DONE: done.append((lp + l[c], sp.last)) else: second.append((lp + l[c], sp, sp.vals[c])) if not second: alive = [] break opts2 = [sp.operation_options(a) for _, sp, a in second] l2 = self._logprobs([(sp.state(a), I_OPERATION, [t for t, _, _ in o]) for (_, sp, a), o in zip(second, opts2)]) new = [] for (lp, sp, a), o, l in zip(second, opts2, l2): for c in np.argsort(-l)[:self.k_op]: _, s, b = o[c] sp2 = copy.deepcopy(sp) if sp2.apply(a, s, b) is not None: new.append((lp + l[c], sp2)) alive = sorted(new, key=lambda x: -x[0])[:self.beam] done += [(lp, sp.last) for lp, sp in alive] return sorted([(lp, r) for lp, r in done if r is not None], reverse=True) def __call__(self, state, questions): problem = state if isinstance(state, str) else ( max((v for v in state.values() if isinstance(v, str) and re.search(r"\d", v)), key=len, default=None) if isinstance(state, dict) else None) if not problem or len(problem) < 40 or len(_numbers(problem)) < 2: return {} out = {} for key, q in questions.items(): if q.get("type") != "choice": continue keys = list(q["criteria"]) options = [q["criteria"][k] for k in keys] if len(options) < 3 or not all(isinstance(o, str) and NUMERIC_OPTION.match(o) for o in options): continue values = [float(o.replace("$", "").replace(",", "").replace("%", "").strip()) for o in options] mass = defaultdict(float) for lp, r in self.hypotheses(problem): hit = [i for i, v in enumerate(values) if abs(r - v) < 1e-4] if hit: mass[hit[0]] += math.exp(lp) if not mass: continue best = max(mass, key=mass.get) rest = (1.0 - self.p) / (len(keys) - 1) out[key] = {"type": "choice", "choice": keys[best], "confidence": self.p, "probabilities": {k: (self.p if i == best else rest) for i, k in enumerate(keys)}} return out def _calculator(directory: Path, device: str | None, dtype: str | None) -> Dinah: """The calculator expert is stored as a rank-64 difference from the root encoder (calculator/delta.safetensors, fp16): W = W_root + A @ B for large matrices, W_root + delta for the rest. Rebuilt in memory at load time.""" from safetensors.torch import load_file model = Dinah(directory, device, dtype) # starts from the root encoder full = directory / "calculator" / "model.safetensors" if full.exists(): # Dinah-1: the calculator is a complete model (no difference to rebuild) model.net.load_state_dict(load_file(str(full)), strict=True) return model delta = load_file(str(directory / "calculator" / "delta.safetensors")) state = {k: v.float().cpu() for k, v in model.net.state_dict().items()} for k in state: if k + ".A" in delta: state[k] = state[k] + delta[k + ".A"].float() @ delta[k + ".B"].float() elif k in delta: state[k] = state[k] + delta[k].float() model.net.load_state_dict(state, strict=True) return model class Dinah01: FILES = ["*.json", "*.py", "model.safetensors", "onnx/model_int8.onnx", "calculator/*"] def __init__(self, directory: Path, device: str | None = None, dtype: str | None = None, tools: str = "code,calculator", backend: str = "torch"): self.encoder = DinahONNX(directory) if backend == "onnx" else Dinah(directory, device, dtype) self.max_length = self.encoder.max_length names = [] if str(tools).lower() in ("", "0", "none", "off") else [t.strip() for t in str(tools).split(",")] self.experts = [Calculator(_calculator(directory, device, dtype)) if n == "calculator" else TOOLS[n] for n in names] self.tools, self.backend = names, backend @classmethod def from_pretrained(cls, path_or_repo: str | Path = ".", device: str | None = None, dtype: str | None = None, revision: str | None = None, tools: str = "code,calculator", backend: str = "torch") -> "Dinah01": return cls(_resolve(path_or_repo, cls.FILES, revision), device, dtype, tools, backend) def predict(self, state: Any, questions: dict[str, dict[str, Any]], batch_size: int = 16) -> dict[str, dict[str, Any]]: """Answers {key: answer} for one request (a shared state and several typed questions).""" settled = {} for expert in self.experts: settled.update(expert(state, {k: q for k, q in questions.items() if k not in settled})) rest = [k for k in questions if k not in settled] out = dict(zip(rest, self.encoder.predict([{**questions[k], "state": state} for k in rest], batch_size))) if rest else {} out.update(settled) return {k: out[k] for k in questions}