Spaces:
Running on Zero
Running on Zero
File size: 6,742 Bytes
0a87665 8a2b0d3 0a87665 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """Decision prompt compiler.
Turns a Jev-style request (state + typed questions) into chat messages whose
assistant turn is a JSON skeleton with one ``<decision>`` marker per field.
The model's next-token logits just before each marker, restricted to that
field's answer symbols, give the field's probability distribution.
With default settings the output is byte-identical to Intern-Decision's
compiler, so the start checkpoint sees exactly the format it was trained on.
Extensions: an optional reasoning trace inside ``<think>`` and an optional
``unknown`` option for Choice and Score questions.
"""
from __future__ import annotations
import json
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any
DECISION_TOKEN = "<decision>"
ANSWER_SYMBOLS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
MAX_OPTIONS = len(ANSWER_SYMBOLS)
UNKNOWN = "unknown"
SYSTEM_PROMPT = (
"You are a careful decision assistant. Use the state and decision schema in "
"the user message to make the requested decisions. For every field, choose "
"exactly one answer symbol (e.g. A, B, C, ...) from its listed options and "
"return one valid JSON object mapping each field name to its chosen symbol. "
"Use the field names and symbols exactly as given. Do not include "
"explanations, Markdown, or extra text."
)
NOUL_YES = "The answer is yes (affirmative, or align with the claim)."
NOUL_NO = "The answer is no (negative, or disagree with the claim)."
UNKNOWN_TEXT = {
"en": "The state does not contain enough information to decide.",
"tr": "Durum, karar vermek için yeterli bilgiyi içermiyor.",
"de": "Der Zustand enthält nicht genügend Informationen, um zu entscheiden.",
}
@dataclass(frozen=True)
class Field:
name: str
kind: str # choice | score | noul
values: tuple[str, ...] # option values in prompt order
symbols: tuple[str, ...]
@dataclass(frozen=True)
class Compiled:
messages: list[dict[str, Any]]
fields: tuple[Field, ...]
def _text(value: Any) -> str:
"""Strings pass through; structured values render as compact JSON."""
if isinstance(value, str):
return value
if value is None:
return ""
return json.dumps(value, ensure_ascii=False)
def options(question: Mapping[str, Any]) -> list[tuple[str, str]]:
"""Ordered (value, description) pairs for a question."""
kind = question.get("type")
criteria = question.get("criteria")
if kind == "choice":
if not isinstance(criteria, Mapping) or not criteria:
raise ValueError("choice criteria must be a non-empty object")
return [(str(k), _text(v)) for k, v in criteria.items()]
if kind == "score":
if isinstance(criteria, list) and len(criteria) >= 2:
return [(str(i), _text(v)) for i, v in enumerate(criteria)]
if isinstance(criteria, Mapping) and len(criteria) >= 2:
return [(str(k), _text(v)) for k, v in criteria.items()]
raise ValueError("score criteria must list at least two levels")
if kind == "noul":
described = criteria if isinstance(criteria, Mapping) else {}
yes = next((_text(described[k]) for k in described if str(k).lower() in {"yes", "true", "1"}), NOUL_YES)
no = next((_text(described[k]) for k in described if str(k).lower() in {"no", "false", "0"}), NOUL_NO)
return [("no", no), ("yes", yes)]
raise ValueError(f"unsupported question type: {kind!r}")
def compile_request(
request: Mapping[str, Any],
*,
reasoning: str | None = None,
abstain: bool = False,
language: str = "en",
) -> Compiled:
questions = request.get("questions")
if not isinstance(questions, Mapping) or not questions:
raise ValueError("questions must be a non-empty object")
fields: list[Field] = []
lines: list[str] = []
for name, question in questions.items():
if not isinstance(question, Mapping):
raise ValueError(f"question {name!r} must be an object")
opts = options(question)
kind = question["type"]
if abstain and kind in {"choice", "score"} and UNKNOWN not in {v for v, _ in opts}:
opts.append((UNKNOWN, UNKNOWN_TEXT.get(language, UNKNOWN_TEXT["en"])))
if len(opts) > MAX_OPTIONS:
raise ValueError(f"question {name!r} has {len(opts)} options; the limit is {MAX_OPTIONS}")
symbols = tuple(ANSWER_SYMBOLS[: len(opts)])
fields.append(Field(str(name), kind, tuple(v for v, _ in opts), symbols))
lines.append(f"{name}: {_text(question.get('instructions', ''))}")
for symbol, (value, description) in zip(symbols, opts):
lines.append(f" {symbol} = {value}: {description}" if description else f" {symbol} = {value}")
state = json.dumps(request.get("state"), ensure_ascii=False, indent=2)
user = (
"Return one answer for every field using the supplied answer symbols.\n\n"
"## State\n" + state + "\n## Decision schema\n" + "\n".join(lines)
)
if DECISION_TOKEN in user:
raise ValueError("the reserved <decision> marker appears in the input")
skeleton = json.dumps(dict.fromkeys((f.name for f in fields), DECISION_TOKEN), ensure_ascii=False, indent=4)
assistant: dict[str, Any] = {"role": "assistant", "content": skeleton}
if reasoning:
assistant["reasoning_content"] = reasoning.strip()
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user},
assistant,
]
return Compiled(messages=messages, fields=tuple(fields))
def render(compiled: Compiled, *, close: bool = True) -> str:
"""Render the chat exactly like the Qwen3.5 template with enable_thinking=False.
``close=False`` stops right after ``<think>\\n`` so a backend can generate a
reasoning trace, then append ``render_tail``.
"""
system, user, assistant = compiled.messages
head = (
f"<|im_start|>system\n{system['content']}<|im_end|>\n"
f"<|im_start|>user\n{user['content']}<|im_end|>\n"
"<|im_start|>assistant\n<think>\n"
)
if not close:
return head
return head + render_tail(compiled, assistant.get("reasoning_content", ""))
def render_tail(compiled: Compiled, reasoning: str = "") -> str:
skeleton = compiled.messages[2]["content"]
return f"{reasoning.strip()}\n</think>\n\n{skeleton}<|im_end|>\n"
def confidence(probabilities: list[float]) -> float:
"""TypeSafe-style confidence: 1 when all mass is on one option, 0 when flat."""
n = len(probabilities)
if n < 2:
return 1.0
return min(1.0, max(0.0, (n * max(probabilities) - 1) / (n - 1)))
|