| """ |
| Molecule Maestro – Category Shuffle Edition (Markdown‑Table Output) |
| ------------------------------------------------------------------ |
| * Six category boxes (🔴 Acids, 🔵 Bases, …). |
| * **Reshuffle** keeps 50 % of reactants. |
| * One free‑form **Conditions** textbox. |
| * **NEW:** GPT now returns a ready‑made **Markdown table** – no JSON parsing required. |
| |
| ```bash |
| pip install "gradio>=4" "openai>=1.0" |
| export OPENAI_API_KEY="sk‑…" |
| python molecule_maestro_gradio.py |
| ``` |
| """ |
|
|
| from __future__ import annotations |
| import random |
| from typing import List, Dict |
| from openai import OpenAI |
| import gradio as gr |
|
|
| client = OpenAI() |
|
|
| CATEGORIES = [ |
| ("acid", "🔴 Acids"), |
| ("base", "🔵 Bases"), |
| ("solvent", "🟢 Solvents"), |
| ("gas", "🟡 Gases"), |
| ("oxidizer", "🟣 Oxidizers"), |
| ("other", "⚪ Others"), |
| ] |
|
|
| SYSTEM_PROMPT_PALETTE = ( |
| "You are a helpful chemistry assistant. Provide a JSON array named 'palette' with exactly 12 reactants commonly used in school labs. " |
| "Each item must have fields: name (string) and category (acid, base, solvent, gas, oxidizer, or other)." |
| ) |
|
|
| SYSTEM_PROMPT_REACTION = ( |
| "You are a chemistry mentor. The user will supply 1‑5 reactants and an optional conditions string. " |
| "Respond ONLY with a Markdown table **including the header row**: \n" |
| "| Reactants | Conditions | Reaction Equation | Observations |\n" |
| "|---|---|---|---|\n" |
| "Each subsequent row lists **one** plausible, single‑step, school‑level reaction (max 5 rows total). " |
| "Observations must indicate exothermic or endothermic and an approximate ΔH in kJ, plus a ≤25‑word note. " |
| "If no simple reaction is feasible, output the header row followed by a row stating 'No simple reaction found' in the Reaction Equation column and leave other cells blank." |
| ) |
|
|
|
|
| def chat(prompt: str, system: str, temp: float = 0.4) -> str: |
| return client.chat.completions.create( |
| model="gpt-3.5-turbo", |
| messages=[{"role": "system", "content": system}, {"role": "user", "content": prompt}], |
| temperature=temp, |
| ).choices[0].message.content.strip() |
|
|
|
|
| |
|
|
| def gen_palette() -> List[Dict[str, str]]: |
| try: |
| import json |
| raw = chat("Give me the palette", SYSTEM_PROMPT_PALETTE) |
| return json.loads(raw)["palette"] |
| except Exception: |
| return [ |
| {"name": "HCl", "category": "acid"}, {"name": "NaOH", "category": "base"}, {"name": "Ethanol", "category": "solvent"}, |
| {"name": "Acetic Acid", "category": "acid"}, {"name": "NH3", "category": "base"}, {"name": "H2O", "category": "solvent"}, |
| {"name": "H2", "category": "gas"}, {"name": "O2", "category": "gas"}, {"name": "KMnO4", "category": "oxidizer"}, |
| {"name": "H2SO4", "category": "acid"}, {"name": "NaCl", "category": "other"}, {"name": "CuSO4", "category": "other"}, |
| ] |
|
|
| palette: List[Dict[str, str]] = gen_palette() |
|
|
|
|
| def labels_by_cat(cat: str) -> List[str]: |
| return [item["name"] for item in palette if item["category"] == cat] |
|
|
|
|
| |
|
|
| def evaluate(reactants: List[str], cond: str) -> str: |
| if not reactants: |
| return "⚠️ Select at least one reactant." |
|
|
| prompt = f"Reactants: {', '.join(reactants)}. Conditions: {cond or 'none'}." |
| table = chat(prompt, SYSTEM_PROMPT_REACTION, 0.3) |
| |
| if "| Reactants |" not in table: |
| return f"Parse err 🤖\n```\n{table}\n```" |
| return table |
|
|
|
|
| |
| with gr.Blocks() as demo: |
| gr.Markdown("# 🧪 Molecule Maestro – Category Shuffle Edition") |
|
|
| with gr.Row(): |
| cat_groups = {} |
| for cat, title in CATEGORIES: |
| with gr.Column(): |
| cat_groups[cat] = gr.CheckboxGroup(choices=labels_by_cat(cat), label=title) |
| with gr.Column(): |
| shuffle_btn = gr.Button("🔀 Reshuffle (50 % new)") |
| conditions_tb = gr.Textbox(label="Conditions (optional)") |
| run_btn = gr.Button("▶︎ Generate Reactions") |
|
|
| result_md = gr.Markdown("—") |
|
|
| |
| def _shuffle(): |
| global palette |
| keep = random.sample(palette, len(palette)//2) |
| kept_names = {i['name'] for i in keep} |
| fresh: List[Dict[str, str]] = [] |
| attempts = 0 |
| while len(fresh) < len(palette)//2 and attempts < 5: |
| for item in gen_palette(): |
| if item['name'] not in kept_names and item['name'] not in {f['name'] for f in fresh}: |
| fresh.append(item) |
| if len(fresh) == len(palette)//2: |
| break |
| attempts += 1 |
| palette[:] = keep + fresh |
| random.shuffle(palette) |
| updates = [gr.update(choices=labels_by_cat(cat), value=[]) for cat, _ in CATEGORIES] |
| updates.append("—") |
| return updates |
|
|
| def _run(*inputs): |
| *checkbox_lists, cond = inputs |
| chosen: List[str] = [] |
| for lst in checkbox_lists: |
| chosen.extend(lst) |
| return evaluate(chosen, cond) |
|
|
| shuffle_btn.click(_shuffle, None, [cat_groups[c] for c, _ in CATEGORIES] + [result_md]) |
| run_btn.click(_run, [cat_groups[c] for c, _ in CATEGORIES] + [conditions_tb], result_md) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|