decido-ai 1.0.0__tar.gz

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 (47) hide show
  1. decido_ai-1.0.0/PKG-INFO +62 -0
  2. decido_ai-1.0.0/README.md +36 -0
  3. decido_ai-1.0.0/pyproject.toml +29 -0
  4. decido_ai-1.0.0/setup.cfg +4 -0
  5. decido_ai-1.0.0/src/decido/__init__.py +5 -0
  6. decido_ai-1.0.0/src/decido/_vendor/__init__.py +0 -0
  7. decido_ai-1.0.0/src/decido/_vendor/ds/__init__.py +0 -0
  8. decido_ai-1.0.0/src/decido/_vendor/ds/derive.py +42 -0
  9. decido_ai-1.0.0/src/decido/_vendor/ds/predictor_v4.py +85 -0
  10. decido_ai-1.0.0/src/decido/_vendor/ds/readout.py +175 -0
  11. decido_ai-1.0.0/src/decido/_vendor/ds/template.py +333 -0
  12. decido_ai-1.0.0/src/decido/_vendor/ds/v4/__init__.py +3 -0
  13. decido_ai-1.0.0/src/decido/_vendor/ds/v4/attention.py +109 -0
  14. decido_ai-1.0.0/src/decido/_vendor/ds/v4/experts.py +53 -0
  15. decido_ai-1.0.0/src/decido/_vendor/ds/v4/fp4.py +71 -0
  16. decido_ai-1.0.0/src/decido/_vendor/ds/v4/grouped.py +165 -0
  17. decido_ai-1.0.0/src/decido/_vendor/ds/v4/loader.py +144 -0
  18. decido_ai-1.0.0/src/decido/_vendor/ds/v4/numerics.py +91 -0
  19. decido_ai-1.0.0/src/decido/_vendor/ds/v4/pipeline.py +111 -0
  20. decido_ai-1.0.0/src/decido/_vendor/ds/v4/score.py +113 -0
  21. decido_ai-1.0.0/src/decido/_vendor/teacher/__init__.py +0 -0
  22. decido_ai-1.0.0/src/decido/_vendor/teacher/derive.py +42 -0
  23. decido_ai-1.0.0/src/decido/_vendor/teacher/predictor_hf.py +143 -0
  24. decido_ai-1.0.0/src/decido/_vendor/teacher/predictor_reason.py +317 -0
  25. decido_ai-1.0.0/src/decido/_vendor/teacher/readout.py +159 -0
  26. decido_ai-1.0.0/src/decido/_vendor/teacher/template.py +301 -0
  27. decido_ai-1.0.0/src/decido/api.py +86 -0
  28. decido_ai-1.0.0/src/decido/build.py +90 -0
  29. decido_ai-1.0.0/src/decido/cli.py +86 -0
  30. decido_ai-1.0.0/src/decido/core/__init__.py +0 -0
  31. decido_ai-1.0.0/src/decido/core/derive.py +42 -0
  32. decido_ai-1.0.0/src/decido/core/readout.py +159 -0
  33. decido_ai-1.0.0/src/decido/core/template.py +301 -0
  34. decido_ai-1.0.0/src/decido/cpu.py +56 -0
  35. decido_ai-1.0.0/src/decido/csrc/vserve.cpp +237 -0
  36. decido_ai-1.0.0/src/decido/gpu_systems.py +115 -0
  37. decido_ai-1.0.0/src/decido/models.py +154 -0
  38. decido_ai-1.0.0/src/decido/pro.py +78 -0
  39. decido_ai-1.0.0/src/decido/questions.py +56 -0
  40. decido_ai-1.0.0/src/decido/server.py +288 -0
  41. decido_ai-1.0.0/src/decido/torch_engine.py +65 -0
  42. decido_ai-1.0.0/src/decido_ai.egg-info/PKG-INFO +62 -0
  43. decido_ai-1.0.0/src/decido_ai.egg-info/SOURCES.txt +45 -0
  44. decido_ai-1.0.0/src/decido_ai.egg-info/dependency_links.txt +1 -0
  45. decido_ai-1.0.0/src/decido_ai.egg-info/entry_points.txt +2 -0
  46. decido_ai-1.0.0/src/decido_ai.egg-info/requires.txt +20 -0
  47. decido_ai-1.0.0/src/decido_ai.egg-info/top_level.txt +1 -0
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: decido-ai
3
+ Version: 1.0.0
4
+ Summary: Calibrated decisions (yes/no, pick-one, ordered scale) from a document — Decido models from the Hugging Face Hub, on a laptop CPU or a GPU.
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://huggingface.co/decido-ai
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: numpy>=1.24
10
+ Requires-Dist: tokenizers>=0.20
11
+ Requires-Dist: huggingface_hub>=0.25
12
+ Provides-Extra: gpu
13
+ Requires-Dist: torch>=2.4; extra == "gpu"
14
+ Requires-Dist: transformers>=5.17; extra == "gpu"
15
+ Requires-Dist: accelerate; extra == "gpu"
16
+ Provides-Extra: xl
17
+ Requires-Dist: torch>=2.6; extra == "xl"
18
+ Requires-Dist: transformers>=5.22; extra == "xl"
19
+ Requires-Dist: accelerate; extra == "xl"
20
+ Requires-Dist: peft>=0.21; extra == "xl"
21
+ Requires-Dist: triton>=3.2; extra == "xl"
22
+ Requires-Dist: safetensors; extra == "xl"
23
+ Provides-Extra: pro
24
+ Requires-Dist: vllm==0.31.0; extra == "pro"
25
+ Requires-Dist: transformers>=5.17; extra == "pro"
26
+
27
+ # decido
28
+
29
+ **Calibrated decisions from a document — on a laptop CPU or a GPU.** Give Decido a document and questions (yes/no, pick-one,
30
+ or an ordered scale) and get a probability for every answer. This package runs the [Decido 1 models](https://huggingface.co/decido-ai)
31
+ from the Hugging Face Hub.
32
+
33
+ ```bash
34
+ pip install decido-ai
35
+ decido build-runtime # only on Linux / Intel Macs (one-time, needs cmake + a C++ compiler); Apple Silicon Macs: skip
36
+ ```
37
+ ```python
38
+ from decido import Decido
39
+
40
+ m = Decido("decido-ai/decido-1-fast-1.7b") # or decido-1-26b-a4b (most accurate on CPU), decido-1-ultra-0.6b (fastest)
41
+ m.decide("Customer: I was charged twice for order 4821. Please refund one of the charges today.",
42
+ {"refund": "Is the customer asking for a refund?",
43
+ "team": {"q": "Which team should handle this?", "options": ["billing", "shipping", "tech"]},
44
+ "urgency": {"q": "How urgent is it?", "scale": ["low", "normal", "urgent"]}})
45
+ ```
46
+ ```bash
47
+ decido models # the family, with scores and latency
48
+ decido run decido-ai/decido-1-fast-1.7b "Customer: charged twice" -q refund="Is a refund requested?"
49
+ decido serve decido-ai/decido-1-26b-a4b # Jev-compatible HTTP API on http://127.0.0.1:8080/v1/systemone
50
+ ```
51
+
52
+ | model | runs on | six-group score (Jev 1.13 = 83.85) | latency (M4 Pro CPU, 1k-token document, 5 questions) |
53
+ |---|---|---|---|
54
+ | [decido-1-26b-a4b](https://huggingface.co/decido-ai/decido-1-26b-a4b) | laptop CPU (~30 GB RAM) · GPU | 85.38 | 4.1 s |
55
+ | [decido-1-fast-1.7b](https://huggingface.co/decido-ai/decido-1-fast-1.7b) | laptop CPU | 73.01 | 0.64 s |
56
+ | [decido-1-ultra-0.6b](https://huggingface.co/decido-ai/decido-1-ultra-0.6b) | laptop CPU | 62.16 | 0.41 s |
57
+ | [decido-1-think-26b-a4b](https://huggingface.co/decido-ai/decido-1-think-26b-a4b) | GPU · laptop CPU (slow) | 88.71 (GPU) | see card |
58
+ | [decido-1-xl](https://huggingface.co/decido-ai/decido-1-xl) | 4× A100 80 GB | 87.25 (GPU) | — |
59
+ | [decido-1-pro](https://huggingface.co/decido-ai/decido-1-pro) | GPU | 89.57 (GPU) | — |
60
+
61
+ GPU extras: `pip install "decido-ai[gpu]"` (transformers), `"decido-ai[xl]"`, `"decido-ai[pro]"` (vLLM). Results, calibration,
62
+ disclosures and limitations are on each model card. License: Apache-2.0; the CPU runtime links llama.cpp (MIT).
@@ -0,0 +1,36 @@
1
+ # decido
2
+
3
+ **Calibrated decisions from a document — on a laptop CPU or a GPU.** Give Decido a document and questions (yes/no, pick-one,
4
+ or an ordered scale) and get a probability for every answer. This package runs the [Decido 1 models](https://huggingface.co/decido-ai)
5
+ from the Hugging Face Hub.
6
+
7
+ ```bash
8
+ pip install decido-ai
9
+ decido build-runtime # only on Linux / Intel Macs (one-time, needs cmake + a C++ compiler); Apple Silicon Macs: skip
10
+ ```
11
+ ```python
12
+ from decido import Decido
13
+
14
+ m = Decido("decido-ai/decido-1-fast-1.7b") # or decido-1-26b-a4b (most accurate on CPU), decido-1-ultra-0.6b (fastest)
15
+ m.decide("Customer: I was charged twice for order 4821. Please refund one of the charges today.",
16
+ {"refund": "Is the customer asking for a refund?",
17
+ "team": {"q": "Which team should handle this?", "options": ["billing", "shipping", "tech"]},
18
+ "urgency": {"q": "How urgent is it?", "scale": ["low", "normal", "urgent"]}})
19
+ ```
20
+ ```bash
21
+ decido models # the family, with scores and latency
22
+ decido run decido-ai/decido-1-fast-1.7b "Customer: charged twice" -q refund="Is a refund requested?"
23
+ decido serve decido-ai/decido-1-26b-a4b # Jev-compatible HTTP API on http://127.0.0.1:8080/v1/systemone
24
+ ```
25
+
26
+ | model | runs on | six-group score (Jev 1.13 = 83.85) | latency (M4 Pro CPU, 1k-token document, 5 questions) |
27
+ |---|---|---|---|
28
+ | [decido-1-26b-a4b](https://huggingface.co/decido-ai/decido-1-26b-a4b) | laptop CPU (~30 GB RAM) · GPU | 85.38 | 4.1 s |
29
+ | [decido-1-fast-1.7b](https://huggingface.co/decido-ai/decido-1-fast-1.7b) | laptop CPU | 73.01 | 0.64 s |
30
+ | [decido-1-ultra-0.6b](https://huggingface.co/decido-ai/decido-1-ultra-0.6b) | laptop CPU | 62.16 | 0.41 s |
31
+ | [decido-1-think-26b-a4b](https://huggingface.co/decido-ai/decido-1-think-26b-a4b) | GPU · laptop CPU (slow) | 88.71 (GPU) | see card |
32
+ | [decido-1-xl](https://huggingface.co/decido-ai/decido-1-xl) | 4× A100 80 GB | 87.25 (GPU) | — |
33
+ | [decido-1-pro](https://huggingface.co/decido-ai/decido-1-pro) | GPU | 89.57 (GPU) | — |
34
+
35
+ GPU extras: `pip install "decido-ai[gpu]"` (transformers), `"decido-ai[xl]"`, `"decido-ai[pro]"` (vLLM). Results, calibration,
36
+ disclosures and limitations are on each model card. License: Apache-2.0; the CPU runtime links llama.cpp (MIT).
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "decido-ai"
7
+ version = "1.0.0"
8
+ description = "Calibrated decisions (yes/no, pick-one, ordered scale) from a document — Decido models from the Hugging Face Hub, on a laptop CPU or a GPU."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "Apache-2.0"}
12
+ dependencies = ["numpy>=1.24", "tokenizers>=0.20", "huggingface_hub>=0.25"]
13
+
14
+ [project.optional-dependencies]
15
+ gpu = ["torch>=2.4", "transformers>=5.17", "accelerate"]
16
+ xl = ["torch>=2.6", "transformers>=5.22", "accelerate", "peft>=0.21", "triton>=3.2", "safetensors"]
17
+ pro = ["vllm==0.31.0", "transformers>=5.17"]
18
+
19
+ [project.scripts]
20
+ decido = "decido.cli:main"
21
+
22
+ [project.urls]
23
+ Homepage = "https://huggingface.co/decido-ai"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ decido = ["_bin/*", "csrc/*.cpp"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Decido: calibrated decisions (yes/no, pick-one, ordered scale) from a document. https://huggingface.co/decido-ai"""
2
+ from decido.api import Decido
3
+
4
+ __version__ = "1.0.0"
5
+ __all__ = ["Decido", "__version__"]
File without changes
File without changes
@@ -0,0 +1,42 @@
1
+ """Jev 1.13 wire answers + derived fields (formulas from research/jev-teardown.md §4), computed unrounded."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+
8
+ def choice_confidence(p) -> float:
9
+ n = len(p)
10
+ if n <= 1:
11
+ return 1.0
12
+ return float((n * max(p) - 1) / (n - 1))
13
+
14
+
15
+ def score_confidence(p) -> float:
16
+ p = np.asarray(p, dtype=float)
17
+ n = len(p)
18
+ if n <= 1:
19
+ return 1.0
20
+ m = int(np.argmax(p))
21
+ idx = np.arange(n)
22
+ mad_unif = float(np.mean(np.abs(idx - (n - 1) / 2)))
23
+ return float(max(0.0, 1.0 - float((p * np.abs(idx - m)).sum()) / mad_unif))
24
+
25
+
26
+ def wire_answer(q: dict, p, round_dp: int | None = None) -> dict:
27
+ """p is aligned with eval.parity.common.option_keys(q)."""
28
+ p = np.asarray(p, dtype=float)
29
+ r = (lambda x: round(float(x), round_dp)) if round_dp is not None else float
30
+ t = q["type"]
31
+ if t == "noul":
32
+ return {"type": "noul", "noul": r(p[1])}
33
+ if t == "choice":
34
+ keys = list(q["criteria"].keys())
35
+ return {"type": "choice", "choice": keys[int(np.argmax(p))],
36
+ "probabilities": {k: r(v) for k, v in zip(keys, p)}, "confidence": r(choice_confidence(p))}
37
+ if t == "score":
38
+ levels = list(q["criteria"])
39
+ return {"type": "score", "score": r(float((np.arange(len(p)) * p).sum())),
40
+ "legend": {str(i): levels[i] for i in range(len(levels))},
41
+ "probabilities": {str(i): r(v) for i, v in enumerate(p)}, "confidence": r(score_confidence(p))}
42
+ raise ValueError(t)
@@ -0,0 +1,85 @@
1
+ """DeepSeek-V4-Flash predictor for the parity harness and gold dev (GPU, HF eager forward via verdict.v4).
2
+
3
+ VERDICT_ALLOW_GPU=1 python -m eval.parity.run --allow-gpu --batch-size 2048 \
4
+ --predictor verdict.predictor_v4:VerdictV4 --predictor-kw adapter=runs/train/dsD/frac0.6-stepN
5
+
6
+ Every question is scored as a whole prompt (state prefix + one question [+ first label symbol for >26 options]); V4's
7
+ compressor cache is stateful, so there is no prefix reuse (cost = full re-prefill per question). Batches are
8
+ length-sorted under a padded-token and a padded B*L^2 budget. The LoRA adapter stays unmerged (peft), FP4 experts stay
9
+ packed. Throughput (prompts, tokens, tok/s) is appended to `stats` after every batch call.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import time
16
+
17
+ from decido._vendor.ds.readout import Readout
18
+ from decido._vendor.ds.template import ChatFormat
19
+
20
+
21
+ class VerdictV4:
22
+ def __init__(self, model: str = "deepseek-ai/DeepSeek-V4-Flash-0731", adapter: str | None = None, perm: int = 1,
23
+ temps=None, max_prompt_tokens: int = 8192, max_tokens: int = 32768, max_s2: int = 16_000_000,
24
+ attn: str = "band", on_overflow: str = "truncate", pipeline_mb: int = 8):
25
+ import torch
26
+ from decido._vendor.ds.v4.loader import load_v4
27
+ self.model, self.tok, self.info = load_v4(model, log=print)
28
+ if attn == "band":
29
+ from decido._vendor.ds.v4.attention import register
30
+ register(self.model)
31
+ if adapter:
32
+ from peft import PeftModel
33
+ from decido._vendor.ds.template import check_model_template
34
+ check_model_template(adapter, "verdict-ds-t1")
35
+ self.model = PeftModel.from_pretrained(self.model, adapter, is_trainable=False)
36
+ self.model.eval()
37
+ torch.set_grad_enabled(False)
38
+ if isinstance(temps, str) and temps:
39
+ temps = json.load(open(temps))
40
+ self.cf = ChatFormat(self.tok, family="deepseek_v4", version="verdict-ds-t1")
41
+ self.readout = Readout(self.cf, perm=perm, temps=temps or None, max_prompt_tokens=max_prompt_tokens,
42
+ on_overflow=on_overflow)
43
+ self.kw = {"max_tokens": max_tokens, "max_s2": max_s2, "pad_id": self.tok.pad_token_id or 0,
44
+ "pipeline_mb": int(pipeline_mb)}
45
+ self.stats = []
46
+
47
+ def _base(self):
48
+ m = self.model
49
+ return m.get_base_model() if hasattr(m, "get_base_model") else m
50
+
51
+ def predict_batch(self, requests):
52
+ from decido._vendor.ds.v4.score import score_plans
53
+ plans, out_idx, outs = [], [], [None] * len(requests)
54
+ for i, r in enumerate(requests):
55
+ try:
56
+ plans.append(self.readout.plan(r))
57
+ out_idx.append(i)
58
+ except ValueError as e:
59
+ outs[i] = {"__error__": str(e)[:200]}
60
+ t0 = time.time()
61
+ rec = {}
62
+ res = score_plans(self._base(), self.readout, plans, log=lambda m: rec.setdefault("log", m), **self.kw)
63
+ for i, pl, r in zip(out_idx, plans, res):
64
+ raw = r["answers"]
65
+ from decido._vendor.ds.derive import wire_answer
66
+ import numpy as np
67
+ resp = {"answers": {qid: wire_answer(pl.request["questions"][qid], np.asarray(p)) for qid, p in raw.items()}}
68
+ if "_logp" in r:
69
+ resp["_logp"] = r["_logp"]
70
+ if pl.truncated_tokens:
71
+ resp["truncated_state_tokens"] = pl.truncated_tokens
72
+ outs[i] = resp
73
+ self.stats.append({"requests": len(requests), "s": round(time.time() - t0, 2), **rec})
74
+ return outs
75
+
76
+ def predict(self, request):
77
+ return self.predict_batch([request])[0]
78
+
79
+ def predict_raw(self, requests):
80
+ """Raw (temperature-applied) distributions + pre-temperature log-probs, for gold dev / calib."""
81
+ from decido._vendor.ds.v4.score import score_plans
82
+ import os
83
+ os.environ["VERDICT_DUMP_DIST"] = "1"
84
+ plans = [self.readout.plan(r) for r in requests]
85
+ return score_plans(self._base(), self.readout, plans, **self.kw)
@@ -0,0 +1,175 @@
1
+ """Backend-agnostic verbalizer readout: request -> token queries -> calibrated distributions.
2
+
3
+ A backend only has to answer two kinds of query on (prefix_ids + suffix_ids [+ extra_ids]):
4
+ "log-probabilities of these token ids at the next position".
5
+ Everything else (labels, variants, permutation averaging, temperature, wire format) lives here so the
6
+ vLLM, HF and llama.cpp paths produce the same numbers up to backend numerics.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import os
13
+ from dataclasses import dataclass, field
14
+
15
+ import numpy as np
16
+
17
+ from decido._vendor.ds.derive import wire_answer
18
+ from decido._vendor.ds.template import LabelTrie
19
+
20
+
21
+ def _lse(xs):
22
+ m = max(xs)
23
+ if m == -math.inf:
24
+ return -math.inf
25
+ return m + math.log(sum(math.exp(x - m) for x in xs))
26
+
27
+
28
+ def request_key(request: dict) -> str:
29
+ import hashlib
30
+ import json
31
+ return hashlib.sha256(json.dumps({"state": request["state"], "questions": request["questions"]}, sort_keys=True,
32
+ ensure_ascii=False).encode()).hexdigest()[:32]
33
+
34
+
35
+ def cyclic_orders(k: int, m: int) -> list[list[int]]:
36
+ m = max(1, min(m, k))
37
+ shifts = sorted({round(j * k / m) % k for j in range(m)})
38
+ return [[(i + s) % k for i in range(k)] for s in shifts]
39
+
40
+
41
+ @dataclass
42
+ class Query:
43
+ qid: str
44
+ order: list[int]
45
+ suffix_ids: list[int]
46
+ trie: LabelTrie
47
+ step1: dict = field(default_factory=dict) # token id -> logprob
48
+ step2: dict = field(default_factory=dict) # first symbol -> {token id -> logprob}
49
+
50
+
51
+ @dataclass
52
+ class Plan:
53
+ request: dict
54
+ prefix_ids: list[int]
55
+ queries: list[Query]
56
+ truncated_tokens: int = 0
57
+
58
+
59
+ class Readout:
60
+ def __init__(self, chat_format, perm: int = 1, temps: dict | None = None, max_prompt_tokens: int = 16000,
61
+ on_overflow: str = "error"):
62
+ self.cf = chat_format
63
+ self.tok = chat_format.tok
64
+ self.perm = int(perm)
65
+ self.temps = temps or {}
66
+ self.max_prompt_tokens = max_prompt_tokens
67
+ self.on_overflow = on_overflow # "error" (Jev-like max_tokens_exceeded) or "truncate" (flagged)
68
+ self._trie_cache = {}
69
+ self._enc_cache = {}
70
+
71
+ def trie(self, labels: list[str]) -> LabelTrie:
72
+ key = tuple(labels)
73
+ if key not in self._trie_cache:
74
+ self._trie_cache[key] = LabelTrie(self.tok, labels)
75
+ return self._trie_cache[key]
76
+
77
+ def _enc(self, text: str) -> list[int]:
78
+ ids = self._enc_cache.get(text)
79
+ if ids is None:
80
+ ids = self.cf.encode(text)
81
+ if len(self._enc_cache) < 200000:
82
+ self._enc_cache[text] = ids
83
+ return ids
84
+
85
+ @staticmethod
86
+ def validate(request: dict):
87
+ """Jev request rules (research/jev-teardown.md §2.2): choice 2-255 options, score 2-10 levels."""
88
+ qs = request.get("questions")
89
+ if not isinstance(qs, dict) or not qs:
90
+ raise ValueError("invalid_request: questions must be a non-empty map")
91
+ for qid, q in qs.items():
92
+ t = q.get("type")
93
+ if t not in ("noul", "choice", "score"):
94
+ raise ValueError(f"invalid_request: question {qid!r} has unsupported type {t!r}")
95
+ if q.get("instructions") in (None, ""):
96
+ raise ValueError(f"invalid_request: question {qid!r} needs instructions")
97
+ if t == "choice" and not (isinstance(q.get("criteria"), dict) and 2 <= len(q["criteria"]) <= 255):
98
+ raise ValueError(f"invalid_request: choice {qid!r} needs 2-255 options")
99
+ if t == "score" and not (isinstance(q.get("criteria"), list) and 2 <= len(q["criteria"]) <= 10):
100
+ raise ValueError(f"invalid_request: score {qid!r} needs 2-10 levels")
101
+
102
+ def plan(self, request: dict, perm: int | None = None) -> Plan:
103
+ self.validate(request)
104
+ perm = self.perm if perm is None else perm
105
+ prefix_ids = self.cf.encode(self.cf.prefix_text(request["state"]))
106
+ queries = []
107
+ max_suffix = 0
108
+ for qid, q in request["questions"].items():
109
+ orders = [None]
110
+ if q["type"] == "choice" and perm > 1:
111
+ orders = cyclic_orders(len(q["criteria"]), perm)
112
+ for order in orders:
113
+ text, labels, order_used = self.cf.qblock(q, order)
114
+ sids = self._enc(self.cf.suffix_text(text))
115
+ max_suffix = max(max_suffix, len(sids))
116
+ queries.append(Query(qid, order_used, sids, self.trie(labels)))
117
+ budget = self.max_prompt_tokens - max_suffix - 4
118
+ truncated = 0
119
+ if len(prefix_ids) > budget:
120
+ if self.on_overflow != "truncate":
121
+ raise ValueError(f"max_tokens_exceeded: state+question needs {len(prefix_ids) + max_suffix} tokens, "
122
+ f"limit {self.max_prompt_tokens}")
123
+ closing = self.cf.encode("\n</document>\n\n") # keep the head of the state; flagged in the response
124
+ truncated = len(prefix_ids) - max(budget - len(closing), 16) - len(closing)
125
+ prefix_ids = prefix_ids[:max(budget - len(closing), 16)] + closing
126
+ pl = Plan(request, prefix_ids, queries)
127
+ pl.truncated_tokens = truncated
128
+ return pl
129
+
130
+ # ---------------------------------------------------------------- combination
131
+
132
+ def label_logprobs(self, qu: Query) -> np.ndarray:
133
+ tr = qu.trie
134
+ if not tr.two_step:
135
+ s = [_lse([qu.step1.get(t, -math.inf) for t in tr.first_variants[sym]]) for sym in tr.labels]
136
+ else:
137
+ s = []
138
+ for lab in tr.labels:
139
+ a = _lse([qu.step1.get(t, -math.inf) for t in tr.first_variants[lab[0]]])
140
+ b = _lse([qu.step2.get(lab[0], {}).get(t, -math.inf) for t in tr.second_variants[lab[1]]])
141
+ s.append(a + b)
142
+ s = np.array(s, dtype=float)
143
+ if not np.isfinite(s).any():
144
+ s = np.zeros_like(s)
145
+ s = np.where(np.isfinite(s), s, -1e4)
146
+ return s - (s.max() + np.log(np.exp(s - s.max()).sum()))
147
+
148
+ def combine(self, plan: Plan, round_dp: int | None = None, raw: bool = False) -> dict:
149
+ req = plan.request
150
+ by_q: dict[str, list[np.ndarray]] = {}
151
+ for qu in plan.queries:
152
+ lp_disp = self.label_logprobs(qu) # display positions
153
+ lp = np.empty_like(lp_disp)
154
+ lp[np.array(qu.order)] = lp_disp # -> option_keys order
155
+ by_q.setdefault(qu.qid, []).append(lp)
156
+ answers, logp = {}, {}
157
+ for qid, q in req["questions"].items():
158
+ z = np.mean(by_q[qid], axis=0)
159
+ logp[qid] = z.tolist()
160
+ T = float(self.temps.get(q["type"], 1.0))
161
+ z = z / T
162
+ p = np.exp(z - z.max())
163
+ p /= p.sum()
164
+ answers[qid] = p.tolist() if raw else wire_answer(q, p, round_dp)
165
+ out = {"answers": answers}
166
+ if os.environ.get("VERDICT_DUMP_DIST") == "1": # eval/parity/run.py --dump-dist: pre-temperature log-probs
167
+ out["_logp"] = logp
168
+ cache = os.environ.get("VERDICT_LOGP_CACHE")
169
+ if cache: # request-content-keyed log-probs (verdict.replay rebuilds ensembles for any request, incl. flips)
170
+ import json
171
+ with open(cache, "a") as fh:
172
+ fh.write(json.dumps({"h": request_key(req), "logp": logp}) + "\n")
173
+ if plan.truncated_tokens:
174
+ out["truncated_state_tokens"] = plan.truncated_tokens
175
+ return out