File size: 5,395 Bytes
efb5cee
493bc10
 
 
 
e321e95
493bc10
efb5cee
 
 
e321e95
efb5cee
 
 
 
 
493bc10
efb5cee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493bc10
 
efb5cee
 
 
493bc10
 
 
 
 
 
 
efb5cee
 
 
493bc10
 
efb5cee
 
 
493bc10
efb5cee
 
 
 
 
 
493bc10
 
efb5cee
 
 
493bc10
 
 
 
efb5cee
 
 
 
 
 
 
 
 
 
 
 
 
 
e321e95
493bc10
 
 
 
 
 
efb5cee
 
 
 
 
 
 
 
 
 
493bc10
efb5cee
 
 
 
 
 
 
 
 
 
 
 
e321e95
efb5cee
 
493bc10
efb5cee
 
 
 
 
493bc10
efb5cee
e321e95
493bc10
efb5cee
 
e321e95
 
493bc10
efb5cee
493bc10
 
efb5cee
493bc10
 
efb5cee
 
 
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
"""
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()


# ---------- Palette helpers ----------

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]


# ---------- Reaction evaluation ----------

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)
    # Basic sanity check: ensure header present
    if "| Reactants |" not in table:
        return f"Parse err 🤖\n```\n{table}\n```"
    return table


# ---------- Gradio UI ----------
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("—")

    # ---------- Callbacks ----------
    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()