File size: 14,126 Bytes
95b8ba8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b84c7c4
95b8ba8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
import spaces  # must be imported before torch

import html
import json
import sys
import time
from pathlib import Path

import gradio as gr
import torch
from huggingface_hub import snapshot_download
from safetensors.torch import load_file
from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration

MODEL_ID = "Cloudflare/clef-flash"
MODEL_PATH = Path(snapshot_download(MODEL_ID))
sys.path.insert(0, str(MODEL_PATH))
from joint_schema_model import (  # noqa: E402
    QUESTION_TYPES,
    ClefModel,
    JointSchemaHead,
    collate_records,
    encode_record,
    systemone_answer,
)

# Authors' load_release_model uses device_map, which initializes CUDA before the
# ZeroGPU patch can intercept it. Load eagerly with .to("cuda") instead.
backbone = Qwen3_5ForConditionalGeneration.from_pretrained(
    MODEL_PATH,
    dtype=torch.bfloat16,
    attn_implementation="sdpa",
).to("cuda")
backbone.config.use_cache = False
head = JointSchemaHead(**json.loads((MODEL_PATH / "joint_head_config.json").read_text()))
head.load_state_dict(load_file(MODEL_PATH / "joint_head.safetensors"), strict=True)
head = head.to(device="cuda", dtype=torch.bfloat16)
model = ClefModel(backbone, head).eval()
processor = AutoProcessor.from_pretrained(MODEL_PATH)

MAX_IMAGE_SIDE = 1024

EX_SUPPORT_STATE = "Our checkout started returning errors and orders are blocked."
EX_SUPPORT_Q = {
    "department": {
        "type": "choice",
        "instructions": "Which team should handle the message?",
        "criteria": {"billing": "Payments or invoices", "technical": "Bugs or outages"},
    },
    "urgency": {"type": "score", "criteria": ["Can wait", "This week", "Today"]},
    "outage": {"type": "noul", "instructions": "Is a service down?"},
}
EX_INVOICE_STATE = {
    "invoice": {"vendor": "Acme", "total": 1250.0, "currency": "USD", "status": "overdue"}
}
EX_INVOICE_Q = {
    "status": {
        "type": "choice",
        "instructions": "What is the invoice status?",
        "criteria": {
            "paid": "Invoice is paid.",
            "overdue": "Invoice is past due.",
            "draft": "Not sent.",
        },
    },
    "large": {"type": "noul", "instructions": "Is the total above 1000 USD?"},
}
EX_REVIEW_STATE = (
    "Ordered the noise-cancelling headphones two weeks ago. Sound is great but the left ear cup "
    "started crackling yesterday and the support chat keeps disconnecting. I want a replacement, not a refund."
)
EX_REVIEW_Q = {
    "sentiment": {
        "type": "score",
        "instructions": "Overall customer sentiment",
        "criteria": ["Very negative", "Negative", "Mixed", "Positive", "Very positive"],
    },
    "action": {
        "type": "choice",
        "instructions": "What should the agent do next?",
        "criteria": {
            "replace": "Ship a replacement unit",
            "refund": "Issue a refund",
            "troubleshoot": "Send troubleshooting steps",
            "escalate": "Escalate to a supervisor",
        },
    },
    "defect": {"type": "noul", "instructions": "Does the customer report a hardware defect?"},
}
EX_FOOD_STATE = {
    "task": "Moderate a photo uploaded to a restaurant listing.",
    "listing": "Vegan Garden Bistro",
}
EX_FOOD_Q = {
    "category": {
        "type": "choice",
        "instructions": "What does the photo mainly show?",
        "criteria": {
            "food": "A dish or meal",
            "interior": "Restaurant interior",
            "menu": "A menu or text",
            "people": "People",
        },
    },
    "matches_listing": {
        "type": "noul",
        "instructions": "Is the dish plausibly vegan, consistent with the listing?",
    },
    "appetizing": {
        "type": "score",
        "instructions": "How appetizing is the photo?",
        "criteria": ["Not at all", "Somewhat", "Very"],
    },
}
EX_ROOM_STATE = "A guest submitted this photo with a listing for a short-term rental apartment."
EX_ROOM_Q = {
    "room": {
        "type": "choice",
        "instructions": "Which room is shown?",
        "criteria": {
            "bedroom": "Bedroom",
            "kitchen": "Kitchen",
            "living_room": "Living room",
            "bathroom": "Bathroom",
        },
    },
    "has_tv": {"type": "noul", "instructions": "Is there a television in the room?"},
    "tidiness": {
        "type": "score",
        "instructions": "How tidy is the room?",
        "criteria": ["Messy", "Average", "Spotless"],
    },
}


