| | """ |
| | 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() |
| |
|