File size: 10,491 Bytes
9159e0f 809d930 9159e0f 809d930 9159e0f 356ab0d 9159e0f 9875a28 9159e0f 9875a28 9159e0f 809d930 356ab0d 9159e0f 27761a1 9159e0f 356ab0d 89f1d21 9159e0f 356ab0d 9159e0f 9875a28 9159e0f 27761a1 9159e0f 9875a28 356ab0d 9159e0f 356ab0d 9875a28 9159e0f c4aab4c 9159e0f c4aab4c 9159e0f 6e54fae 9159e0f 9875a28 9159e0f 9875a28 9159e0f d22192b c4aab4c 9159e0f 27761a1 9159e0f c4aab4c 9159e0f 27761a1 c4aab4c 9159e0f c4aab4c 9159e0f 27761a1 9159e0f 9875a28 c4aab4c 9159e0f 9875a28 6e54fae 9875a28 9159e0f 27761a1 9159e0f 9875a28 9159e0f | 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 | """
Dialectical Transition Operator — interactive demo.
Given a (question, answer, 3 critiques), the model returns a (revised answer, 3 fresh
critiques). Iterating it deepens the answer.
Model = Qwen3-8B + two stacked LoRA adapters (a frozen "SFT-voice" + a trainable "RL"
adapter trained with GRPO / set-VPO under a readability-and-addressability-aware judge).
"""
import spaces # MUST be imported before anything initializes CUDA
import os
import re
import random
from threading import Thread
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
from peft import PeftModel
from huggingface_hub import snapshot_download
import gradio as gr
from questions import QUESTIONS
MODEL_ID = "Qwen/Qwen3-8B"
REPO = "andreiski/dialectical-transition-cot"
SFT_SUB = "mission-diversity-sft/adapter-final" # frozen SFT-voice adapter
RL_SUB = "mission-diversity-rl/adapter" # trained RL adapter
HF_TOKEN = os.environ.get("HF_TOKEN")
# Pre-download weights at startup (pure file fetch, no CUDA) so the lazy GPU-load is quick.
print("warming weight cache ...")
snapshot_download(MODEL_ID, token=HF_TOKEN)
snapshot_download(REPO, token=HF_TOKEN, allow_patterns=[SFT_SUB + "/*", RL_SUB + "/*"])
TOK = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN)
if TOK.pad_token is None:
TOK.pad_token = TOK.eos_token
MODEL = None
def _ensure_model():
"""Load base + stacked adapters INSIDE the GPU context (cached after first call).
ZeroGPU has no GPU at import time, so model/CUDA setup must happen here."""
global MODEL
if MODEL is not None:
return MODEL
base = AutoModelForCausalLM.from_pretrained(
MODEL_ID, torch_dtype=torch.bfloat16, token=HF_TOKEN).to("cuda")
m = PeftModel.from_pretrained(base, REPO, subfolder=SFT_SUB, adapter_name="sftvoice", token=HF_TOKEN)
m.load_adapter(REPO, subfolder=RL_SUB, adapter_name="rl", token=HF_TOKEN)
m.base_model.set_adapter(["sftvoice", "rl"]) # BOTH active/stacked — matches training
m.eval()
MODEL = m
return MODEL
# ------------------------------------------------------------------ format (verbatim)
def render_prompt(question, answer, critiques):
lines = [f"Question: {question}", f"Answer: {answer}", "Critiques of answer:"]
for i, c in enumerate(critiques):
lines.append(f"[c{i + 1}] {c}")
return "\n".join(lines)
_CLEAN_RE = re.compile(r"<\|[^>]*\|>")
_REVISED_RE = re.compile(r"(?msi)^[ \t]*Revised answer[ \t]*:[ \t]*(.*?)"
r"(?=^[ \t]*Critiques of revised answer[ \t]*:|\Z)")
_CRITHDR_RE = re.compile(r"(?mi)^[ \t]*Critiques of revised answer[ \t]*:[ \t]*")
_CRIT_RE = re.compile(r"(?ms)^[ \t]*\[c(\d+)\][ \t]*(.*?)(?=^[ \t]*\[c\d+\]|\Z)")
def _clean(s):
return _CLEAN_RE.sub("", s or "").strip()
_REVHDR_RE = re.compile(r"(?i)revised answer\s*:\s*")
_CRITHDR2_RE = re.compile(r"(?i)critiques of revised answer\s*:")
_CRITN_RE = re.compile(r"\[c([123])\]\s*(.*?)(?=\[c[123]\]|\Z)", re.S)
def partial_parse(text):
"""Best-effort incremental parse for streaming: returns (thinking, revised, [c1,c2,c3])."""
op = text.find("<think>")
start = op + len("<think>") if op != -1 else 0
cl = text.find("</think>")
if cl != -1:
thinking = text[start:cl]
body = text[cl + len("</think>"):]
else:
thinking = text[start:]
body = ""
revised = ""
mr = _REVHDR_RE.search(body)
if mr:
after = body[mr.end():]
mc = _CRITHDR2_RE.search(after)
revised = after[:mc.start()] if mc else after
crits = ["", "", ""]
mh = _CRITHDR2_RE.search(body)
if mh:
cb = body[mh.end():]
for m in _CRITN_RE.finditer(cb):
crits[int(m.group(1)) - 1] = m.group(2)
return _clean(thinking), _clean(revised), [_clean(c) for c in crits]
# ------------------------------------------------------------------------- seeds
GENERIC_ANSWERS = [
"Who knows?", "It depends.", "That's hard to say.", "There isn't really a clear answer.",
"It's complicated.", "Honestly, it could go either way.", "I'm not sure.",
]
GENERIC_CRITIQUES = [
"This doesn't actually answer the question.",
"It gives the reader nothing useful to act on.",
"It's vague and noncommittal.",
"It dodges the question entirely.",
"It offers no reasoning or explanation.",
"It's too generic to be helpful.",
"It never engages with what was actually asked.",
"It leaves the reader exactly where they started.",
]
def random_seed():
a = random.choice(GENERIC_ANSWERS)
cs = random.sample(GENERIC_CRITIQUES, 3)
return a, cs[0], cs[1], cs[2]
def random_question():
return random.choice(QUESTIONS)
# ---------------------------------------------------------------------- generation
def _run_stream(question, ans, c1, c2, c3):
model = _ensure_model()
user = render_prompt(question, ans, [c1, c2, c3])
prompt = TOK.apply_chat_template([{"role": "user", "content": user}],
tokenize=False, add_generation_prompt=True)
inputs = TOK(prompt, return_tensors="pt").to(model.device)
streamer = TextIteratorStreamer(TOK, skip_prompt=True, skip_special_tokens=False)
kw = dict(**inputs, max_new_tokens=1280, do_sample=True, temperature=1.0,
top_p=1.0, top_k=0, repetition_penalty=1.0,
pad_token_id=TOK.eos_token_id, streamer=streamer)
Thread(target=model.generate, kwargs=kw).start()
full = ""
for chunk in streamer:
full += chunk
yield full
# --------------------------------------------------------------------------- UI html
_IDLE = """
<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:430px;">
<div style="writing-mode:vertical-rl;transform:rotate(180deg);
background:#334155;color:#f8fafc;border-radius:6px;
display:flex;align-items:center;justify-content:center;
padding:10px 16px;min-height:430px;
font-weight:600;font-size:.92em;letter-spacing:3px;text-transform:uppercase;">
transition operator
</div>
</div>"""
_BUSY = """
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:430px;">
<div style="background:#334155;border-radius:6px;min-height:430px;padding:14px 16px;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;">
<div style="width:30px;height:30px;border:4px solid #64748b;border-top-color:#f8fafc;border-radius:50%;animation:spin .8s linear infinite;"></div>
<div style="writing-mode:vertical-rl;transform:rotate(180deg);color:#cbd5e1;font-size:.82em;letter-spacing:2px;text-transform:uppercase;">thinking</div>
</div>
</div>"""
@spaces.GPU(duration=60)
def ui_generate(question, ans, c1, c2, c3):
if not (question or "").strip():
yield _IDLE, "", "Enter a question first.", "", "", ""
return
yield _BUSY, "", "", "", "", ""
cot, rev, cs = "", "", ["", "", ""]
for full in _run_stream(question, ans or "", c1 or "", c2 or "", c3 or ""):
cot, rev, cs = partial_parse(full)
box = _IDLE if rev else _BUSY # spinner during <think>, then stream answer + critiques
yield box, cot, rev, cs[0], cs[1], cs[2]
msg = rev if rev else "Generation was malformed — press Generate to try again."
yield _IDLE, cot, msg, cs[0], cs[1], cs[2]
# ------------------------------------------------------------------------------- app
_THEME = gr.themes.Default(font=[gr.themes.GoogleFont("Inter")])
with gr.Blocks(title="Dialectical Transition Operator", theme=_THEME) as demo:
gr.Markdown("# Dialectical Transition Operator")
gr.Markdown(
"Give the model a **question**, a starting **answer**, and **three critiques** of that answer. "
"It returns a **revised answer** and **three fresh critiques**. "
"**Copy the output back to the input and generate again** to iteratively deepen the answer.")
with gr.Row():
question = gr.Textbox(label="Question (X)", lines=2, placeholder="Ask anything…", scale=8)
randq_btn = gr.Button("Random question", scale=1, min_width=130)
with gr.Row(equal_height=True):
with gr.Column(scale=6):
gr.Markdown("#### Input")
in_ans = gr.Textbox(label="Answer", lines=5)
gr.Markdown("Critiques")
in_c1 = gr.Textbox(show_label=False, lines=2)
in_c2 = gr.Textbox(show_label=False, lines=2)
in_c3 = gr.Textbox(show_label=False, lines=2)
with gr.Column(scale=1, min_width=66):
model_box = gr.HTML(_IDLE)
with gr.Column(scale=6):
gr.Markdown("#### Output")
out_ans = gr.Textbox(label="Revised answer", lines=5)
gr.Markdown("Critiques")
out_c1 = gr.Textbox(show_label=False, lines=2)
out_c2 = gr.Textbox(show_label=False, lines=2)
out_c3 = gr.Textbox(show_label=False, lines=2)
with gr.Row():
seed_btn = gr.Button("Random input")
gen_btn = gr.Button("Generate", variant="primary")
copy_btn = gr.Button("Copy output → input")
with gr.Accordion("Chain of thought (the model's live reasoning)", open=True):
cot_box = gr.Textbox(show_label=False, lines=8)
gr.Examples(
examples=[
"How does Hurricane Florence lose strength?",
"Explain Kant like I'm five but a philosophy professor would approve.",
"I signed a lease last week and found someone already living in the basement — what can I do?",
"Write a Python script that alerts me if NVIDIA stock moves 10% within 5 days.",
],
inputs=question)
randq_btn.click(random_question, outputs=question)
seed_btn.click(random_seed, outputs=[in_ans, in_c1, in_c2, in_c3])
gen_btn.click(ui_generate, inputs=[question, in_ans, in_c1, in_c2, in_c3],
outputs=[model_box, cot_box, out_ans, out_c1, out_c2, out_c3])
copy_btn.click(lambda a, b, c, d: (a, b, c, d, "", "", "", ""),
inputs=[out_ans, out_c1, out_c2, out_c3],
outputs=[in_ans, in_c1, in_c2, in_c3, out_ans, out_c1, out_c2, out_c3])
demo.load(random_seed, outputs=[in_ans, in_c1, in_c2, in_c3])
if __name__ == "__main__":
demo.queue(max_size=20).launch()
|