File size: 12,264 Bytes
78d8ece
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# knowline_layout.py: release copy of openjev.header_layout (OpenJev src/openjev/header_layout.py), generated by
# make_knowline_layout.py. Only the helper import differs (knowline_server instead of openjev.systemone.prompt).
# source sha256: 8dfa4571ac18c1f890bb1662ac8241b156417a49dc05d4ebec8257406a3a5011
"""Request layouts beyond the default "chat" layout of openjev.systemone.prompt: the header (the request's question list
before the state), read twice, and option rotations. Spec: docs/plans/2026-10-22-header-layout.md.

Pure Python. The only outside object is the model's tokenizer or HF processor (`template`): its chat template
(apply_chat_template) renders the turns and its encode() counts tokens for the read-twice cut. With every switch off,
render_request() / render() give byte-identical prompts to openjev.systemone.prompt.render(..., style="chat"), the layout
our training data and release serving use (tests/test_header_layout.py checks this).

    from openjev.header_layout import Layout, render, render_request
    text = render({"state": ..., "questions": {...}}, 0, tokenizer, Layout(header=True))          # one question
    r = render_request(request, tokenizer, Layout(header=True, read_twice=True), rotations=(0, 1))  # a whole request
    r["prompts"][qid][offset] -> (prompt text ending in "Answer:", answer keys in shown order)

Option rotations are a scoring technique: score the question at offsets first_pass_offsets(n), map each offset's label
distribution back with to_original(), average them with equal weight (combine_rotations), and when the two first-pass
offsets pick different options of a 3+ option question also score gated_offsets(n) and average all of them.
"""
import uuid
from dataclasses import dataclass

from knowline_server import ANSWER, DEFAULT_QUESTION, INSTRUCTION, find_labels, options_of, render_value, state_messages

HEADER_HEAD = "Questions that will be asked about the conversation or state below:\n"
HEADER_ITEM = "- "
READ_AGAIN = "Read the same input again before answering:\n"
TWICE_MAX_TOKENS = 3000
_KW = dict(tokenize=False, add_generation_prompt=True, enable_thinking=False)


@dataclass(frozen=True)
class Layout:
    header: bool = False             # list every question of the request before the state
    header_min_questions: int = 1    # requests with fewer questions keep the default layout (1 = every request)
    read_twice: bool = False         # write the whole input twice, joined by READ_AGAIN, ...
    twice_max_tokens: int = TWICE_MAX_TOKENS  # ... in text-only requests of at most this many tokens read once


# ------------------------------------------------------------------ pieces
def question_text(q):
    """The question stem, as the question block shows it."""
    return render_value(q["instructions"]) if q.get("instructions") is not None else DEFAULT_QUESTION


def header_text(questions):
    """'Questions that will be asked ...:\\n- <stem>\\n- <stem>\\n\\n' in request order (stems only, no options)."""
    return HEADER_HEAD + "\n".join(HEADER_ITEM + question_text(q) for q in questions.values()) + "\n\n"


def rotation(n, offset):
    """Original option indices in shown order for a cyclic rotation by `offset` (offset 0 = request order)."""
    offset %= n
    return list(range(offset, n)) + list(range(offset))


def question_block(q, labels, offset=0):
    """-> (block text, answer keys in shown order). Offset 0 is the default layout's block byte for byte."""
    keys, texts = options_of(q)
    order = rotation(len(keys), offset)
    lines = "".join(f"{labels[j]}. {texts[i]}\n" for j, i in enumerate(order))
    return f"Question: {question_text(q)}\nOptions:\n{lines.rstrip()}", [keys[i] for i in order]


def _prepend(msgs, text):
    """`text` before the first non-system turn: prefixed to that turn's content when it is a user turn, else its own user
    turn (without the trailing newlines) inserted there."""
    i = next((k for k, m in enumerate(msgs) if m.get("role") != "system"), len(msgs))
    if i < len(msgs) and msgs[i].get("role") == "user":
        c = msgs[i].get("content")
        c = [{"type": "text", "text": text}] + list(c) if isinstance(c, list) else text + (c or "")
        return msgs[:i] + [{**msgs[i], "content": c}] + msgs[i + 1:]
    return msgs[:i] + [{"role": "user", "content": text.rstrip("\n")}] + msgs[i:]


