jet / format.py
michaljach's picture
Release Jet v6: full merged Qwen3.5-4B BF16 weights and native typed inference
e5b8f61 verified
Raw History Blame Contribute Delete
5.53 kB
"""Prompt format shared by training and inference.
Every question is reduced to "pick one label token". The model is run once over
the prompt and we read the next-token logits restricted to the label tokens:
- choice: one label per option (A..Z, then single-token AA, AB, ...), up to 255 options
- score: one digit per level (0..9), 2-10 levels, ordered low -> high
- noul: "no" / "yes"
"""
from __future__ import annotations
import itertools
import json
import string
from dataclasses import dataclass
from typing import Any, Literal
QuestionType = Literal["choice", "score", "noul"]
MAX_CHOICE_OPTIONS = 255
MIN_SCORE_LEVELS, MAX_SCORE_LEVELS = 2, 10
SYSTEM_PROMPT = (
"You are Jet, a decision model. Read the state and the question, then answer "
"with exactly one label from the allowed labels."
)
@dataclass(frozen=True)
class Question:
type: QuestionType
instructions: str
# choice: {key: description}; score: [level descriptions, low -> high]; noul: None
criteria: dict[str, str] | list[str] | None = None
@property
def keys(self) -> list[str]:
"""Answer keys in label order (option keys, level descriptions, or no/yes)."""
if self.type == "choice":
return list(self.criteria)
if self.type == "score":
return list(self.criteria)
return ["no", "yes"]
@classmethod
def from_dict(cls, d: dict[str, Any]) -> Question:
q = cls(type=d["type"], instructions=d["instructions"], criteria=d.get("criteria"))
q.validate()
return q
def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {"type": self.type, "instructions": self.instructions}
if self.criteria is not None:
d["criteria"] = self.criteria
return d
def validate(self) -> None:
if self.type not in ("choice", "score", "noul"):
raise ValueError(f"unknown question type {self.type!r}")
if not isinstance(self.instructions, str) or not self.instructions.strip():
raise ValueError("instructions must be a non-empty string")
if self.type == "choice":
if not isinstance(self.criteria, dict) or not 2 <= len(self.criteria) <= MAX_CHOICE_OPTIONS:
raise ValueError(f"choice criteria must be an object with 2-{MAX_CHOICE_OPTIONS} options")
elif self.type == "score":
if not isinstance(self.criteria, list) or not MIN_SCORE_LEVELS <= len(self.criteria) <= MAX_SCORE_LEVELS:
raise ValueError(f"score criteria must be a list of {MIN_SCORE_LEVELS}-{MAX_SCORE_LEVELS} levels")
elif self.criteria is not None:
raise ValueError("noul questions take no criteria")
_CHOICE_LABELS: dict[int, list[str]] = {}
def choice_labels(tokenizer, n: int) -> list[str]:
"""A..Z, then the two-letter labels AA, AB, ... that are single tokens for this tokenizer."""
key = id(tokenizer)
if key not in _CHOICE_LABELS:
letters = string.ascii_uppercase
pairs = ("".join(p) for p in itertools.product(letters, repeat=2))
candidates = list(letters) + [p for p in pairs if len(tokenizer.encode(p, add_special_tokens=False)) == 1]
if len(candidates) < MAX_CHOICE_OPTIONS:
raise ValueError(f"tokenizer only has {len(candidates)} single-token choice labels")
_CHOICE_LABELS[key] = candidates[:MAX_CHOICE_OPTIONS]
return _CHOICE_LABELS[key][:n]
def labels_for(tokenizer, q: Question) -> list[str]:
if q.type == "choice":
return choice_labels(tokenizer, len(q.criteria))
if q.type == "score":
return [str(i) for i in range(len(q.criteria))]
return ["no", "yes"]
def render_state(state: Any) -> str:
if isinstance(state, str):
return state
if isinstance(state, list) and all(isinstance(s, str) for s in state):
return "\n\n".join(state)
return json.dumps(state, ensure_ascii=False, indent=2)
def render_user(state: Any, q: Question, labels: list[str]) -> str:
parts = [f"<state>\n{render_state(state)}\n</state>", ""]
if q.type == "choice":
opts = "\n".join(f"{lab}: {key}: {desc}" for lab, (key, desc) in zip(labels, q.criteria.items()))
parts += [
f"Question: {q.instructions}",
f"Options:\n{opts}",
f"Answer with the label of the best option ({labels[0]}-{labels[-1]}).",
]
elif q.type == "score":
levels = "\n".join(f"{lab}: {desc}" for lab, desc in zip(labels, q.criteria))
parts += [
f"Question: {q.instructions}",
f"Scale (lowest to highest):\n{levels}",
f"Answer with the level number (0-{len(labels) - 1}).",
]
else:
parts += [f"Question: {q.instructions}", "Answer yes or no."]
return "\n".join(parts)
def build_prompt(tokenizer, state: Any, q: Question) -> str:
return tokenizer.apply_chat_template(
[{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": render_user(state, q, labels_for(tokenizer, q))}],
add_generation_prompt=True,
tokenize=False,
enable_thinking=False,
)
def _label_token_id(tokenizer, label: str) -> int:
ids = tokenizer.encode(label, add_special_tokens=False)
if len(ids) != 1:
raise ValueError(f"label {label!r} is not a single token for this tokenizer")
return ids[0]
def label_token_ids(tokenizer, q: Question) -> list[int]:
return [_label_token_id(tokenizer, lab) for lab in labels_for(tokenizer, q)]