def _j(obj) -> str:
    return json.dumps(obj, indent=2, ensure_ascii=False)


def render_html(questions: dict, answers: dict, probs: dict, latency_ms: float, n_tokens: int) -> str:
    parts = ['<div class="clef-out">']
    for qid, q in questions.items():
        qtype = q["type"]
        p = probs[qid]
        ans = answers[qid]
        if qtype == "noul":
            headline = f"{'true' if p['true'] >= 0.5 else 'false'} · P(true) = {p['true']:.3f}"
            labels = {"true": "true", "false": "false"}
        elif qtype == "choice":
            headline = f"{html.escape(str(ans['choice']))} · {ans['confidence']:.3f}"
            labels = {k: html.escape(str(k)) for k in p}
        else:
            headline = f"expected score {ans['score']:.2f} / {len(q['criteria']) - 1}"
            labels = {str(i): f"{i} · {html.escape(str(c))}" for i, c in enumerate(q["criteria"])}
        best = max(p, key=p.get)
        instr = html.escape(str(q.get("instructions") or qid))
        parts.append(
            f'<div class="clef-q"><div class="clef-qh"><span class="clef-id">{html.escape(str(qid))}</span>'
            f'<span class="clef-type">{html.escape(qtype)}</span></div>'
            f'<div class="clef-instr">{instr}</div><div class="clef-head">{headline}</div>'
        )
        order = list(p.keys()) if qtype in ("score", "noul") else sorted(p, key=p.get, reverse=True)
        for opt in order:
            pct = 100 * p[opt]
            cls = "clef-bar best" if opt == best else "clef-bar"
            parts.append(
                f'<div class="clef-row"><div class="clef-lbl">{labels.get(opt, html.escape(str(opt)))}</div>'
                f'<div class="clef-track"><div class="{cls}" style="width:{pct:.1f}%"></div></div>'
                f'<div class="clef-pct">{pct:.1f}%</div></div>'
            )
        parts.append("</div>")
    parts.append(
        f'<div class="clef-meta">{n_tokens} input tokens · forward pass {latency_ms:.0f} ms</div></div>'
    )
    return "".join(parts)


def _parse_state(state: str):
    text = (state or "").strip()
    if text[:1] in "{[":
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            pass
    return text


def _validate(questions):
    if not isinstance(questions, dict) or not questions:
        raise gr.Error("Questions must be a non-empty JSON object mapping question IDs to questions.")
    for qid, question in questions.items():
        if not isinstance(question, dict) or question.get("type") not in QUESTION_TYPES:
            raise gr.Error(f"{qid}: type must be noul, choice, or score.")
        if question["type"] == "choice" and not (
            isinstance(question.get("criteria"), dict) and question["criteria"]
        ):
            raise gr.Error(f"{qid}: a choice question needs criteria as option id → description.")
        if question["type"] == "score" and not (
            isinstance(question.get("criteria"), list) and question["criteria"]
        ):
            raise gr.Error(f"{qid}: a score question needs criteria as a list of level descriptions.")


@spaces.GPU(duration=45)
def decide(state: str, questions_json: str, image=None):
    """Score every allowed option of every typed question about a state with Clef-Flash.

    Args:
        state: The situation to decide on, as free text or a JSON value.
        questions_json: JSON object mapping question IDs to questions. Each question has a
            type (`noul` for true/false, `choice` for named options, `score` for ordered
            levels), optional instructions, and criteria.
        image: Optional image that is part of the state.

    Returns:
        An HTML view of per-option probabilities and a SystemOne-style JSON response.
    """
    try:
        questions = json.loads(questions_json)
    except json.JSONDecodeError as exc:
        raise gr.Error(f"Questions JSON is invalid: {exc}") from exc
    _validate(questions)
    if not (state or "").strip() and image is None:
        raise gr.Error("Provide a state, an image, or both.")

    record = {"model": "clef-flash", "state": _parse_state(state), "questions": questions}
    if image is not None:
        image = image.convert("RGB")
        image.thumbnail((MAX_IMAGE_SIDE, MAX_IMAGE_SIDE))
        record["images"] = [image]

    try:
        encoded = encode_record(processor.tokenizer, record, max_length=16384, processor=processor)
    except Exception as exc:
        raise gr.Error(f"Could not encode the request: {exc}") from exc

    batch = collate_records([encoded], processor.tokenizer.pad_token_id, torch.device("cuda"))
    torch.cuda.synchronize()
    started = time.perf_counter()
    with torch.inference_mode():
        logits = model(batch)[0]
    torch.cuda.synchronize()
    latency_ms = (time.perf_counter() - started) * 1000

    probs, answers = {}, {}
    for encoded_question, question_logits in zip(encoded.questions, logits):
        probabilities = dict(
            zip(encoded_question.option_ids, question_logits.float().softmax(-1).tolist())
        )
        probs[encoded_question.question_id] = probabilities
        answers[encoded_question.question_id] = systemone_answer(
            questions[encoded_question.question_id], probabilities
        )
    response = {
        "model": "clef-flash",
        "answers": answers,
        "usage": {"input_tokens": len(encoded.input_ids), "output_tokens": 0},
    }
    print(f"clef-flash forward {latency_ms:.1f} ms tokens={len(encoded.input_ids)}", flush=True)
    return render_html(questions, answers, probs, latency_ms, len(encoded.input_ids)), response