def _join(a, b):
    """Content of two adjacent turns of one role, folded into one turn with a blank line between them (for a text `b`
    after a text or part-list `a`, exactly the default layout's fold of the ask into the last user turn)."""
    if isinstance(a, list):
        return a + ([{"type": "text", "text": "\n\n" + b}] if isinstance(b, str) else [{"type": "text", "text": "\n\n"}] + list(b))
    if isinstance(b, list):
        return [{"type": "text", "text": f"{a}\n\n"}] + list(b)
    return f"{a}\n\n{b}"


def _merge_adjacent(msgs):
    out = []
    for m in msgs:
        if out and out[-1].get("role") == m.get("role") and m.get("role") in ("user", "system"):
            out[-1] = {**out[-1], "content": _join(out[-1].get("content"), m.get("content"))}
        else:
            out.append(dict(m))
    return out


def _conv(msgs, hdr, m1, m2):
    """[system turns] + [header] + state turns + ask(m1); with m2 also READ_AGAIN + the same non-system turns + ask(m2)."""
    if hdr:
        msgs = _prepend(msgs, hdr)
    conv = msgs + [{"role": "user", "content": INSTRUCTION + "\n\n" + m1}]
    if m2 is None:
        return conv
    again = [m for m in conv if m.get("role") != "system"]  # system turns appear once, at the start
    again[-1] = {"role": "user", "content": INSTRUCTION + "\n\n" + m2}
    return conv + _prepend(again, READ_AGAIN)


def _scaffold(template, state, hdr, twice):
    """-> (prefix, middle or None, ending, media). The question block goes at the markers: once at m1, twice at m1 and m2."""
    m1 = f"OPENJEV_{uuid.uuid4().hex}"
    m2 = f"OPENJEV_{uuid.uuid4().hex}" if twice else None
    msgs, media = state_messages(state)
    try:
        text = template.apply_chat_template(_conv(msgs, hdr, m1, m2), **_KW)
    except Exception:
        try:
            if not twice:  # the default layout's fold: the ask into the last user turn (strict-alternation templates)
                conv = _conv(msgs, hdr, m1, None)
                if len(conv) < 2 or conv[-2].get("role") != "user":
                    raise ValueError("last turn is not a user turn")
                conv = conv[:-2] + [{**conv[-2], "content": _join(conv[-2]["content"], conv[-1]["content"])}]
            else:
                conv = _merge_adjacent(_conv(msgs, hdr, m1, m2))
            text = template.apply_chat_template(conv, **_KW)
        except Exception:  # roles the template rejects: the whole chat as one user message, as the default layout does
            msgs, media = [{"role": "user", "content": render_value(state)}], []
            text = template.apply_chat_template(_conv(msgs, hdr, m1, m2), **_KW)
    if text.count(m1) != 1 or (m2 is not None and text.count(m2) != 1):
        raise ValueError("chat template dropped or duplicated the question slot")
    prefix, rest = text.split(m1)
    if m2 is None:
        return prefix, None, rest, media
    middle, ending = rest.split(m2)
    return prefix, middle, ending, media


_LABELS = {}


def labels_and_ids(template):
    """The default engine's label set and token ids: single-token labels A..Z, AA.. checked right after the real prompt
    ending (as openjev.systemone.engine.Engine finds them)."""
    key = id(template)
    if key not in _LABELS:
        prefix, _, ending, _ = _scaffold(template, "x", None, False)
        block, _ = question_block({"type": "noul"}, ["A", "B"])
        _LABELS[key] = (template, find_labels(getattr(template, "tokenizer", template), prefix + block + ending + ANSWER))
    return _LABELS[key][1]


def default_labels(template):
    return labels_and_ids(template)[0]


