Q1z commited on
Commit
2d8be88
·
verified ·
1 Parent(s): 9a94bbf

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: LiquidAI/LFM2.5-Encoder-350M
3
+ base_model_relation: finetune
4
+ library_name: transformers
5
+ tags:
6
+ - safetensors
7
+ - custom_code
8
+ - decision-making
9
+ - classification
10
+ - routing
11
+ - scoring
12
+ ---
13
+ # Pivot
14
+
15
+ Pivot is a decision model developed by **Q1z**. It scores a supplied set of
16
+ options and returns a choice with probabilities. It does not generate chat text.
17
+
18
+ Built on [LiquidAI/LFM2.5-Encoder-350M](https://huggingface.co/LiquidAI/LFM2.5-Encoder-350M)
19
+ with a trained DSBT option-set scorer. Context and options are encoded separately,
20
+ mean-pooled, scored together, and normalized over the valid options.
21
+
22
+ ## Model details
23
+
24
+ - Parameters: 357,631,745
25
+ - Weight types: float32
26
+ - Format: Safetensors, full model including the decision scorer
27
+ - Outputs: choice, noul (Boolean choice), and discrete score distributions
28
+ - Base revision: `b886781f7c6f10ca9b7096e21b83e30a073c2f39`
29
+ - Saved checkpoint: epoch index 0, global step 3102
30
+ - Export verification: exact weight round-trip and typed output agreement PASS
31
+
32
+ ## Quick start
33
+
34
+ Install `torch`, `transformers`, `safetensors`, and `numpy` using the versions
35
+ recorded in `requirements.txt`. Review the bundled custom code before trusting it.
36
+
37
+ ```python
38
+ from transformers import AutoModel, AutoTokenizer
39
+
40
+ repo = "Q1z/Pivot"
41
+ tokenizer = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
42
+ model = AutoModel.from_pretrained(repo, trust_remote_code=True).eval()
43
+ result = model.choose(tokenizer,
44
+ context="My invoice has the wrong total.",
45
+ options=["billing", "technical support", "sales"])
46
+ print(result)
47
+ ```
48
+
49
+ For typed questions use `model.decide(tokenizer, state=..., questions=...)`;
50
+ see `serving/example_request.json` and `predict_example.py`.
51
+ CPU is supported. For CUDA, call `model.to("cuda")` before inference.
52
+ After downloading the complete repository, use its local path with
53
+ `local_files_only=True`; no base-model weights are fetched during loading.
54
+ Private repository access requires an authorized Hugging Face token.
55
+
56
+ ## Evaluation and limitations
57
+
58
+ JevBench results will be added after testing. This export runs no full benchmark.
59
+ The smoke test verifies loading and output structure, not accuracy or calibration.
60
+ Probabilities have not been independently validated for calibration.
61
+ Multilingual quality and speed are not yet benchmarked. Supplied options determine
62
+ the available answers; this is not an open-ended text generator.
63
+
64
+ ## Package
65
+
66
+ `model.safetensors` is the only weight file. `config.json`, tokenizer files, and
67
+ bundled Python modules implement loading and inference. `dsbt_config.yaml` records
68
+ the saved training configuration. `provenance.json` records the checkpoint hash.
69
+ Training backups and optimizer state are not included. Keep the source best.pt
70
+ separately if further training is needed. ONNX is not exported.
71
+
72
+ Project: https://trypivot.me
config.json ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "pivot",
3
+ "architectures": [
4
+ "PivotModel"
5
+ ],
6
+ "auto_map": {
7
+ "AutoConfig": "configuration_pivot.PivotConfig",
8
+ "AutoModel": "modeling_pivot.PivotModel"
9
+ },
10
+ "dsbt_config": {
11
+ "backbone": {
12
+ "encoder": "huggingface",
13
+ "model_id": "LiquidAI/LFM2.5-Encoder-350M",
14
+ "revision": "b886781f7c6f10ca9b7096e21b83e30a073c2f39",
15
+ "trust_remote_code": true,
16
+ "pooling": "mean",
17
+ "hidden_size": 1024,
18
+ "max_position_embeddings": 8192,
19
+ "stub_hidden_size": 32,
20
+ "stub_vocab_size": 256
21
+ },
22
+ "scorer": {
23
+ "type": "mlp",
24
+ "hidden_size": 1024,
25
+ "dropout": 0.0
26
+ },
27
+ "data": {
28
+ "k_min": 2,
29
+ "k_max": 16,
30
+ "max_context_tokens": 512,
31
+ "max_option_tokens": 64,
32
+ "noul_true": "true",
33
+ "noul_false": "false",
34
+ "split_ratios": [
35
+ 0.8,
36
+ 0.1,
37
+ 0.1
38
+ ],
39
+ "family_sample_caps": {
40
+ "casehold": 0.15
41
+ }
42
+ },
43
+ "train": {
44
+ "epochs": 4,
45
+ "warmup_epochs": 2,
46
+ "batch_size": 32,
47
+ "grad_accum": 1,
48
+ "lr_encoder": 1e-05,
49
+ "lr_scorer": 5e-05,
50
+ "weight_decay": 0.01,
51
+ "grad_clip": 1.0,
52
+ "precision": "bf16",
53
+ "gradient_checkpointing": true,
54
+ "seed": 42,
55
+ "decoupling": "pcgrad",
56
+ "log_every": 20,
57
+ "num_workers": 4,
58
+ "early_stop_patience": 2,
59
+ "checkpoint_dir": "checkpoints",
60
+ "log_dir": "logs",
61
+ "lora": {
62
+ "enabled": false,
63
+ "r": 16,
64
+ "alpha": 32,
65
+ "dropout": 0.05,
66
+ "target_modules": [
67
+ "q_proj",
68
+ "k_proj",
69
+ "v_proj",
70
+ "out_proj",
71
+ "in_proj"
72
+ ]
73
+ }
74
+ },
75
+ "eval": {
76
+ "k_strata": [
77
+ 2,
78
+ 4,
79
+ 8,
80
+ 16
81
+ ],
82
+ "option_mode": "resample_strata",
83
+ "ece_bins": 15,
84
+ "calibration_epsilon": 0.02,
85
+ "temperature_scaling": false,
86
+ "jev_baseline": null,
87
+ "latency_batches": 20,
88
+ "require_latency_vs_jev": true,
89
+ "win_on_overall_acc_only": false,
90
+ "mid_epoch_max_examples": 4096,
91
+ "full_frozen_eval_only_at_end": true,
92
+ "frozen_eval_path": null
93
+ },
94
+ "modal_contract": {
95
+ "softmax_dtype": "fp32",
96
+ "casehold_cap": 0.15,
97
+ "forbid_jev_softlabel_distill": true,
98
+ "win_requires": [
99
+ "k_stratified_acc_brier_ece",
100
+ "latency_p50_p95_vs_jev",
101
+ "live_brier_pcgrad",
102
+ "casehold_sample_cap_le_015"
103
+ ],
104
+ "gpu": "H100",
105
+ "expected_steps_batch64": "16878\u201322504 for 3\u20134 epochs; early-stop may cut short",
106
+ "budget_note": "~$8\u201316 expected at $3.95/h for 2\u20134h; keep \u2265$8 reserve"
107
+ },
108
+ "brier": {
109
+ "reduction": "mean_over_real_options"
110
+ }
111
+ },
112
+ "encoder_config": {
113
+ "transformers_version": "5.17.0",
114
+ "architectures": [
115
+ "Lfm2BidirectionalForMaskedLM"
116
+ ],
117
+ "output_hidden_states": false,
118
+ "return_dict": true,
119
+ "dtype": "float32",
120
+ "chunk_size_feed_forward": 0,
121
+ "is_encoder_decoder": false,
122
+ "id2label": {
123
+ "0": "LABEL_0",
124
+ "1": "LABEL_1"
125
+ },
126
+ "label2id": {
127
+ "LABEL_0": 0,
128
+ "LABEL_1": 1
129
+ },
130
+ "problem_type": null,
131
+ "vocab_size": 65536,
132
+ "hidden_size": 1024,
133
+ "intermediate_size": 6656,
134
+ "num_hidden_layers": 16,
135
+ "num_attention_heads": 16,
136
+ "num_key_value_heads": 8,
137
+ "max_position_embeddings": 128000,
138
+ "initializer_range": 0.02,
139
+ "norm_eps": 1e-05,
140
+ "use_cache": false,
141
+ "pad_token_id": 0,
142
+ "bos_token_id": 1,
143
+ "eos_token_id": 7,
144
+ "tie_word_embeddings": true,
145
+ "rope_parameters": {
146
+ "rope_theta": 1000000.0,
147
+ "rope_type": "default"
148
+ },
149
+ "conv_bias": false,
150
+ "conv_L_cache": 3,
151
+ "block_multiple_of": 256,
152
+ "block_ffn_dim_multiplier": 1.0,
153
+ "block_auto_adjust_ff_dim": true,
154
+ "full_attn_idxs": null,
155
+ "layer_types": [
156
+ "conv",
157
+ "conv",
158
+ "full_attention",
159
+ "conv",
160
+ "conv",
161
+ "full_attention",
162
+ "conv",
163
+ "conv",
164
+ "full_attention",
165
+ "conv",
166
+ "full_attention",
167
+ "conv",
168
+ "full_attention",
169
+ "conv",
170
+ "full_attention",
171
+ "conv"
172
+ ],
173
+ "_name_or_path": "LiquidAI/LFM2.5-Encoder-350M",
174
+ "block_dim": 1024,
175
+ "block_mlp_init_scale": 1.0,
176
+ "block_norm_eps": 1e-05,
177
+ "block_out_init_scale": 1.0,
178
+ "block_use_swiglu": true,
179
+ "block_use_xavier_init": true,
180
+ "conv_dim": 1024,
181
+ "conv_dim_out": 1024,
182
+ "conv_use_xavier_init": true,
183
+ "model_type": "lfm2",
184
+ "num_heads": 16,
185
+ "use_pos_enc": true,
186
+ "auto_map": {
187
+ "AutoModel": "modeling_lfm2_bidirectional.Lfm2BidirectionalModel",
188
+ "AutoModelForMaskedLM": "modeling_lfm2_bidirectional.Lfm2BidirectionalForMaskedLM"
189
+ },
190
+ "output_attentions": false
191
+ }
192
+ }
configuration_pivot.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration for the Pivot set-scored decision encoder."""
2
+ from transformers import PretrainedConfig
3
+
4
+
5
+ class PivotConfig(PretrainedConfig):
6
+ model_type = "pivot"
7
+
8
+ def __init__(self, dsbt_config=None, encoder_config=None, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.dsbt_config = dsbt_config or {}
11
+ self.encoder_config = encoder_config or {}
dsbt_config.yaml ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ backbone:
2
+ encoder: huggingface
3
+ model_id: LiquidAI/LFM2.5-Encoder-350M
4
+ revision: b886781f7c6f10ca9b7096e21b83e30a073c2f39
5
+ trust_remote_code: true
6
+ pooling: mean
7
+ hidden_size: 1024
8
+ max_position_embeddings: 8192
9
+ stub_hidden_size: 32
10
+ stub_vocab_size: 256
11
+ scorer:
12
+ type: mlp
13
+ hidden_size: 1024
14
+ dropout: 0.0
15
+ data:
16
+ k_min: 2
17
+ k_max: 16
18
+ max_context_tokens: 512
19
+ max_option_tokens: 64
20
+ noul_true: 'true'
21
+ noul_false: 'false'
22
+ split_ratios:
23
+ - 0.8
24
+ - 0.1
25
+ - 0.1
26
+ family_sample_caps:
27
+ casehold: 0.15
28
+ train:
29
+ epochs: 4
30
+ warmup_epochs: 2
31
+ batch_size: 32
32
+ grad_accum: 1
33
+ lr_encoder: 1.0e-05
34
+ lr_scorer: 5.0e-05
35
+ weight_decay: 0.01
36
+ grad_clip: 1.0
37
+ precision: bf16
38
+ gradient_checkpointing: true
39
+ seed: 42
40
+ decoupling: pcgrad
41
+ log_every: 20
42
+ num_workers: 4
43
+ early_stop_patience: 2
44
+ checkpoint_dir: checkpoints
45
+ log_dir: logs
46
+ lora:
47
+ enabled: false
48
+ r: 16
49
+ alpha: 32
50
+ dropout: 0.05
51
+ target_modules:
52
+ - q_proj
53
+ - k_proj
54
+ - v_proj
55
+ - out_proj
56
+ - in_proj
57
+ eval:
58
+ k_strata:
59
+ - 2
60
+ - 4
61
+ - 8
62
+ - 16
63
+ option_mode: resample_strata
64
+ ece_bins: 15
65
+ calibration_epsilon: 0.02
66
+ temperature_scaling: false
67
+ jev_baseline: null
68
+ latency_batches: 20
69
+ require_latency_vs_jev: true
70
+ win_on_overall_acc_only: false
71
+ mid_epoch_max_examples: 4096
72
+ full_frozen_eval_only_at_end: true
73
+ frozen_eval_path: null
74
+ modal_contract:
75
+ softmax_dtype: fp32
76
+ casehold_cap: 0.15
77
+ forbid_jev_softlabel_distill: true
78
+ win_requires:
79
+ - k_stratified_acc_brier_ece
80
+ - latency_p50_p95_vs_jev
81
+ - live_brier_pcgrad
82
+ - casehold_sample_cap_le_015
83
+ gpu: H100
84
+ expected_steps_batch64: "16878\u201322504 for 3\u20134 epochs; early-stop may cut\
85
+ \ short"
86
+ budget_note: "~$8\u201316 expected at $3.95/h for 2\u20134h; keep \u2265$8 reserve"
87
+ brier:
88
+ reduction: mean_over_real_options
evaluation_status.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "status": "skipped",
3
+ "reason": "user_requested_export_only"
4
+ }
export_verification.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "package_format": "pivot-hf-v1",
3
+ "reload_verified": true,
4
+ "parameters": 357631745,
5
+ "tensor_dtypes": [
6
+ "float32"
7
+ ]
8
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8bda6d789bbd7e136ab870db3a5ce060bdc299ea87c3302dce4dffff7707fcc1
3
+ size 1430546228
modeling_pivot.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pivot: load full fine-tuned weights without fetching base-model weights."""
2
+ import torch
3
+ from torch import nn
4
+ from transformers import AutoConfig, AutoModel, PreTrainedModel
5
+ from .configuration_pivot import PivotConfig
6
+ from .pivot_model import (SetBrierEncoder, StubEncoder, HuggingFaceEncoder,
7
+ MLPScorer, DotScorer, _wrap_lora)
8
+ from .pivot_infer import decide_typed, predict
9
+ from .pivot_data import DecisionCollator
10
+ from .pivot_serving_schema import build_response
11
+
12
+
13
+ class PivotModel(PreTrainedModel):
14
+ config_class = PivotConfig
15
+ base_model_prefix = "network"
16
+ _no_split_modules = ["SetBrierEncoder"]
17
+
18
+ def __init__(self, config):
19
+ super().__init__(config)
20
+ cfg = config.dsbt_config
21
+ backbone = cfg["backbone"]
22
+ if backbone.get("encoder") == "stub":
23
+ encoder = StubEncoder(int(backbone.get("stub_vocab_size", 256)),
24
+ int(backbone.get("stub_hidden_size", 32)))
25
+ else:
26
+ body_config = dict(config.encoder_config)
27
+ model_type = body_config.pop("model_type")
28
+ body = AutoModel.from_config(AutoConfig.for_model(model_type, **body_config))
29
+ encoder = HuggingFaceEncoder.__new__(HuggingFaceEncoder)
30
+ nn.Module.__init__(encoder)
31
+ encoder.hidden_size = int(body.config.hidden_size)
32
+ lora = cfg["train"].get("lora")
33
+ encoder.model = _wrap_lora(body, lora) if lora and lora.get("enabled") else body
34
+ hidden = encoder.hidden_size
35
+ sc = cfg["scorer"]
36
+ if sc.get("type", "mlp") == "mlp":
37
+ scorer = MLPScorer(hidden, int(sc.get("hidden_size", hidden)), float(sc.get("dropout", 0)))
38
+ elif sc["type"] == "dot":
39
+ scorer = DotScorer(hidden)
40
+ else:
41
+ raise ValueError("Unsupported scorer")
42
+ self.network = SetBrierEncoder(encoder, scorer)
43
+ self.post_init()
44
+
45
+ def forward(self, ctx_ids, ctx_mask, opt_ids, opt_mask, opt_attn):
46
+ return self.network(ctx_ids, ctx_mask, opt_ids, opt_mask, opt_attn)
47
+
48
+ @torch.no_grad()
49
+ def decide(self, tokenizer, state, questions):
50
+ self.eval()
51
+ data = self.config.dsbt_config["data"]
52
+ return decide_typed(self.network, tokenizer, state, questions,
53
+ max_context_tokens=int(data["max_context_tokens"]),
54
+ max_option_tokens=int(data["max_option_tokens"]),
55
+ device=next(self.parameters()).device, model_id="Pivot")
56
+
57
+ @torch.no_grad()
58
+ def choose(self, tokenizer, context, options):
59
+ self.eval()
60
+ data = self.config.dsbt_config["data"]
61
+ return predict(self.network, tokenizer, context, options,
62
+ max_context_tokens=int(data["max_context_tokens"]),
63
+ max_option_tokens=int(data["max_option_tokens"]),
64
+ device=next(self.parameters()).device)
pivot_data.py ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """JSONL dataset, dynamic-K set augmentation, masks, and collate (§5).
2
+
3
+ Each record:
4
+
5
+ {
6
+ "context": str,
7
+ "options": [str, ...],
8
+ "label": int,
9
+ "task_type": "choice" | "noul" | "score",
10
+ "meta": {"domain": ..., "source_family": ..., "pair_id": ..., "hard_negatives": [...]}
11
+ }
12
+
13
+ On every training step (§5.3): sample K, keep gold, add hard negatives, fill
14
+ distractors, shuffle option order, remap ``label`` to the new gold index, pad
15
+ to Kmax with opt_mask=0.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import random
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Any, Iterator, Optional, Sequence
26
+
27
+ import torch
28
+ from torch.utils.data import Dataset
29
+
30
+ TASK_TYPES = {"choice", "noul", "score"}
31
+
32
+
33
+ @dataclass
34
+ class DecisionExample:
35
+ context: str
36
+ options: list[str]
37
+ label: int
38
+ task_type: str
39
+ meta: dict[str, Any] = field(default_factory=dict)
40
+
41
+ @property
42
+ def gold_text(self) -> str:
43
+ return self.options[self.label]
44
+
45
+ @property
46
+ def source_family(self) -> str:
47
+ fam = self.meta.get("source_family")
48
+ if fam:
49
+ return str(fam)
50
+ # Unique singleton family so untagged rows cannot leak across splits.
51
+ key = json.dumps([self.context, self.options, self.label], ensure_ascii=True)
52
+ digest = hashlib.md5(key.encode("utf-8")).hexdigest()[:16]
53
+ return f"singleton::{digest}"
54
+
55
+ @property
56
+ def hard_negatives(self) -> list[str]:
57
+ raw = self.meta.get("hard_negatives") or []
58
+ return [str(x) for x in raw]
59
+
60
+
61
+ def parse_record(obj: dict[str, Any], *, noul_true: str = "true", noul_false: str = "false") -> DecisionExample:
62
+ if "context" not in obj or "label" not in obj:
63
+ raise ValueError("record requires 'context' and 'label'")
64
+ task_type = str(obj.get("task_type", "choice"))
65
+ if task_type not in TASK_TYPES:
66
+ raise ValueError(f"task_type must be one of {sorted(TASK_TYPES)}")
67
+ options = obj.get("options")
68
+ if not options:
69
+ if task_type == "noul":
70
+ options = [noul_true, noul_false]
71
+ else:
72
+ raise ValueError("record requires a non-empty options list")
73
+ options = [str(x) for x in options]
74
+ label = int(obj["label"])
75
+ if not 0 <= label < len(options):
76
+ raise ValueError(f"label {label} out of range for {len(options)} options")
77
+ meta = dict(obj.get("meta") or {})
78
+ return DecisionExample(
79
+ context=str(obj["context"]),
80
+ options=options,
81
+ label=label,
82
+ task_type=task_type,
83
+ meta=meta,
84
+ )
85
+
86
+
87
+ def load_jsonl(
88
+ path: str | Path,
89
+ *,
90
+ noul_true: str = "true",
91
+ noul_false: str = "false",
92
+ ) -> list[DecisionExample]:
93
+ path = Path(path)
94
+ rows: list[DecisionExample] = []
95
+ with path.open("r", encoding="utf-8") as f:
96
+ for line_no, line in enumerate(f, 1):
97
+ line = line.strip()
98
+ if not line:
99
+ continue
100
+ try:
101
+ obj = json.loads(line)
102
+ except json.JSONDecodeError as exc:
103
+ raise ValueError(f"{path}:{line_no} invalid JSON") from exc
104
+ rows.append(parse_record(obj, noul_true=noul_true, noul_false=noul_false))
105
+ return rows
106
+
107
+
108
+ def split_by_source_family(
109
+ examples: Sequence[DecisionExample],
110
+ *,
111
+ seed: int,
112
+ ratios: Sequence[float] = (0.8, 0.1, 0.1),
113
+ ) -> tuple[list[DecisionExample], list[DecisionExample], list[DecisionExample]]:
114
+ """Assign whole source families to train / val / holdout (no family leakage)."""
115
+ if len(ratios) != 3:
116
+ raise ValueError("ratios must be (train, val, holdout)")
117
+ families: dict[str, list[DecisionExample]] = {}
118
+ for ex in examples:
119
+ families.setdefault(ex.source_family, []).append(ex)
120
+ keys = sorted(families)
121
+ rng = random.Random(seed)
122
+ rng.shuffle(keys)
123
+ n = len(keys)
124
+ n_train = int(round(n * ratios[0]))
125
+ n_val = int(round(n * ratios[1]))
126
+ n_train = min(n_train, n)
127
+ n_val = min(n_val, n - n_train)
128
+ train_k = keys[:n_train]
129
+ val_k = keys[n_train : n_train + n_val]
130
+ hold_k = keys[n_train + n_val :]
131
+ if not train_k and keys:
132
+ train_k, val_k, hold_k = keys[:1], keys[1:2], keys[2:]
133
+ def _take(ks: Sequence[str]) -> list[DecisionExample]:
134
+ out: list[DecisionExample] = []
135
+ for k in ks:
136
+ out.extend(families[k])
137
+ return out
138
+ return _take(train_k), _take(val_k), _take(hold_k)
139
+
140
+
141
+
142
+ def family_counts(examples: Sequence[DecisionExample]) -> dict[str, int]:
143
+ counts: dict[str, int] = {}
144
+ for ex in examples:
145
+ fam = ex.source_family
146
+ counts[fam] = counts.get(fam, 0) + 1
147
+ return counts
148
+
149
+
150
+ def compute_family_sample_weights(
151
+ examples: Sequence[DecisionExample],
152
+ caps: dict[str, float] | None,
153
+ ) -> list[float]:
154
+ """Per-example sample weights so capped families have train mass ≤ cap.
155
+
156
+ For a single capped family with raw fraction ``f > cap``, each of its rows
157
+ gets weight ``cap * (N - n_f) / (n_f * (1 - cap))`` so that
158
+ ``P(family) = cap`` under WeightedRandomSampler (with replacement).
159
+ Uncapped rows keep weight 1. Multiple caps are applied independently on the
160
+ uncapped pool (greedy): each capped family is solved against the current
161
+ non-capped mass. Empty caps → all ones.
162
+ """
163
+ n = len(examples)
164
+ if n == 0:
165
+ return []
166
+ weights = [1.0] * n
167
+ if not caps:
168
+ return weights
169
+ counts = family_counts(examples)
170
+ # Apply stricter caps first.
171
+ ordered = sorted(
172
+ ((str(fam), float(cap)) for fam, cap in caps.items() if float(cap) > 0),
173
+ key=lambda x: x[1],
174
+ )
175
+ for fam, cap in ordered:
176
+ if not 0 < cap < 1:
177
+ raise ValueError(f"family_sample_caps[{fam!r}]={cap} must be in (0, 1)")
178
+ n_f = counts.get(fam, 0)
179
+ if n_f == 0:
180
+ continue
181
+ frac = n_f / n
182
+ if frac <= cap:
183
+ continue
184
+ # Exact single-family solution vs current weight-1 mass outside the family.
185
+ outside = sum(weights[i] for i, ex in enumerate(examples) if ex.source_family != fam)
186
+ # P = (n_f * w) / (n_f * w + outside) = cap => w = cap * outside / (n_f * (1-cap))
187
+ w = (cap * outside) / (n_f * (1.0 - cap))
188
+ for i, ex in enumerate(examples):
189
+ if ex.source_family == fam:
190
+ weights[i] = float(w)
191
+ return weights
192
+
193
+
194
+ def expected_family_mass(weights: Sequence[float], examples: Sequence[DecisionExample], family: str) -> float:
195
+ """Expected draw fraction for ``family`` under the given sample weights."""
196
+ total = float(sum(weights))
197
+ if total <= 0:
198
+ return 0.0
199
+ mass = sum(w for w, ex in zip(weights, examples) if ex.source_family == family)
200
+ return mass / total
201
+
202
+
203
+ def build_option_pool(examples: Sequence[DecisionExample]) -> list[str]:
204
+ seen: set[str] = set()
205
+ pool: list[str] = []
206
+ for ex in examples:
207
+ for opt in ex.options:
208
+ if opt not in seen:
209
+ seen.add(opt)
210
+ pool.append(opt)
211
+ for hn in ex.hard_negatives:
212
+ if hn not in seen:
213
+ seen.add(hn)
214
+ pool.append(hn)
215
+ if not pool:
216
+ pool = ["true", "false", "unknown", "not applicable"]
217
+ return pool
218
+
219
+
220
+ def build_pair_gold_index(examples: Sequence[DecisionExample]) -> dict[str, list[str]]:
221
+ """Map pair_id -> gold texts of siblings (contrastive flips)."""
222
+ idx: dict[str, list[str]] = {}
223
+ for ex in examples:
224
+ pid = ex.meta.get("pair_id")
225
+ if not pid:
226
+ continue
227
+ idx.setdefault(str(pid), []).append(ex.gold_text)
228
+ return idx
229
+
230
+
231
+ @dataclass
232
+ class AugmentedSet:
233
+ options: list[str]
234
+ label: int
235
+ k: int
236
+
237
+
238
+ class SetAugmenter:
239
+ """On-the-fly variable-K option sets with shuffle + label remap."""
240
+
241
+ def __init__(
242
+ self,
243
+ *,
244
+ k_min: int,
245
+ k_max: int,
246
+ pool: Sequence[str],
247
+ pair_golds: Optional[dict[str, list[str]]] = None,
248
+ ):
249
+ if k_min < 2:
250
+ raise ValueError("k_min must be >= 2")
251
+ if k_max < k_min:
252
+ raise ValueError("k_max must be >= k_min")
253
+ self.k_min = k_min
254
+ self.k_max = k_max
255
+ self.pool = list(pool)
256
+ self.pair_golds = pair_golds or {}
257
+
258
+ def sample_k(self, rng: random.Random) -> int:
259
+ return rng.randint(self.k_min, self.k_max)
260
+
261
+ def _hard_negatives(self, example: DecisionExample) -> list[str]:
262
+ gold = example.gold_text
263
+ out: list[str] = []
264
+ seen = {gold}
265
+ for text in example.hard_negatives:
266
+ if text not in seen:
267
+ out.append(text)
268
+ seen.add(text)
269
+ pid = example.meta.get("pair_id")
270
+ if pid:
271
+ for text in self.pair_golds.get(str(pid), []):
272
+ if text not in seen:
273
+ out.append(text)
274
+ seen.add(text)
275
+ return out
276
+
277
+ def build(
278
+ self,
279
+ example: DecisionExample,
280
+ rng: random.Random,
281
+ k: Optional[int] = None,
282
+ ) -> AugmentedSet:
283
+ k = int(k if k is not None else self.sample_k(rng))
284
+ k = max(self.k_min, min(self.k_max, k))
285
+ gold = example.gold_text
286
+ chosen = [gold]
287
+ chosen_set = {gold}
288
+
289
+ for text in self._hard_negatives(example):
290
+ if len(chosen) >= k:
291
+ break
292
+ if text not in chosen_set:
293
+ chosen.append(text)
294
+ chosen_set.add(text)
295
+
296
+ # Fill remaining slots from the distractor pool (other labels' texts, etc.).
297
+ candidates = [t for t in self.pool if t not in chosen_set]
298
+ rng.shuffle(candidates)
299
+ for text in candidates:
300
+ if len(chosen) >= k:
301
+ break
302
+ chosen.append(text)
303
+ chosen_set.add(text)
304
+
305
+ filler = 0
306
+ while len(chosen) < k:
307
+ dummy = f"[distractor {filler}]"
308
+ filler += 1
309
+ if dummy not in chosen_set:
310
+ chosen.append(dummy)
311
+ chosen_set.add(dummy)
312
+
313
+ chosen = chosen[:k]
314
+ # Gold is at index 0 of `chosen` before shuffle. Remap via the permutation,
315
+ # not str.index, so duplicate strings cannot attach the label to the wrong slot.
316
+ perm = list(range(k))
317
+ rng.shuffle(perm)
318
+ shuffled = [chosen[i] for i in perm]
319
+ new_label = perm.index(0)
320
+ return AugmentedSet(options=shuffled, label=new_label, k=k)
321
+
322
+
323
+ class DecisionDataset(Dataset):
324
+ def __init__(
325
+ self,
326
+ examples: Sequence[DecisionExample],
327
+ augmenter: SetAugmenter,
328
+ *,
329
+ seed: int,
330
+ augment: bool = True,
331
+ fixed_k: Optional[int] = None,
332
+ epoch: int = 0,
333
+ ):
334
+ self.examples = list(examples)
335
+ self.augmenter = augmenter
336
+ self.seed = int(seed)
337
+ self.augment = augment
338
+ self.fixed_k = fixed_k
339
+ self.epoch = int(epoch)
340
+
341
+ def set_epoch(self, epoch: int) -> None:
342
+ self.epoch = int(epoch)
343
+
344
+ def __len__(self) -> int:
345
+ return len(self.examples)
346
+
347
+ def _rng(self, index: int) -> random.Random:
348
+ # Deterministic per (epoch, index) so a seeded DataLoader is reproducible.
349
+ return random.Random(self.seed + 1_000_003 * (self.epoch + 1) + index)
350
+
351
+ def __getitem__(self, index: int) -> dict[str, Any]:
352
+ ex = self.examples[index]
353
+ rng = self._rng(index)
354
+ if self.augment:
355
+ aug = self.augmenter.build(ex, rng, k=self.fixed_k)
356
+ options, label = aug.options, aug.label
357
+ else:
358
+ options, label = list(ex.options), int(ex.label)
359
+ if self.fixed_k is not None:
360
+ aug = self.augmenter.build(ex, rng, k=self.fixed_k)
361
+ options, label = aug.options, aug.label
362
+ return {
363
+ "context": ex.context,
364
+ "options": options,
365
+ "label": label,
366
+ "task_type": ex.task_type,
367
+ "k": len(options),
368
+ "gold_text": ex.gold_text,
369
+ }
370
+
371
+
372
+ class DecisionCollator:
373
+ """Pad variable-K option lists to Kmax and tokenize."""
374
+
375
+ def __init__(
376
+ self,
377
+ tokenizer: Any,
378
+ *,
379
+ k_max: int,
380
+ max_context_tokens: int,
381
+ max_option_tokens: int,
382
+ ):
383
+ self.tokenizer = tokenizer
384
+ self.k_max = k_max
385
+ self.max_context_tokens = max_context_tokens
386
+ self.max_option_tokens = max_option_tokens
387
+
388
+ def _tok(self, texts: list[str], max_length: int) -> dict[str, torch.Tensor]:
389
+ if getattr(self.tokenizer, "is_hash_tokenizer", False):
390
+ return self.tokenizer(texts, max_length=max_length)
391
+ return self.tokenizer(
392
+ texts,
393
+ padding="max_length",
394
+ truncation=True,
395
+ max_length=max_length,
396
+ return_tensors="pt",
397
+ )
398
+
399
+ def __call__(self, batch: list[dict[str, Any]]) -> dict[str, torch.Tensor]:
400
+ bsz = len(batch)
401
+ kmax = self.k_max
402
+ contexts = [row["context"] for row in batch]
403
+ ctx = self._tok(contexts, self.max_context_tokens)
404
+
405
+ opt_ids = torch.full(
406
+ (bsz, kmax, self.max_option_tokens),
407
+ fill_value=int(getattr(self.tokenizer, "pad_token_id", 0) or 0),
408
+ dtype=torch.long,
409
+ )
410
+ opt_attn = torch.zeros(bsz, kmax, self.max_option_tokens, dtype=torch.long)
411
+ opt_mask = torch.zeros(bsz, kmax, dtype=torch.bool)
412
+ labels = torch.zeros(bsz, dtype=torch.long)
413
+
414
+ flat_opts: list[str] = []
415
+ coords: list[tuple[int, int]] = []
416
+ for b, row in enumerate(batch):
417
+ options = row["options"]
418
+ if len(options) > kmax:
419
+ raise ValueError(f"example has K={len(options)} > k_max={kmax}")
420
+ lab = int(row["label"])
421
+ if not 0 <= lab < len(options):
422
+ raise ValueError("label not in real option range after shuffle")
423
+ labels[b] = lab
424
+ for i, text in enumerate(options):
425
+ opt_mask[b, i] = True
426
+ flat_opts.append(text)
427
+ coords.append((b, i))
428
+
429
+ if flat_opts:
430
+ tok = self._tok(flat_opts, self.max_option_tokens)
431
+ for n, (b, i) in enumerate(coords):
432
+ opt_ids[b, i] = tok["input_ids"][n]
433
+ opt_attn[b, i] = tok["attention_mask"][n]
434
+
435
+ return {
436
+ "ctx_ids": ctx["input_ids"],
437
+ "ctx_mask": ctx["attention_mask"],
438
+ "opt_ids": opt_ids,
439
+ "opt_mask": opt_mask,
440
+ "opt_attn": opt_attn,
441
+ "y": labels,
442
+ }
443
+
444
+
445
+ def iter_k_views(
446
+ examples: Sequence[DecisionExample],
447
+ augmenter: SetAugmenter,
448
+ k_values: Sequence[int],
449
+ *,
450
+ seed: int,
451
+ ) -> Iterator[tuple[int, DecisionDataset]]:
452
+ """Deterministic fixed-K holdout views for K-stratified eval."""
453
+ for k in k_values:
454
+ ds = DecisionDataset(
455
+ examples,
456
+ augmenter,
457
+ seed=seed + 17 * int(k),
458
+ augment=True,
459
+ fixed_k=int(k),
460
+ epoch=0,
461
+ )
462
+ yield int(k), ds
pivot_infer.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Serving contract (§8): context + option list → {choice, index, probs}."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from typing import Any, Optional
8
+
9
+ import torch
10
+
11
+ from .pivot_data import DecisionCollator
12
+ from .pivot_model import SetBrierEncoder, build_tokenizer
13
+
14
+
15
+ @torch.no_grad()
16
+ def predict(
17
+ model: SetBrierEncoder,
18
+ tokenizer: Any,
19
+ context: str,
20
+ options: list[str],
21
+ *,
22
+ max_context_tokens: int,
23
+ max_option_tokens: int,
24
+ device: Optional[torch.device] = None,
25
+ ) -> dict[str, Any]:
26
+ """One-forward decision. ``probs`` has length K and sums to 1. No generation."""
27
+ if len(options) < 2:
28
+ raise ValueError("need at least two options")
29
+ device = device or next(model.parameters()).device
30
+ collator = DecisionCollator(
31
+ tokenizer,
32
+ k_max=len(options),
33
+ max_context_tokens=max_context_tokens,
34
+ max_option_tokens=max_option_tokens,
35
+ )
36
+ batch = collator(
37
+ [{"context": context, "options": options, "label": 0, "k": len(options), "gold_text": options[0]}]
38
+ )
39
+ batch = {k: v.to(device) for k, v in batch.items() if k != "y"}
40
+ return model.decide(
41
+ ctx_ids=batch["ctx_ids"],
42
+ ctx_mask=batch["ctx_mask"],
43
+ opt_ids=batch["opt_ids"],
44
+ opt_mask=batch["opt_mask"],
45
+ opt_attn=batch["opt_attn"],
46
+ option_texts=options,
47
+ )
48
+
49
+
50
+
51
+ @torch.no_grad()
52
+ def decide_typed(
53
+ model: SetBrierEncoder,
54
+ tokenizer: Any,
55
+ state: str,
56
+ questions: list[dict[str, Any]],
57
+ *,
58
+ max_context_tokens: int,
59
+ max_option_tokens: int,
60
+ device: Optional[torch.device] = None,
61
+ model_id: str = "Pivot-Alpha",
62
+ ) -> dict[str, Any]:
63
+ """Jev-style: one unstructured state → many typed probabilistic decisions.
64
+
65
+ Each question dict:
66
+ id: str
67
+ primitive: "choice" | "noul" | "score"
68
+ options: list[str] (K>=2)
69
+ description: optional str
70
+ """
71
+ from .pivot_serving_schema import build_response, build_typed_decision
72
+
73
+ if not questions:
74
+ raise ValueError("need at least one typed question")
75
+ decisions = []
76
+ for q in questions:
77
+ prim = str(q.get("primitive", "choice"))
78
+ if prim not in {"choice", "noul", "score"}:
79
+ raise ValueError(f"bad primitive {prim}")
80
+ options = list(q["options"])
81
+ raw = predict(
82
+ model,
83
+ tokenizer,
84
+ state,
85
+ options,
86
+ max_context_tokens=max_context_tokens,
87
+ max_option_tokens=max_option_tokens,
88
+ device=device,
89
+ )
90
+ decisions.append(
91
+ build_typed_decision(
92
+ decision_id=str(q.get("id", f"d{len(decisions)}")),
93
+ primitive=prim, # type: ignore[arg-type]
94
+ options=options,
95
+ index=int(raw["index"]),
96
+ probs=[float(x) for x in raw["probs"]],
97
+ description=q.get("description"),
98
+ )
99
+ )
100
+ return build_response(state=state, decisions=decisions, model_id=model_id)
pivot_model.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Set-Brier Encoder: LFM2.5 (or stub) + option-set scorer + masked softmax.
2
+
3
+ The same probability vector ``p`` is used for training losses and inference.
4
+ Padded option slots are filled with -inf before softmax and never receive
5
+ softmax mass or loss. Pad option content is zeroed before the scorer so it
6
+ cannot enter ``p``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from dataclasses import dataclass
13
+ from typing import Any, Optional
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+
19
+ PAD_ID = 0
20
+
21
+
22
+ def masked_mean_pool(last_hidden: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
23
+ """Mean-pool non-padding tokens of last_hidden_state. Enc(·) ∈ R^d."""
24
+ mask = attention_mask.unsqueeze(-1).to(dtype=last_hidden.dtype)
25
+ summed = (last_hidden * mask).sum(dim=1)
26
+ denom = mask.sum(dim=1).clamp_min(1e-6)
27
+ return summed / denom
28
+
29
+
30
+ def mask_option_logits(logits: torch.Tensor, opt_mask: torch.Tensor) -> torch.Tensor:
31
+ """Replace pad slots with dtype.min so they get zero softmax mass.
32
+
33
+ Uses ``masked_fill`` (not a post-softmax multiply) so pads do not affect
34
+ the partition function of real options.
35
+ """
36
+ fill = torch.finfo(logits.dtype).min
37
+ return logits.masked_fill(~opt_mask.bool(), fill)
38
+
39
+
40
+ class HashTokenizer:
41
+ """Deterministic whitespace tokenizer for stub / smoke runs (no HF download)."""
42
+
43
+ def __init__(self, vocab_size: int = 256, pad_token_id: int = PAD_ID):
44
+ if vocab_size < 4:
45
+ raise ValueError("vocab_size must be >= 4")
46
+ self.vocab_size = vocab_size
47
+ self.pad_token_id = pad_token_id
48
+ self.unk_token_id = 1
49
+ self.is_hash_tokenizer = True
50
+
51
+ def token_id(self, token: str) -> int:
52
+ digest = hashlib.md5(token.encode("utf-8")).digest()
53
+ return 2 + (int.from_bytes(digest[:4], "little") % (self.vocab_size - 2))
54
+
55
+ def encode(self, text: str, max_length: int) -> tuple[list[int], list[int]]:
56
+ pieces = text.lower().split() or ["<empty>"]
57
+ ids = [self.token_id(p) for p in pieces][:max_length]
58
+ attn = [1] * len(ids)
59
+ while len(ids) < max_length:
60
+ ids.append(self.pad_token_id)
61
+ attn.append(0)
62
+ return ids, attn
63
+
64
+ def __call__(
65
+ self,
66
+ texts: list[str],
67
+ *,
68
+ max_length: int,
69
+ padding: str = "max_length",
70
+ truncation: bool = True,
71
+ return_tensors: Optional[str] = "pt",
72
+ ) -> dict[str, torch.Tensor]:
73
+ del padding, truncation
74
+ ids, attn = zip(*(self.encode(t, max_length) for t in texts))
75
+ out = {
76
+ "input_ids": torch.tensor(ids, dtype=torch.long),
77
+ "attention_mask": torch.tensor(attn, dtype=torch.long),
78
+ }
79
+ if return_tensors != "pt":
80
+ raise ValueError("HashTokenizer only supports return_tensors='pt'")
81
+ return out
82
+
83
+
84
+ class StubEncoder(nn.Module):
85
+ """Tiny embedding encoder so CI/smoke never downloads the HF backbone."""
86
+
87
+ def __init__(self, vocab_size: int, hidden_size: int, pad_token_id: int = PAD_ID):
88
+ super().__init__()
89
+ self.hidden_size = hidden_size
90
+ self.embed = nn.Embedding(vocab_size, hidden_size, padding_idx=pad_token_id)
91
+
92
+ def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
93
+ return masked_mean_pool(self.embed(input_ids), attention_mask)
94
+
95
+
96
+ class HuggingFaceEncoder(nn.Module):
97
+ """LiquidAI LFM2.5-Encoder body + masked mean-pool."""
98
+
99
+ def __init__(
100
+ self,
101
+ model_id: str,
102
+ revision: str,
103
+ *,
104
+ trust_remote_code: bool = True,
105
+ torch_dtype: Optional[torch.dtype] = None,
106
+ lora: Optional[dict[str, Any]] = None,
107
+ ):
108
+ super().__init__()
109
+ from transformers import AutoModel
110
+
111
+ kwargs: dict[str, Any] = {
112
+ "revision": revision,
113
+ "trust_remote_code": trust_remote_code,
114
+ }
115
+ if torch_dtype is not None:
116
+ kwargs["torch_dtype"] = torch_dtype
117
+ self.model = AutoModel.from_pretrained(model_id, **kwargs)
118
+ self.hidden_size = int(getattr(self.model.config, "hidden_size", 1024))
119
+ if lora and lora.get("enabled"):
120
+ self.model = _wrap_lora(self.model, lora)
121
+
122
+ def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
123
+ out = self.model(input_ids=input_ids, attention_mask=attention_mask)
124
+ hidden = out.last_hidden_state if hasattr(out, "last_hidden_state") else out[0]
125
+ return masked_mean_pool(hidden, attention_mask)
126
+
127
+
128
+ def _wrap_lora(model: nn.Module, lora: dict[str, Any]) -> nn.Module:
129
+ try:
130
+ from peft import LoraConfig, get_peft_model
131
+ except ImportError as exc:
132
+ raise ImportError("LoRA requested but peft is not installed. pip install dsbt[lora]") from exc
133
+ cfg = LoraConfig(
134
+ r=int(lora.get("r", 16)),
135
+ lora_alpha=int(lora.get("alpha", 32)),
136
+ lora_dropout=float(lora.get("dropout", 0.05)),
137
+ target_modules=list(lora.get("target_modules") or ["q_proj", "v_proj"]),
138
+ bias="none",
139
+ task_type="FEATURE_EXTRACTION",
140
+ )
141
+ return get_peft_model(model, cfg)
142
+
143
+
144
+ class MLPScorer(nn.Module):
145
+ """Default scorer: 2-layer MLP on [h_c; h_o; h_c ⊙ h_o]."""
146
+
147
+ def __init__(self, hidden_size: int, mlp_hidden: int, dropout: float = 0.0):
148
+ super().__init__()
149
+ in_dim = 3 * hidden_size
150
+ self.net = nn.Sequential(
151
+ nn.Linear(in_dim, mlp_hidden),
152
+ nn.GELU(),
153
+ nn.Dropout(dropout),
154
+ nn.Linear(mlp_hidden, 1),
155
+ )
156
+
157
+ def forward(self, h_c: torch.Tensor, h_o: torch.Tensor) -> torch.Tensor:
158
+ # h_c: [B, d], h_o: [B, K, d] -> scores [B, K]
159
+ # Align dtypes: HF encoder may be bf16 while scorer Linear defaults to fp32.
160
+ wdtype = next(self.net.parameters()).dtype
161
+ h_c = h_c.to(dtype=wdtype)
162
+ h_o = h_o.to(dtype=wdtype)
163
+ h_c_exp = h_c.unsqueeze(1).expand_as(h_o)
164
+ feat = torch.cat([h_c_exp, h_o, h_c_exp * h_o], dim=-1)
165
+ return self.net(feat).squeeze(-1)
166
+
167
+
168
+ class DotScorer(nn.Module):
169
+ """Allowed simpler baseline: s_i = h_c^T h_{o,i} / sqrt(d)."""
170
+
171
+ def __init__(self, hidden_size: int):
172
+ super().__init__()
173
+ self.scale = hidden_size ** 0.5
174
+
175
+ def forward(self, h_c: torch.Tensor, h_o: torch.Tensor) -> torch.Tensor:
176
+ # Match dtype if encoder/scorer mix bf16/fp32
177
+ if h_c.dtype != h_o.dtype:
178
+ h_o = h_o.to(dtype=h_c.dtype)
179
+ return torch.einsum("bd,bkd->bk", h_c, h_o) / self.scale
180
+
181
+
182
+ @dataclass
183
+ class SetBrierOutput:
184
+ """Forward bundle. ``probs`` is the inference distribution (same p as train)."""
185
+
186
+ logits: torch.Tensor # masked s [B, Kmax]
187
+ probs: torch.Tensor # p = softmax(s) [B, Kmax]
188
+ pred_index: torch.Tensor # argmax among real options [B]
189
+ h_c: torch.Tensor
190
+ h_o: torch.Tensor
191
+
192
+
193
+ class SetBrierEncoder(nn.Module):
194
+ """Decision encoder: Enc(context), Enc(options), score, masked softmax."""
195
+
196
+ def __init__(self, encoder: nn.Module, scorer: nn.Module):
197
+ super().__init__()
198
+ self.encoder = encoder
199
+ self.scorer = scorer
200
+
201
+ @property
202
+ def hidden_size(self) -> int:
203
+ return int(self.encoder.hidden_size)
204
+
205
+ def encode(self, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
206
+ return self.encoder(input_ids=input_ids, attention_mask=attention_mask)
207
+
208
+ def score_options(
209
+ self,
210
+ h_c: torch.Tensor,
211
+ h_o: torch.Tensor,
212
+ opt_mask: torch.Tensor,
213
+ ) -> torch.Tensor:
214
+ """Score options. Pad embeddings are zeroed; pad logits are -inf.
215
+
216
+ Pads are never allowed to contribute to ``p``: zeroing removes pad
217
+ *content* from the scorer, and ``masked_fill`` removes pad *slots*
218
+ from the softmax partition.
219
+ """
220
+ mask = opt_mask.unsqueeze(-1).to(dtype=h_o.dtype)
221
+ h_o_real = h_o * mask
222
+ logits = self.scorer(h_c, h_o_real)
223
+ return mask_option_logits(logits, opt_mask)
224
+
225
+ def forward(
226
+ self,
227
+ ctx_ids: torch.Tensor,
228
+ ctx_mask: torch.Tensor,
229
+ opt_ids: torch.Tensor,
230
+ opt_mask: torch.Tensor,
231
+ opt_attn: torch.Tensor,
232
+ ) -> SetBrierOutput:
233
+ """
234
+ ctx_ids: [B, Lc]
235
+ ctx_mask: [B, Lc]
236
+ opt_ids: [B, Kmax, Lo]
237
+ opt_mask: [B, Kmax]
238
+ opt_attn: [B, Kmax, Lo]
239
+ """
240
+ h_c = self.encode(ctx_ids, ctx_mask)
241
+ bsz, kmax, lo = opt_ids.shape
242
+ flat_ids = opt_ids.reshape(bsz * kmax, lo)
243
+ flat_attn = opt_attn.reshape(bsz * kmax, lo)
244
+ h_o = self.encode(flat_ids, flat_attn).reshape(bsz, kmax, -1)
245
+ logits = self.score_options(h_c, h_o, opt_mask)
246
+ # Live softmax — this is the inference p and the Brier p. Do not detach.
247
+ # Research hard rule: compute p in fp32 even when logits are bf16
248
+ # (pad -inf / partition stability for calibration).
249
+ probs = F.softmax(logits.float(), dim=-1)
250
+ pred_index = probs.argmax(dim=-1)
251
+ return SetBrierOutput(
252
+ logits=logits,
253
+ probs=probs,
254
+ pred_index=pred_index,
255
+ h_c=h_c,
256
+ h_o=h_o,
257
+ )
258
+
259
+ @torch.no_grad()
260
+ def decide(
261
+ self,
262
+ ctx_ids: torch.Tensor,
263
+ ctx_mask: torch.Tensor,
264
+ opt_ids: torch.Tensor,
265
+ opt_mask: torch.Tensor,
266
+ opt_attn: torch.Tensor,
267
+ option_texts: list[str],
268
+ ) -> dict[str, Any]:
269
+ """Serving contract: {choice, index, probs} with probs over real options."""
270
+ self.eval()
271
+ out = self.forward(ctx_ids, ctx_mask, opt_ids, opt_mask, opt_attn)
272
+ # Packed real options occupy mask=1 slots (collate puts them at 0..K-1).
273
+ # CPU copy for JSON only; this is not a calibration map.
274
+ probs = out.probs[0][opt_mask[0].bool()].cpu().float()
275
+ probs = probs.clamp_min(0)
276
+ z = probs.sum().clamp_min(1e-12)
277
+ probs = probs / z
278
+ index = int(out.pred_index[0].item())
279
+ return {
280
+ "choice": option_texts[index],
281
+ "index": index,
282
+ "probs": probs.tolist(),
283
+ }
284
+
285
+ def encoder_parameters(self):
286
+ return self.encoder.parameters()
287
+
288
+ def scorer_parameters(self):
289
+ return self.scorer.parameters()
290
+
291
+
292
+ def build_tokenizer(cfg: dict[str, Any]):
293
+ backbone = cfg["backbone"]
294
+ if backbone.get("encoder", "huggingface") == "stub":
295
+ return HashTokenizer(vocab_size=int(backbone.get("stub_vocab_size", 256)))
296
+ from transformers import AutoTokenizer
297
+
298
+ return AutoTokenizer.from_pretrained(
299
+ backbone["model_id"],
300
+ revision=backbone["revision"],
301
+ trust_remote_code=bool(backbone.get("trust_remote_code", True)),
302
+ )
303
+
304
+
305
+ def _torch_dtype(precision: str, device: torch.device) -> Optional[torch.dtype]:
306
+ if precision == "bf16" and device.type == "cuda" and torch.cuda.is_bf16_supported():
307
+ return torch.bfloat16
308
+ return torch.float32
309
+
310
+
311
+ def build_model(cfg: dict[str, Any], device: Optional[torch.device] = None) -> SetBrierEncoder:
312
+ """Build SetBrierEncoder from config. Stub path never touches HuggingFace weights."""
313
+ device = device or torch.device("cpu")
314
+ backbone = cfg["backbone"]
315
+ scorer_cfg = cfg["scorer"]
316
+ mode = backbone.get("encoder", "huggingface")
317
+ precision = cfg["train"].get("precision", "fp32")
318
+ dtype = _torch_dtype(precision, device)
319
+
320
+ if mode == "stub":
321
+ hidden = int(backbone.get("stub_hidden_size", backbone.get("hidden_size", 32)))
322
+ encoder: nn.Module = StubEncoder(
323
+ vocab_size=int(backbone.get("stub_vocab_size", 256)),
324
+ hidden_size=hidden,
325
+ )
326
+ elif mode == "huggingface":
327
+ encoder = HuggingFaceEncoder(
328
+ model_id=backbone["model_id"],
329
+ revision=backbone["revision"],
330
+ trust_remote_code=bool(backbone.get("trust_remote_code", True)),
331
+ torch_dtype=dtype if dtype != torch.float32 else None,
332
+ lora=cfg["train"].get("lora"),
333
+ )
334
+ hidden = encoder.hidden_size
335
+ else:
336
+ raise ValueError(f"unknown backbone.encoder {mode!r}")
337
+
338
+ stype = scorer_cfg.get("type", "mlp")
339
+ if stype == "mlp":
340
+ scorer: nn.Module = MLPScorer(
341
+ hidden_size=hidden,
342
+ mlp_hidden=int(scorer_cfg.get("hidden_size", hidden)),
343
+ dropout=float(scorer_cfg.get("dropout", 0.0)),
344
+ )
345
+ elif stype == "dot":
346
+ scorer = DotScorer(hidden_size=hidden)
347
+ else:
348
+ raise ValueError(f"unknown scorer.type {stype!r}; use 'mlp' or 'dot'")
349
+
350
+ model = SetBrierEncoder(encoder, scorer)
351
+ model.to(device)
352
+ # Keep encoder+scorer on the same compute dtype (bf16 on H100). Softmax p
353
+ # stays fp32 in forward() — that is the calibration contract.
354
+ if dtype == torch.bfloat16:
355
+ model.to(dtype=dtype)
356
+ return model
pivot_serving_schema.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pivot-Alpha / Jev-aligned typed decision schema (English).
2
+
3
+ Jev: unstructured state in → typed probabilistic decisions out.
4
+ We are NOT a free-text generator. Each call returns structured decisions
5
+ the host program can wire into a workflow.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Literal, Optional
10
+
11
+ Primitive = Literal["choice", "noul", "score"]
12
+
13
+
14
+ def build_typed_decision(
15
+ *,
16
+ decision_id: str,
17
+ primitive: Primitive,
18
+ options: list[str],
19
+ index: int,
20
+ probs: list[float],
21
+ description: Optional[str] = None,
22
+ ) -> dict[str, Any]:
23
+ if len(options) != len(probs):
24
+ raise ValueError("options/probs length mismatch")
25
+ if not (0 <= index < len(options)):
26
+ raise ValueError("index out of range")
27
+ # named map for program consumption
28
+ prob_map = {str(opt): float(p) for opt, p in zip(options, probs)}
29
+ value = options[index]
30
+ out: dict[str, Any] = {
31
+ "id": decision_id,
32
+ "primitive": primitive,
33
+ "description": description,
34
+ "options": list(options),
35
+ "index": int(index),
36
+ "value": value,
37
+ "probs": prob_map,
38
+ "prob_vector": [float(p) for p in probs],
39
+ "confidence": float(max(probs) if probs else 0.0),
40
+ }
41
+ if primitive == "noul":
42
+ # binary probabilistic decision (yes-mass = prob of first true-like option if present)
43
+ true_aliases = {"true", "yes", "y", "1"}
44
+ true_idx = next((i for i, o in enumerate(options) if str(o).lower() in true_aliases), index)
45
+ out["p_true"] = float(probs[true_idx])
46
+ if primitive == "score":
47
+ # expected score if options are ordered numeric levels; else keep categorical
48
+ try:
49
+ levels = [float(o) for o in options]
50
+ out["expected"] = float(sum(l * p for l, p in zip(levels, probs)))
51
+ except ValueError:
52
+ out["expected"] = None
53
+ return out
54
+
55
+
56
+ def build_response(
57
+ *,
58
+ state: str,
59
+ decisions: list[dict[str, Any]],
60
+ model_id: str = "Pivot-Alpha",
61
+ ) -> dict[str, Any]:
62
+ return {
63
+ "model": model_id,
64
+ "contract": "unstructured_state_in__typed_probabilistic_decisions_out",
65
+ "state": state,
66
+ "decisions": decisions,
67
+ "schema_version": "pivot-alpha-v1",
68
+ }
predict_example.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import json
3
+ from transformers import AutoModel, AutoTokenizer
4
+
5
+ root = Path(__file__).resolve().parent
6
+ tokenizer = AutoTokenizer.from_pretrained(root, trust_remote_code=True, local_files_only=True)
7
+ model = AutoModel.from_pretrained(root, trust_remote_code=True, local_files_only=True).eval()
8
+ request = json.loads((root / "serving/example_request.json").read_text())
9
+ print(json.dumps(model.decide(tokenizer, **request), indent=2))
provenance.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "source": {
3
+ "type": "huggingface_dataset",
4
+ "repo": "Yinhaoc/dsbt-cleared-corpus-v1",
5
+ "revision": "0afd5031a19ece346b3287b8804ebb6b7c5a276d",
6
+ "file": "best.pt"
7
+ },
8
+ "checkpoint_sha256": "73d1d2959409229825a39e5a0136cae38541cd5a22d7b4836061683da96d59c8",
9
+ "created_utc": "2026-09-20T21:55:59.682825+00:00",
10
+ "checkpoint": {
11
+ "epoch": 0,
12
+ "global_step": 3102,
13
+ "extra": {
14
+ "epoch": 0,
15
+ "warmup": true,
16
+ "steps_per_sec": 1.8812464419600063,
17
+ "train_L_dec": 0.5377311386806899,
18
+ "train_L_cal": 0.040688835784281306,
19
+ "train_acc": 0.7659171502256609,
20
+ "val_acc": 0.319091796875,
21
+ "val_brier": 0.11288549791788682,
22
+ "val_ece": 0.1171102523803711,
23
+ "seconds": 1648.9068133831024
24
+ },
25
+ "completed_epochs": 1,
26
+ "optimizer_present": true,
27
+ "base_model": "LiquidAI/LFM2.5-Encoder-350M",
28
+ "base_revision": "b886781f7c6f10ca9b7096e21b83e30a073c2f39",
29
+ "configured_epochs": 4,
30
+ "evaluation_batch_saved": false,
31
+ "note": "best.pt is the best epoch, not proof of the final training epoch or eval progress."
32
+ },
33
+ "source_sha256": {
34
+ "__init__.py": "0143657a43a2b1fe4bc7196971f4792087ed0d32ef426c248a2e2474863ea4d9",
35
+ "__main__.py": "df98998af529a0dc97c09725fe512169bdc6c4dd718c908cc381c16de6d904f0",
36
+ "config.py": "3970b69bb4893c5f113dbac75fee901c43c9905a8733789beaec40fe32f7e1df",
37
+ "data.py": "21919e9fd760df1665ac27cc1f63df86edaa312101ccc1c008a0770ce00593da",
38
+ "eval.py": "3e986c0ae827d9457ee1016dedf405cfd21f25b56bc209249427a8c1b62652e9",
39
+ "infer.py": "be72f8a76ac22a8a1cc93d946b61cad499e0c63dac5d54d4b671f7329b8176b5",
40
+ "losses.py": "01a7f50adb43b563cfc66d7bc5782a75e9a1cf7f9170dc86ffaee46a6c702c3d",
41
+ "model.py": "8b17e60eca7f2a54d8579fc88cff8eb70cd1202222e8872c7e3447772df61724",
42
+ "seed.py": "8549ad3cdf0cca42c777b50943101ebddf6bee02e336b5b7f43c4068916b3f5d",
43
+ "serving_schema.py": "8a777f03e595b675dd2d0eb6c12a9247990bb48dd794bb0a806d1c18e297159a",
44
+ "train.py": "8276a95b8d2b6d5f5dfe5cb9072c9b5ee4fa9fb8e401e8e2ebaaff4d5c0ce369"
45
+ }
46
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch==2.8.0
2
+ transformers==5.17.0
3
+ safetensors==0.8.0
4
+ numpy==1.26.4
run_metrics.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mode": "export_only",
3
+ "training": "not_run",
4
+ "frozen_eval": {
5
+ "status": "skipped",
6
+ "reason": "user_requested_export_only"
7
+ },
8
+ "latency_benchmark": "not_run",
9
+ "typed_smoke": "passed",
10
+ "safetensors": true,
11
+ "onnx": "not_exported",
12
+ "jev_win_verified": false
13
+ }
serving/example_request.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "state": "Customer dispute: invoice 120 vs PO 100, age=3d, region=US",
3
+ "questions": [
4
+ {
5
+ "id": "route",
6
+ "primitive": "choice",
7
+ "options": [
8
+ "billing",
9
+ "tech",
10
+ "sales"
11
+ ]
12
+ },
13
+ {
14
+ "id": "approve",
15
+ "primitive": "noul",
16
+ "options": [
17
+ "true",
18
+ "false"
19
+ ]
20
+ },
21
+ {
22
+ "id": "severity",
23
+ "primitive": "score",
24
+ "options": [
25
+ "0",
26
+ "1",
27
+ "2",
28
+ "3"
29
+ ]
30
+ }
31
+ ]
32
+ }
serving/example_response.json ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "Pivot",
3
+ "contract": "unstructured_state_in__typed_probabilistic_decisions_out",
4
+ "state": "Customer dispute: invoice 120 vs PO 100, age=3d, region=US",
5
+ "decisions": [
6
+ {
7
+ "id": "route",
8
+ "primitive": "choice",
9
+ "description": null,
10
+ "options": [
11
+ "billing",
12
+ "tech",
13
+ "sales"
14
+ ],
15
+ "index": 0,
16
+ "value": "billing",
17
+ "probs": {
18
+ "billing": 0.709779679775238,
19
+ "tech": 0.013995149172842503,
20
+ "sales": 0.27622511982917786
21
+ },
22
+ "prob_vector": [
23
+ 0.709779679775238,
24
+ 0.013995149172842503,
25
+ 0.27622511982917786
26
+ ],
27
+ "confidence": 0.709779679775238
28
+ },
29
+ {
30
+ "id": "approve",
31
+ "primitive": "noul",
32
+ "description": null,
33
+ "options": [
34
+ "true",
35
+ "false"
36
+ ],
37
+ "index": 1,
38
+ "value": "false",
39
+ "probs": {
40
+ "true": 0.44199028611183167,
41
+ "false": 0.5580097436904907
42
+ },
43
+ "prob_vector": [
44
+ 0.44199028611183167,
45
+ 0.5580097436904907
46
+ ],
47
+ "confidence": 0.5580097436904907,
48
+ "p_true": 0.44199028611183167
49
+ },
50
+ {
51
+ "id": "severity",
52
+ "primitive": "score",
53
+ "description": null,
54
+ "options": [
55
+ "0",
56
+ "1",
57
+ "2",
58
+ "3"
59
+ ],
60
+ "index": 3,
61
+ "value": "3",
62
+ "probs": {
63
+ "0": 0.007189917378127575,
64
+ "1": 0.22580669820308685,
65
+ "2": 0.04488111659884453,
66
+ "3": 0.7221222519874573
67
+ },
68
+ "prob_vector": [
69
+ 0.007189917378127575,
70
+ 0.22580669820308685,
71
+ 0.04488111659884453,
72
+ 0.7221222519874573
73
+ ],
74
+ "confidence": 0.7221222519874573,
75
+ "expected": 2.4819356873631477
76
+ }
77
+ ],
78
+ "schema_version": "pivot-alpha-v1"
79
+ }
serving/schema.json ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Pivot typed decision response",
4
+ "type": "object",
5
+ "required": [
6
+ "model",
7
+ "contract",
8
+ "state",
9
+ "decisions",
10
+ "schema_version"
11
+ ],
12
+ "properties": {
13
+ "model": {
14
+ "const": "Pivot"
15
+ },
16
+ "state": {
17
+ "type": "string"
18
+ },
19
+ "contract": {
20
+ "const": "unstructured_state_in__typed_probabilistic_decisions_out"
21
+ },
22
+ "schema_version": {
23
+ "const": "pivot-alpha-v1"
24
+ },
25
+ "decisions": {
26
+ "type": "array",
27
+ "minItems": 1,
28
+ "items": {
29
+ "type": "object",
30
+ "required": [
31
+ "id",
32
+ "primitive",
33
+ "options",
34
+ "index",
35
+ "value",
36
+ "probs",
37
+ "prob_vector",
38
+ "confidence"
39
+ ],
40
+ "properties": {
41
+ "id": {
42
+ "type": "string"
43
+ },
44
+ "primitive": {
45
+ "enum": [
46
+ "choice",
47
+ "noul",
48
+ "score"
49
+ ]
50
+ },
51
+ "options": {
52
+ "type": "array",
53
+ "minItems": 2,
54
+ "items": {
55
+ "type": "string"
56
+ }
57
+ },
58
+ "index": {
59
+ "type": "integer",
60
+ "minimum": 0
61
+ },
62
+ "value": {
63
+ "type": "string"
64
+ },
65
+ "probs": {
66
+ "type": "object",
67
+ "additionalProperties": {
68
+ "type": "number",
69
+ "minimum": 0,
70
+ "maximum": 1
71
+ }
72
+ },
73
+ "prob_vector": {
74
+ "type": "array",
75
+ "items": {
76
+ "type": "number",
77
+ "minimum": 0,
78
+ "maximum": 1
79
+ }
80
+ },
81
+ "confidence": {
82
+ "type": "number",
83
+ "minimum": 0,
84
+ "maximum": 1
85
+ },
86
+ "p_true": {
87
+ "type": "number",
88
+ "minimum": 0,
89
+ "maximum": 1
90
+ },
91
+ "expected": {
92
+ "type": [
93
+ "number",
94
+ "null"
95
+ ]
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "bos_token": "<|startoftext|>",
4
+ "clean_up_tokenization_spaces": false,
5
+ "eos_token": "<|im_end|>",
6
+ "is_local": false,
7
+ "local_files_only": false,
8
+ "mask_token": "<|mask|>",
9
+ "model_max_length": 1000000000000000019884624838656,
10
+ "pad_token": "<|pad|>",
11
+ "tokenizer_class": "TokenizersBackend"
12
+ }