CSS = """
#col-container { max-width: 1200px; margin: 0 auto; }
.clef-out { display: flex; flex-direction: column; gap: 12px; }
.clef-q { border: 1px solid var(--border-color-primary); border-radius: 10px; padding: 12px 14px; background: var(--block-background-fill); }
.clef-qh { display: flex; gap: 8px; align-items: center; }
.clef-id { font-weight: 700; font-family: var(--font-mono); }
.clef-type { font-size: 0.75em; padding: 1px 8px; border-radius: 99px; background: var(--color-accent-soft); }
.clef-instr { font-size: 0.9em; opacity: 0.8; margin: 4px 0 6px; }
.clef-head { font-weight: 600; margin-bottom: 8px; }
.clef-row { display: grid; grid-template-columns: minmax(90px, 38%) 1fr 56px; gap: 8px; align-items: center; margin: 3px 0; }
.clef-lbl { font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clef-track { height: 12px; border-radius: 6px; background: var(--background-fill-secondary); overflow: hidden; }
.clef-bar { height: 100%; background: var(--neutral-400); border-radius: 6px; }
.clef-bar.best { background: var(--color-accent); }
.clef-pct { font-size: 0.85em; text-align: right; font-family: var(--font-mono); }
.clef-meta { font-size: 0.8em; opacity: 0.7; }
"""

with gr.Blocks(title="Clef-Flash") as demo:
    with gr.Column(elem_id="col-container"):
        gr.Markdown(
            "# Clef-Flash\n"
            "Give [Cloudflare/clef-flash](https://huggingface.co/Cloudflare/clef-flash) a **state** "
            "(text, JSON, or an image) and a **schema of typed questions**. It returns a probability "
            "for every allowed option of every question in one forward pass — no text generation and "
            "no output parsing.\n\n"
            "`noul` is true/false, `choice` is named options, `score` is ordered levels. "
            "[Announcement](https://blog.cloudflare.com/clef-decision-models)"
        )
        with gr.Row():
            with gr.Column(scale=1):
                state = gr.Textbox(
                    label="State (free text or JSON)",
                    lines=5,
                    value=EX_SUPPORT_STATE,
                    placeholder="Describe the situation, or paste a JSON object.",
                )
                questions = gr.Code(
                    label="Questions schema (JSON)",
                    language="json",
                    value=_j(EX_SUPPORT_Q),
                    lines=14,
                )
                image = gr.Image(label="Image in the state (optional)", type="pil", height=240)
                run = gr.Button("Decide", variant="primary")
            with gr.Column(scale=1):
                out_html = gr.HTML(label="Decisions")
                with gr.Accordion("SystemOne JSON response", open=False):
                    out_json = gr.JSON(label="Response")

        gr.Markdown("### Examples — text or JSON")
        text_examples = gr.Examples(
            examples=[
                [EX_SUPPORT_STATE, _j(EX_SUPPORT_Q)],
                [_j(EX_INVOICE_STATE), _j(EX_INVOICE_Q)],
                [EX_REVIEW_STATE, _j(EX_REVIEW_Q)],
            ],
            inputs=[state, questions],
            outputs=[out_html, out_json],
            fn=decide,
            cache_examples=True,
            cache_mode="lazy",
        )
        gr.Markdown("### Examples — with an image")
        gr.Examples(
            examples=[
                [_j(EX_FOOD_STATE), _j(EX_FOOD_Q), "examples/burger.jpg"],
                [EX_ROOM_STATE, _j(EX_ROOM_Q), "examples/living_room.jpg"],
            ],
            inputs=[state, questions, image],
            outputs=[out_html, out_json],
            fn=decide,
            cache_examples=True,
            cache_mode="lazy",
        )
        text_examples.dataset.click(lambda: None, outputs=image, api_visibility="private")

    run.click(decide, inputs=[state, questions, image], outputs=[out_html, out_json], api_name="decide")

if __name__ == "__main__":
    demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)