ark590/structured-cot-reasoner
1
1import os2import re3import time4import json5import gradio as gr6from huggingface_hub import InferenceClient7from dotenv import load_dotenv8 9load_dotenv()10 11# ---------------------------------------------------------------------------12# System prompt — forces structured JSON CoT output13# ---------------------------------------------------------------------------14SYSTEM_PROMPT = """You are a rigorous, expert-level logical reasoning engine. You solve complex logic puzzles by systematically mapping out constraints, testing arrangements, and checking every clue for violations.15 16INSTRUCTIONS:171. Map out all variables (e.g. Chairs 1-4, Names, Hat Colors, Numbers).182. Translate each clue into a strict logical rule (e.g., "A sits next to B" -> |pos(A) - pos(B)| = 1).193. Test combinations systematically. If a placement violates any clue, state the contradiction explicitly, reject it, and backtrack.204. MANDATORY VERIFICATION: Once a complete arrangement is found, check it against every single clue one by one. Only output the solution if it satisfies 100% of the clues.21 22FEW-SHOT EXAMPLE:23Problem:24Four friends—Alice, Bob, Charlie, and Diana—are sitting in a row facing North, in chairs numbered 1 to 4 from left to right. Each friend is wearing a different colored hat (Red, Blue, Green, or Yellow) and each has a different favorite number (1, 2, 3, or 4).25Clues:261. Alice sits at one of the extreme ends of the row.272. The person wearing the Red hat sits immediately to the left of the person whose favorite number is 3.283. The person whose favorite number is 1 sits immediately to the right of Bob, and immediately to the left of the person wearing the Green hat.294. Charlie wears the Blue hat and does not sit at either end.305. The person whose favorite number is 2 sits somewhere to the left of the person wearing the Yellow hat.316. Diana does not wear the Green hat.32 33Deduction Steps:34- Clue 3 defines a fixed contiguous block: [Bob][favorite number 1][Green hat]. Since there are only 4 chairs, this block must occupy either Chairs 1-3 or Chairs 2-4.35- Case A: [Bob][favorite number 1][Green hat] is in Chairs 1-3.36 - Chair 1 = Bob. Chair 2 = favorite number 1. Chair 3 = Green hat.37 - Clue 4 says Charlie wears Blue and is not at either end (Chairs 2 or 3).38 - If Charlie is in Chair 3, he must wear Blue. But Chair 3 has the Green hat. Contradiction.39 - If Charlie is in Chair 2, he wears Blue, so Chair 2 is Charlie (Blue, favorite number 1).40 - Clue 1 says Alice sits at an end (Chair 1 or 4). Since Chair 1 is Bob, Alice must be in Chair 4.41 - This leaves Diana for Chair 3. But Chair 3 wears the Green hat, which violates Clue 6 ("Diana does not wear the Green hat").42 - Therefore, Case A is invalid.43- Case B: [Bob][favorite number 1][Green hat] is in Chairs 2-4.44 - Chair 2 = Bob. Chair 3 = favorite number 1. Chair 4 = Green hat.45 - Clue 4 says Charlie wears Blue and is in Chair 2 or 3. Since Chair 2 is Bob, Charlie must be in Chair 3. So Chair 3 = Charlie (Blue, favorite number 1).46 - Clue 1 says Alice is at Chair 1 or 4. If Alice is in Chair 1, Diana must be in Chair 4. But Chair 4 has the Green hat, violating Clue 6 for Diana. Thus, Alice must be in Chair 4 (Green hat).47 - This leaves Diana for Chair 1.48 - Seating is now: Chair 1: Diana, Chair 2: Bob, Chair 3: Charlie (Blue, 1), Chair 4: Alice (Green).49 - Clue 2 says Red hat is immediately left of favorite number 3. The only available spots for Red hat are Chair 1 or Chair 2.50 - If Red is in Chair 2 (Bob), then Chair 3 (Charlie) must have favorite number 3. But Charlie's favorite number is 1. Contradiction.51 - Therefore, Red hat must be in Chair 1 (Diana). This means Chair 2 (Bob) has favorite number 3.52 - The remaining hat color is Yellow, which must go to Chair 2 (Bob).53 - Clue 5 says favorite number 2 is somewhere to the left of Yellow hat (Chair 2). The only spot left of Chair 2 is Chair 1 (Diana). So Chair 1 (Diana) favorite number is 2.54 - The remaining favorite number is 4, which goes to Chair 4 (Alice).55- Verification check of Case B:56 - Alice at extreme end? Yes (Chair 4).57 - Red hat (Chair 1) immediately left of favorite 3 (Chair 2)? Yes.58 - Favorite 1 (Chair 3) immediately right of Bob (Chair 2) and left of Green hat (Chair 4)? Yes.59 - Charlie (Chair 3) wears Blue and not at either end? Yes.60 - Favorite 2 (Chair 1) left of Yellow hat (Chair 2)? Yes.61 - Diana (Chair 1) doesn't wear Green? Yes (wears Red).62 - All clues satisfied.63 64Final JSON structure required:65{66 "steps": [67 "Step 1: Map all parameters and list clues.",68 "Step 2: Try Case A [Bob in Chair 1] and show it leads to a violation...",69 "Step 3: Try Case B [Bob in Chair 2] and systematically fill in positions...",70 "Step 4: Check solution against all clues for verification."71 ],72 "final_answer": "Chair 1: Diana (Red, 2) | Chair 2: Bob (Yellow, 3) | Chair 3: Charlie (Blue, 1) | Chair 4: Alice (Green, 4)"73}74 75You MUST respond with a valid JSON object. Do NOT wrap it in markdown code blocks. Do NOT include any text outside the JSON."""76 77# ---------------------------------------------------------------------------78# Robust JSON extraction from model output79# ---------------------------------------------------------------------------80def extract_json_from_text(text: str) -> dict:81 """Extract and parse JSON from potentially messy model output."""82 text = text.strip()83 84 # Strip <think>...</think> blocks (Qwen3 thinking models)85 text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()86 87 # Strip markdown code fences88 if "```" in text:89 match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)90 if match:91 text = match.group(1).strip()92 93 # Try direct parse first94 try:95 return json.loads(text)96 except json.JSONDecodeError:97 pass98 99 # Try extracting from first { to last }100 start = text.find('{')101 end = text.rfind('}')102 if start != -1 and end != -1 and end > start:103 try:104 return json.loads(text[start:end + 1])105 except json.JSONDecodeError:106 pass107 108 raise ValueError(f"Could not extract valid JSON from model output.")109 110 111# ---------------------------------------------------------------------------112# Model mapping for user-friendly ranking labels113# ---------------------------------------------------------------------------114MODEL_MAPPING = {115 "DeepSeek-R1-Distill-Llama-70B [Best]": "deepseek-ai/DeepSeek-R1-Distill-Llama-70B",116 "Qwen-2.5-72B-Instruct [Better]": "Qwen/Qwen2.5-72B-Instruct",117 "Llama-3.3-70B-Instruct [Good]": "meta-llama/Llama-3.3-70B-Instruct",118}119 120# ---------------------------------------------------------------------------121# Backend logic — smart fallback between JSON mode and plain mode122# ---------------------------------------------------------------------------123def call_hf_cot(problem: str, model_name: str):124 token = os.environ.get("HF_TOKEN")125 client = InferenceClient(token=token if token else None)126 127 messages = [128 {"role": "system", "content": SYSTEM_PROMPT},129 {"role": "user", "content": problem},130 ]131 132 # Attempt 1: Try with response_format (structured JSON mode)133 try:134 response = client.chat.completions.create(135 model=model_name,136 messages=messages,137 max_tokens=3000,138 temperature=0.2,139 response_format={"type": "json_object"},140 )141 raw = response.choices[0].message.content142 return extract_json_from_text(raw)143 except Exception:144 pass # Fall through to plain mode145 146 # Attempt 2: Plain mode — let the system prompt enforce JSON147 response = client.chat.completions.create(148 model=model_name,149 messages=messages,150 max_tokens=3000,151 temperature=0.2,152 )153 raw = response.choices[0].message.content154 return extract_json_from_text(raw)155 156 157def solve_problem(problem, model_name, state):158 if not problem.strip():159 return (160 "",161 "",162 {"steps": [], "final_answer": ""},163 "<div class='status-pill awaiting'>Waiting for input</div>",164 )165 166 t0 = time.time()167 try:168 actual_model = MODEL_MAPPING.get(model_name, model_name)169 result = call_hf_cot(problem, actual_model)170 dt = time.time() - t0171 steps = result.get("steps", [])172 return (173 result.get("final_answer", ""),174 "",175 result,176 (177 f"<div class='metrics-row'>"178 f"<span class='status-pill success'>Done</span>"179 f"<span class='status-pill neutral'>{dt:.2f}s</span>"180 f"<span class='status-pill neutral'>{len(steps)} steps</span>"181 f"</div>"182 ),183 )184 except Exception as exc:185 dt = time.time() - t0186 return (187 f"Error: {exc}",188 "",189 {"steps": [], "final_answer": ""},190 f"<div class='status-pill error'>Failed ({dt:.2f}s)</div>",191 )192 193 194def reveal_cot(state):195 if not state or not state.get("steps"):196 return "No reasoning steps cached yet. Click Solve first."197 return "\n\n".join(state["steps"])198 199 200# ---------------------------------------------------------------------------201# Soothing colour palette (hand-picked)202# ---------------------------------------------------------------------------203cot_theme = gr.themes.Base(204 primary_hue=gr.themes.Color(205 c50="#f0eeff", c100="#dbd6ff", c200="#b8b0ff",206 c300="#9589f5", c400="#7c6fef", c500="#6b5ce7",207 c600="#5a4bd6", c700="#4a3cb8", c800="#3b2f99", c900="#2d2473",208 c950="#1f1850",209 ),210 secondary_hue=gr.themes.Color(211 c50="#edfcf6", c100="#c7f7e4", c200="#93eecb",212 c300="#64d4b2", c400="#43c4a0", c500="#2aaa89",213 c600="#21896e", c700="#1a6b57", c800="#155545", c900="#114537",214 c950="#0a2e24",215 ),216 neutral_hue=gr.themes.Color(217 c50="#f5f4fa", c100="#e8e6f0", c200="#d0cde0",218 c300="#b3afc8", c400="#9e9bb8", c500="#7e7a99",219 c600="#5e5a78", c700="#464360", c800="#3d3d5c",220 c900="#282840", c950="#1e1e2f",221 ),222 font=["Inter", "system-ui", "sans-serif"],223 font_mono=["JetBrains Mono", "Fira Code", "monospace"],224)225 226cot_theme.set(227 body_background_fill="#1e1e2f",228 body_text_color="#e8e6f0",229 block_background_fill="#282840",230 block_border_color="#3d3d5c",231 block_label_text_color="#b3afc8",232 block_title_text_color="#e8e6f0",233 block_shadow="0 4px 24px rgba(0,0,0,0.25)",234 block_radius="14px",235 input_background_fill="#1e1e2f",236 input_border_color="#3d3d5c",237 input_radius="10px",238 button_primary_background_fill="linear-gradient(135deg, #7c6fef 0%, #9589f5 100%)",239 button_primary_text_color="#ffffff",240 button_primary_border_color="transparent",241 button_primary_shadow="0 4px 14px rgba(124,111,239,0.35)",242 button_secondary_background_fill="linear-gradient(135deg, #43c4a0 0%, #64d4b2 100%)",243 button_secondary_text_color="#ffffff",244 button_secondary_border_color="transparent",245 button_secondary_shadow="0 4px 14px rgba(67,196,160,0.30)",246 shadow_drop="0 2px 8px rgba(0,0,0,0.18)",247 checkbox_label_text_color="#b3afc8",248 border_color_primary="#7c6fef",249)250 251# ---------------------------------------------------------------------------252# Custom CSS253# ---------------------------------------------------------------------------254custom_css = """255@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');256 257.gradio-container {258 max-width: 1080px !important;259 margin: 0 auto !important;260}261 262/* Header */263.app-header { text-align: center; padding: 2.5rem 1rem 1.5rem; }264.app-header h1 {265 font-size: 2.4rem !important;266 font-weight: 800 !important;267 letter-spacing: -0.04em !important;268 background: linear-gradient(135deg, #9589f5, #64d4b2);269 -webkit-background-clip: text;270 -webkit-text-fill-color: transparent;271 line-height: 1.2 !important;272 margin: 0 !important;273}274.app-subtitle {275 color: #9e9bb8;276 font-size: 1rem;277 margin-top: 0.5rem;278 font-weight: 400;279}280 281/* Card panels */282.panel {283 background: #282840 !important;284 border: 1px solid #3d3d5c !important;285 border-radius: 16px !important;286 padding: 1.25rem !important;287 box-shadow: 0 8px 30px rgba(0,0,0,0.22) !important;288 transition: border-color 0.25s ease !important;289}290.panel:hover { border-color: #4e4a6e !important; }291 292/* Input fields */293textarea, input[type="text"], input[type="password"] {294 background: #1e1e2f !important;295 border: 1px solid #3d3d5c !important;296 color: #e8e6f0 !important;297 border-radius: 10px !important;298 transition: border-color 0.2s ease, box-shadow 0.2s ease !important;299}300textarea:focus, input:focus {301 border-color: #7c6fef !important;302 box-shadow: 0 0 0 3px rgba(124,111,239,0.15) !important;303 outline: none !important;304}305 306/* Buttons */307.solve-btn {308 background: linear-gradient(135deg, #7c6fef 0%, #9589f5 100%) !important;309 color: #fff !important;310 border: none !important;311 border-radius: 12px !important;312 padding: 0.8rem !important;313 font-weight: 700 !important;314 font-size: 1rem !important;315 box-shadow: 0 4px 16px rgba(124,111,239,0.30) !important;316 transition: transform 0.15s ease, box-shadow 0.15s ease !important;317}318.solve-btn:hover {319 transform: translateY(-2px) !important;320 box-shadow: 0 8px 24px rgba(124,111,239,0.40) !important;321}322.solve-btn:active { transform: translateY(0) !important; }323 324.reveal-btn {325 background: linear-gradient(135deg, #43c4a0 0%, #64d4b2 100%) !important;326 color: #fff !important;327 border: none !important;328 border-radius: 12px !important;329 padding: 0.8rem !important;330 font-weight: 700 !important;331 font-size: 1rem !important;332 box-shadow: 0 4px 16px rgba(67,196,160,0.25) !important;333 transition: transform 0.15s ease, box-shadow 0.15s ease !important;334}335.reveal-btn:hover {336 transform: translateY(-2px) !important;337 box-shadow: 0 8px 24px rgba(67,196,160,0.35) !important;338}339.reveal-btn:active { transform: translateY(0) !important; }340 341/* Metrics badges */342.metrics-area {343 border: none !important;344 background: transparent !important;345 padding: 0 !important;346 margin-top: 0.75rem !important;347}348.metrics-row {349 display: flex; gap: 0.5rem;350 justify-content: center; flex-wrap: wrap;351}352.status-pill {353 display: inline-block;354 padding: 0.3rem 0.75rem;355 border-radius: 20px;356 font-size: 0.8rem;357 font-weight: 600;358}359.status-pill.neutral { background: #3d3d5c; color: #d0cde0; }360.status-pill.success { background: rgba(100,212,178,0.15); color: #64d4b2; border: 1px solid rgba(100,212,178,0.25); }361.status-pill.awaiting { background: rgba(224,179,90,0.12); color: #e0b35a; border: 1px solid rgba(224,179,90,0.20); }362.status-pill.error { background: rgba(232,123,123,0.12); color: #e87b7b; border: 1px solid rgba(232,123,123,0.20); }363 364/* Divider */365.soft-divider {366 border: none; height: 1px;367 background: linear-gradient(90deg, transparent, #3d3d5c, transparent);368 margin: 1rem 0;369}370 371/* Labels */372label, .label-wrap span {373 color: #b3afc8 !important;374 font-weight: 600 !important;375 font-size: 0.9rem !important;376}377 378/* Footer */379.app-footer {380 text-align: center; margin-top: 2.5rem; padding-bottom: 1rem;381 color: #5e5a78; font-size: 0.82rem;382}383.app-footer a { color: #7c6fef; text-decoration: none; }384.app-footer a:hover { text-decoration: underline; }385 386/* Examples */387.examples-table button {388 background: #1e1e2f !important;389 border: 1px solid #3d3d5c !important;390 color: #d0cde0 !important;391 border-radius: 8px !important;392 transition: border-color 0.2s ease !important;393}394.examples-table button:hover {395 border-color: #7c6fef !important;396}397 398/* Scrollbar */399::-webkit-scrollbar { width: 6px; }400::-webkit-scrollbar-track { background: #1e1e2f; }401::-webkit-scrollbar-thumb { background: #3d3d5c; border-radius: 3px; }402::-webkit-scrollbar-thumb:hover { background: #4e4a6e; }403"""404 405# ---------------------------------------------------------------------------406# Gradio Blocks layout407# ---------------------------------------------------------------------------408with gr.Blocks(theme=cot_theme, css=custom_css, title="CoT Reasoner") as demo:409 state = gr.State(value={"steps": [], "final_answer": ""})410 411 gr.HTML(412 """413 <div class="app-header">414 <h1>🧠 Structured CoT Reasoner</h1>415 <p class="app-subtitle">416 Multi-step analytical thinking · powered by <strong>DeepSeek-R1</strong>, <strong>Qwen-72B</strong> & <strong>Llama-3.3</strong>417 </p>418 </div>419 """420 )421 422 with gr.Row(equal_height=False):423 with gr.Column(scale=1, elem_classes="panel"):424 problem_input = gr.Textbox(425 label="Problem",426 placeholder="Describe a riddle, logic puzzle, or math problem…",427 lines=7, max_lines=14,428 )429 model_selector = gr.Dropdown(430 choices=[431 "DeepSeek-R1-Distill-Llama-70B [Best]",432 "Qwen-2.5-72B-Instruct [Better]",433 "Llama-3.3-70B-Instruct [Good]",434 ],435 value="DeepSeek-R1-Distill-Llama-70B [Best]",436 label="Model",437 )438 solve_btn = gr.Button("Solve Problem", elem_classes="solve-btn")439 gr.Examples(440 examples=[441 ["A bat and a ball cost $1.10 in total. The bat costs $1.00 more than the ball. How much does the ball cost?"],442 ["A farmer needs to cross a river with a wolf, a goat, and a cabbage. His boat can only hold himself and one at a time. The wolf eats the goat if alone, the goat eats the cabbage. How can he get everything across?"],443 ["If all Bloops are Razzles and all Razzles are Lizzies, are all Bloops definitely Lizzies?"],444 ],445 inputs=problem_input,446 label="Examples",447 examples_per_page=3,448 )449 450 with gr.Column(scale=1, elem_classes="panel"):451 final_answer_output = gr.Textbox(452 label="Solution",453 placeholder="The answer appears here after solving…",454 lines=5, max_lines=10, interactive=False,455 )456 metrics_output = gr.HTML(457 value="<div class='status-pill awaiting'>Awaiting execution</div>",458 elem_classes="metrics-area",459 )460 gr.HTML('<hr class="soft-divider">')461 reveal_btn = gr.Button("👁️ Reveal Reasoning Steps", elem_classes="reveal-btn")462 cot_output = gr.Textbox(463 label="Reasoning Chain",464 placeholder="Click Reveal above to see step-by-step logic…",465 lines=10, max_lines=20, interactive=False,466 )467 468 gr.HTML(469 """470 <div class="app-footer">471 Powered by <a href="https://huggingface.co/docs/api-inference" target="_blank">Hugging Face Inference API</a>472 </div>473 """474 )475 476 solve_btn.click(477 fn=solve_problem,478 inputs=[problem_input, model_selector, state],479 outputs=[final_answer_output, cot_output, state, metrics_output],480 api_name="solve",481 )482 reveal_btn.click(483 fn=reveal_cot,484 inputs=[state],485 outputs=[cot_output],486 api_name="reveal",487 )488 489if __name__ == "__main__":490 demo.launch()491 