# ------------------------------------------------------------------ rendering
def render_request(request, template, layout=Layout(), rotations=(0,), labels=None):
    """-> {"prefix": shared prefix text, "twice": bool, "once_tokens": int | None, "media": [...],
           "prompts": {qid: {offset: (prompt text, answer keys in shown order)}}}.
    `request` is a /v1/systemone body ({"state", "questions"}). `rotations`: cyclic option offsets (see rotation()) for every
    question, or a function (question id, number of options) -> offsets, e.g. lambda qid, n: first_pass_offsets(n)."""
    state, questions = request.get("state", ""), request.get("questions") or {}
    if not questions:
        raise ValueError("no questions")
    labels = labels or default_labels(template)
    hdr = header_text(questions) if layout.header and len(questions) >= layout.header_min_questions else None
    prefix, _, ending, media = _scaffold(template, state, hdr, False)
    offs = rotations if callable(rotations) else (lambda qid, n: rotations)
    blocks = {qid: {o: question_block(q, labels, o) for o in dict.fromkeys(offs(qid, len(options_of(q)[0])))}
              for qid, q in questions.items()}
    blocks = {qid: bo for qid, bo in blocks.items() if bo}
    once = {qid: {o: (prefix + b + ending + ANSWER, keys) for o, (b, keys) in bo.items()} for qid, bo in blocks.items()}
    out = {"prefix": prefix, "twice": False, "once_tokens": None, "media": media, "prompts": once}
    if not layout.read_twice or media:  # read twice is text-only; a request with media is read once
        return out
    tok = getattr(template, "tokenizer", template)
    n = lambda s: len(tok.encode(s, add_special_tokens=False))
    p = n(prefix)
    first = {qid: (prefix + question_block(q, labels, 0)[0] + ending + ANSWER) for qid, q in questions.items()}
    out["once_tokens"] = p + sum(n(t) - p for t in first.values())  # shared prefix + every question row, read once
    if out["once_tokens"] > layout.twice_max_tokens:
        return out
    prefix2, middle, ending2, _ = _scaffold(template, state, hdr, True)
    out.update(prefix=prefix2, twice=True,
               prompts={qid: {o: (prefix2 + b + middle + b + ending2 + ANSWER, keys) for o, (b, keys) in bo.items()}
                        for qid, bo in blocks.items()})
    return out


def render(request, question_idx, template, layout=Layout(), rotation_offset=0, labels=None):
    """The prompt text of one question (by position in the request or by id), ending in "Answer:"."""
    qids = list((request.get("questions") or {}))
    qid = qids[question_idx] if isinstance(question_idx, int) else question_idx
    return render_request(request, template, layout, (rotation_offset,), labels)["prompts"][qid][rotation_offset][0]


# ------------------------------------------------------------------ rotations (scoring side)
def first_pass_offsets(n):
    """Request order and its rotation by n // 2 (a swap for two options)."""
    return list(dict.fromkeys([0, n // 2])) if n >= 2 else [0]


def gated_offsets(n):
    """Second pass for 3+ option questions whose first-pass offsets disagree: the other four-order rotations."""
    first = first_pass_offsets(n)
    return [o for o in dict.fromkeys([n // 4, 3 * n // 4]) if o not in first] if n >= 3 else []


def to_original(shown_probs, offset):
    """A label distribution in shown order -> the same distribution in request (original option) order."""
    n = len(shown_probs)
    out = [0.0] * n
    for j, i in enumerate(rotation(n, offset)):
        out[i] = shown_probs[j]
    return out


def argmax(p):
    return max(range(len(p)), key=p.__getitem__)


def combine_rotations(by_offset):
    """{offset: distribution in shown order} -> equal-weight mean in request order (each already tempered)."""
    rows = [to_original(p, o) for o, p in by_offset.items()]
    return [sum(r[i] for r in rows) / len(rows) for i in range(len(rows[0]))]


def needs_gate(by_offset):
    """True when the first-pass offsets of a 3+ option question pick different original options."""
    n = len(next(iter(by_offset.values())))
    first = first_pass_offsets(n)
    if n < 3 or any(o not in by_offset for o in first):
        return False
    return len({argmax(to_original(by_offset[o], o)) for o in first}) > 1