Vaishnav14220 commited on
Commit
d474c53
Β·
1 Parent(s): d90ea25

Add DeepSeek AI integration for reaction completion: API key input, automatic reaction completion using DeepSeek-V3.2-Exp, enhanced UI with AI options, and OpenAI dependency

Browse files
Files changed (2) hide show
  1. app.py +245 -57
  2. requirements.txt +1 -0
app.py CHANGED
@@ -6,8 +6,9 @@ import re
6
  from functools import partial
7
  from io import StringIO
8
  from textwrap import dedent
9
- from typing import List, Sequence, Tuple
10
  from urllib.parse import quote_plus
 
11
 
12
  import gradio as gr
13
  import pandas as pd
@@ -788,19 +789,147 @@ def _render_smiles_to_svg(smiles_text: str) -> str | None:
788
  return svg
789
 
790
 
791
- def render_reaction_svg(reaction_text: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  reaction_text = (reaction_text or "").strip()
793
  if not reaction_text:
794
  return "", "⚠️ Enter a reaction SMILES/SMARTS string (e.g. CH4.O>>CO2)."
795
- if ">>" not in reaction_text:
796
- return "", "⚠️ Reaction input must include '>>' separating reactants and products."
797
 
798
- svg = _render_smiles_to_svg(reaction_text)
799
- if not svg:
800
- return "", "🚨 Could not parse or render the reaction."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
 
802
- status = "βœ… Reaction rendered successfully."
803
- return svg, status
 
 
 
 
 
804
 
805
 
806
  def _extract_compounds_from_reaction(reaction_text: str) -> List[str]:
@@ -1218,14 +1347,35 @@ def build_interface() -> gr.Blocks:
1218
  with thermo_accordion:
1219
  thermo_data_display = gr.JSON(label="Raw Thermodynamic Data")
1220
 
1221
- # Tab 3: Reaction SVG (Enhanced with NIST reactions)
1222
  with gr.TabItem("Reaction SVG"):
1223
  gr.Markdown(
1224
- "🎨 **Render chemical reactions as SVG using RDKit**\n\n"
1225
- "Choose from NIST database reactions or enter custom SMILES/SMARTS reactions.\n\n"
1226
- "Examples: `CCO.O=C=O>>CC(=O)O` or `[CH3:1].[Cl:2][C@@H](F)[Br]>>[CH3:1][C@@H](F)[Cl]`"
 
 
 
 
1227
  )
1228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1229
  # NIST reactions dropdown
1230
  nist_reactions = _fetch_all_nist_reactions(limit=200)
1231
  nist_reaction_options = [("", "")] + nist_reactions if nist_reactions else []
@@ -1241,31 +1391,32 @@ def build_interface() -> gr.Blocks:
1241
  )
1242
 
1243
  reaction_input = gr.Textbox(
1244
- label="Custom Reaction SMILES/SMARTS",
1245
- placeholder="Reactant1.Reactant2>>Product1.Product2",
1246
- lines=3,
1247
- info="Enter your own reaction in SMILES/SMARTS format"
1248
  )
1249
 
1250
  with gr.Column():
1251
  render_mode = gr.Radio(
1252
  label="Render Mode",
1253
- choices=["Auto (NIST→SMILES)", "Direct SMILES/SMARTS"],
1254
- value="Auto (NIST→SMILES)",
1255
- info="Auto mode converts NIST reaction format to SMILES for RDKit"
1256
  )
1257
 
1258
- render_options = gr.CheckboxGroup(
1259
- label="Render Options",
1260
- choices=["High quality", "Large molecules", "Color atoms"],
1261
- value=["High quality"],
1262
- info="Additional rendering preferences"
1263
  )
1264
 
1265
  # Buttons
1266
  with gr.Row():
1267
- render_nist_btn = gr.Button("🎨 Render Selected NIST Reaction", variant="primary")
1268
- render_custom_btn = gr.Button("πŸ”¬ Render Custom Reaction", variant="secondary")
 
1269
  clear_btn = gr.Button("πŸ—‘οΈ Clear", variant="stop")
1270
 
1271
  # Output
@@ -1285,54 +1436,91 @@ def build_interface() -> gr.Blocks:
1285
  outputs=reaction_input,
1286
  )
1287
 
1288
- # Render NIST reaction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1289
  def render_nist_reaction(reaction_text, options):
1290
  if not reaction_text:
1291
  return "", "⚠️ Please select a reaction from the dropdown or enter a custom reaction."
1292
 
1293
- try:
1294
- svg = _render_reaction_from_nist(reaction_text)
1295
- if svg:
1296
- status = f"βœ… Successfully rendered NIST reaction: {reaction_text[:100]}..."
1297
- if "High quality" in (options or []):
1298
- status += " (High quality mode)"
1299
- return svg, status
1300
- else:
1301
- return "", f"❌ Could not render reaction. The reaction format may not be supported by RDKit: {reaction_text[:100]}..."
1302
- except Exception as exc:
1303
- return "", f"🚨 Error rendering reaction: {exc}"
1304
-
1305
- # Render custom SMILES reaction
1306
- def render_custom_reaction(reaction_text, options):
1307
  if not reaction_text:
1308
  return "", "⚠️ Please enter a reaction in SMILES/SMARTS format."
1309
 
1310
- try:
1311
- svg = _render_smiles_to_svg(reaction_text)
1312
- if svg:
1313
- status = f"βœ… Successfully rendered custom reaction: {reaction_text[:100]}..."
1314
- if "High quality" in (options or []):
1315
- status += " (High quality mode)"
1316
- return svg, status
1317
- else:
1318
- return "", f"❌ Could not parse reaction. Please check your SMILES/SMARTS format: {reaction_text[:100]}..."
1319
- except Exception as exc:
1320
- return "", f"🚨 Error rendering reaction: {exc}"
1321
 
1322
  # Clear function
1323
  def clear_outputs():
1324
  return "", "", ""
1325
 
1326
  # Button handlers
 
 
 
 
 
 
1327
  render_nist_btn.click(
1328
  fn=render_nist_reaction,
1329
- inputs=[reaction_input, render_options],
1330
  outputs=[reaction_svg_output, render_status],
1331
  )
1332
 
1333
- render_custom_btn.click(
1334
- fn=render_custom_reaction,
1335
- inputs=[reaction_input, render_options],
1336
  outputs=[reaction_svg_output, render_status],
1337
  )
1338
 
 
6
  from functools import partial
7
  from io import StringIO
8
  from textwrap import dedent
9
+ from typing import List, Sequence, Tuple, Optional, Dict, Any
10
  from urllib.parse import quote_plus
11
+ import json
12
 
13
  import gradio as gr
14
  import pandas as pd
 
789
  return svg
790
 
791
 
792
+ def _complete_reaction_with_deepseek(partial_reaction: str, api_key: str) -> Optional[str]:
793
+ """Use DeepSeek API to complete missing parts of a chemical reaction."""
794
+ if not api_key or not partial_reaction.strip():
795
+ return None
796
+
797
+ try:
798
+ from openai import OpenAI
799
+
800
+ client = OpenAI(
801
+ api_key=api_key,
802
+ base_url="https://api.deepseek.com",
803
+ )
804
+
805
+ system_prompt = """
806
+ You are a chemistry expert. The user will provide a partial chemical reaction (missing reactants or products).
807
+ Please complete the reaction by inferring the missing components based on chemical knowledge and reaction patterns.
808
+
809
+ Analyze the given reaction and determine what might be missing. Consider:
810
+ - Conservation of mass and atoms
811
+ - Common reaction types (combustion, substitution, addition, etc.)
812
+ - Chemical plausibility
813
+ - Radical reactions, ionic reactions, etc.
814
+
815
+ Output in JSON format with the completed reaction.
816
+
817
+ EXAMPLE INPUT:
818
+ CH4 + O2 β†’ CO2
819
+
820
+ EXAMPLE OUTPUT:
821
+ {"completed_reaction": "CH4 + 2O2 β†’ CO2 + 2H2O", "reasoning": "This is a combustion reaction requiring balanced oxygen and water as product"}
822
+
823
+ EXAMPLE INPUT:
824
+ C2H5β€’ + H2 β†’
825
+
826
+ EXAMPLE OUTPUT:
827
+ {"completed_reaction": "C2H5β€’ + H2 β†’ C2H6 + Hβ€’", "reasoning": "Hydrogen abstraction reaction where ethyl radical abstracts H from H2"}
828
+ """
829
+
830
+ user_prompt = f"Complete this partial chemical reaction: {partial_reaction}"
831
+
832
+ messages = [
833
+ {"role": "system", "content": system_prompt},
834
+ {"role": "user", "content": user_prompt}
835
+ ]
836
+
837
+ response = client.chat.completions.create(
838
+ model="deepseek-chat",
839
+ messages=messages,
840
+ response_format={'type': 'json_object'},
841
+ max_tokens=500,
842
+ temperature=0.1
843
+ )
844
+
845
+ result = json.loads(response.choices[0].message.content)
846
+
847
+ if "completed_reaction" in result:
848
+ return result["completed_reaction"]
849
+
850
+ except Exception as exc:
851
+ print(f"DeepSeek API error: {exc}")
852
+ return None
853
+
854
+ return None
855
+
856
+
857
+ def _analyze_reaction_completeness(reaction_text: str) -> Dict[str, Any]:
858
+ """Analyze if a reaction is complete or needs completion."""
859
+ reaction_text = reaction_text.strip()
860
+
861
+ # Check for reaction arrow
862
+ has_arrow = any(arrow in reaction_text for arrow in ["β†’", "->", "↔", "β‡Œ"])
863
+
864
+ if not has_arrow:
865
+ return {"complete": False, "missing": "reaction arrow", "reason": "No reaction arrow found"}
866
+
867
+ # Split reaction
868
+ parts = None
869
+ for sep in [" β†’ ", " -> ", " ↔ ", " β‡Œ "]:
870
+ if sep in reaction_text:
871
+ parts = reaction_text.split(sep, 1)
872
+ break
873
+
874
+ if not parts or len(parts) != 2:
875
+ return {"complete": False, "missing": "proper format", "reason": "Cannot parse reaction format"}
876
+
877
+ reactants_text, products_text = parts
878
+
879
+ # Check if reactants/products exist
880
+ reactants = [r.strip() for r in reactants_text.split("+") if r.strip()]
881
+ products = [p.strip() for p in products_text.split("+") if p.strip()]
882
+
883
+ if not reactants:
884
+ return {"complete": False, "missing": "reactants", "reason": "No reactants found"}
885
+
886
+ if not products:
887
+ return {"complete": False, "missing": "products", "reason": "No products found"}
888
+
889
+ # Basic completeness check
890
+ if len(reactants) >= 1 and len(products) >= 1:
891
+ return {"complete": True, "reactants": reactants, "products": products}
892
+
893
+ return {"complete": False, "missing": "components", "reason": "Insufficient reaction components"}
894
+
895
+
896
+ def render_reaction_svg(reaction_text: str, api_key: str = "", auto_complete: bool = False):
897
  reaction_text = (reaction_text or "").strip()
898
  if not reaction_text:
899
  return "", "⚠️ Enter a reaction SMILES/SMARTS string (e.g. CH4.O>>CO2)."
 
 
900
 
901
+ # Check if it's already SMILES format (contains >>)
902
+ if ">>" in reaction_text:
903
+ svg = _render_smiles_to_svg(reaction_text)
904
+ if svg:
905
+ status = "βœ… Reaction rendered successfully from SMILES."
906
+ return svg, status
907
+ else:
908
+ return "", "🚨 Could not parse SMILES reaction format."
909
+
910
+ # If not SMILES and auto_complete is enabled, try to complete with DeepSeek
911
+ if auto_complete and api_key:
912
+ analysis = _analyze_reaction_completeness(reaction_text)
913
+ if not analysis["complete"]:
914
+ completed_reaction = _complete_reaction_with_deepseek(reaction_text, api_key)
915
+ if completed_reaction:
916
+ # Try to render the completed reaction
917
+ svg = _render_reaction_from_nist(completed_reaction)
918
+ if svg:
919
+ status = f"βœ… Reaction completed and rendered using DeepSeek AI.\nOriginal: {reaction_text}\nCompleted: {completed_reaction}"
920
+ return svg, status
921
+ else:
922
+ return "", f"🚨 DeepSeek completed reaction but rendering failed: {completed_reaction}"
923
+ else:
924
+ return "", f"🚨 Could not complete reaction with AI. Missing: {analysis.get('missing', 'unknown')}"
925
 
926
+ # Fallback: try NIST format rendering
927
+ svg = _render_reaction_from_nist(reaction_text)
928
+ if svg:
929
+ status = "βœ… Reaction rendered from NIST format."
930
+ return svg, status
931
+
932
+ return "", "🚨 Could not parse or render the reaction. Try SMILES format (reactants>>products) or enable AI completion."
933
 
934
 
935
  def _extract_compounds_from_reaction(reaction_text: str) -> List[str]:
 
1347
  with thermo_accordion:
1348
  thermo_data_display = gr.JSON(label="Raw Thermodynamic Data")
1349
 
1350
+ # Tab 3: Reaction SVG (Enhanced with NIST reactions and AI completion)
1351
  with gr.TabItem("Reaction SVG"):
1352
  gr.Markdown(
1353
+ "🎨 **Render chemical reactions as SVG using RDKit + AI Completion**\n\n"
1354
+ "Choose from NIST database reactions, enter custom reactions, or let AI complete partial reactions!\n\n"
1355
+ "**Features:**\n"
1356
+ "- πŸ§ͺ 200+ NIST database reactions\n"
1357
+ "- πŸ€– AI-powered reaction completion (DeepSeek)\n"
1358
+ "- πŸ”¬ Multiple input formats (NIST, SMILES, SMARTS)\n"
1359
+ "- ⚑ Automatic format detection and conversion"
1360
  )
1361
 
1362
+ # API Key Configuration
1363
+ with gr.Accordion("πŸ”‘ DeepSeek API Configuration", open=False):
1364
+ deepseek_api_key = gr.Textbox(
1365
+ label="DeepSeek API Key",
1366
+ placeholder="sk-...",
1367
+ type="password",
1368
+ info="Get your API key from https://platform.deepseek.com/"
1369
+ )
1370
+ gr.Markdown(
1371
+ "**How to get API key:**\n"
1372
+ "1. Visit https://platform.deepseek.com/\n"
1373
+ "2. Sign up/Login to your account\n"
1374
+ "3. Go to API Keys section\n"
1375
+ "4. Create a new API key\n"
1376
+ "5. Copy and paste it here"
1377
+ )
1378
+
1379
  # NIST reactions dropdown
1380
  nist_reactions = _fetch_all_nist_reactions(limit=200)
1381
  nist_reaction_options = [("", "")] + nist_reactions if nist_reactions else []
 
1391
  )
1392
 
1393
  reaction_input = gr.Textbox(
1394
+ label="Custom Reaction Input",
1395
+ placeholder="Enter reaction in any format:\nNIST: CH4 + O2 β†’ CO2 + H2O\nSMILES: CH4.O2>>CO2.H2O\nPartial: CH4 + O2 β†’ (AI will complete)",
1396
+ lines=4,
1397
+ info="Supports NIST format, SMILES/SMARTS, or partial reactions"
1398
  )
1399
 
1400
  with gr.Column():
1401
  render_mode = gr.Radio(
1402
  label="Render Mode",
1403
+ choices=["Auto (detect format)", "Force NIST format", "Force SMILES/SMARTS"],
1404
+ value="Auto (detect format)",
1405
+ info="Auto mode intelligently detects and converts formats"
1406
  )
1407
 
1408
+ ai_options = gr.CheckboxGroup(
1409
+ label="πŸ€– AI Enhancement",
1410
+ choices=["Enable AI completion", "High quality rendering", "Show completion reasoning"],
1411
+ value=["Enable AI completion"],
1412
+ info="Use DeepSeek AI to complete partial reactions"
1413
  )
1414
 
1415
  # Buttons
1416
  with gr.Row():
1417
+ render_auto_btn = gr.Button("πŸš€ Smart Render (Auto-detect)", variant="primary")
1418
+ render_nist_btn = gr.Button("πŸ§ͺ Render NIST Format", variant="secondary")
1419
+ render_smiles_btn = gr.Button("πŸ”¬ Render SMILES/SMARTS", variant="secondary")
1420
  clear_btn = gr.Button("πŸ—‘οΈ Clear", variant="stop")
1421
 
1422
  # Output
 
1436
  outputs=reaction_input,
1437
  )
1438
 
1439
+ # Smart auto-render function
1440
+ def render_auto_reaction(reaction_text, api_key, ai_options, render_mode):
1441
+ if not reaction_text:
1442
+ return "", "⚠️ Please enter a reaction or select from the NIST dropdown."
1443
+
1444
+ status_prefix = ""
1445
+ final_reaction = reaction_text
1446
+
1447
+ # Check if AI completion is enabled and reaction might be incomplete
1448
+ if "Enable AI completion" in (ai_options or []) and api_key:
1449
+ analysis = _analyze_reaction_completeness(reaction_text)
1450
+ if not analysis["complete"]:
1451
+ completed_reaction = _complete_reaction_with_deepseek(reaction_text, api_key)
1452
+ if completed_reaction:
1453
+ final_reaction = completed_reaction
1454
+ status_prefix = f"πŸ€– **AI Completed Reaction**\nOriginal: {reaction_text}\nCompleted: {final_reaction}\n\n"
1455
+
1456
+ # Render based on detected format or forced mode
1457
+ if render_mode == "Force SMILES/SMARTS" or (render_mode == "Auto (detect format)" and ">>" in final_reaction):
1458
+ svg = _render_smiles_to_svg(final_reaction)
1459
+ render_type = "SMILES/SMARTS"
1460
+ elif render_mode == "Force NIST format" or render_mode == "Auto (detect format)":
1461
+ svg = _render_reaction_from_nist(final_reaction)
1462
+ render_type = "NIST format"
1463
+ else:
1464
+ # Try both formats
1465
+ svg = _render_reaction_from_nist(final_reaction)
1466
+ if not svg:
1467
+ svg = _render_smiles_to_svg(final_reaction)
1468
+ render_type = "auto-detected"
1469
+
1470
+ if svg:
1471
+ quality_note = " (High quality)" if "High quality rendering" in (ai_options or []) else ""
1472
+ status = f"{status_prefix}βœ… Successfully rendered as {render_type}{quality_note}"
1473
+ return svg, status
1474
+ else:
1475
+ return "", f"{status_prefix}❌ Could not render reaction. Try a different format or check syntax: {final_reaction[:100]}..."
1476
+
1477
+ # Legacy render functions (kept for compatibility)
1478
  def render_nist_reaction(reaction_text, options):
1479
  if not reaction_text:
1480
  return "", "⚠️ Please select a reaction from the dropdown or enter a custom reaction."
1481
 
1482
+ svg = _render_reaction_from_nist(reaction_text)
1483
+ if svg:
1484
+ status = f"βœ… Successfully rendered NIST reaction: {reaction_text[:100]}..."
1485
+ if "High quality" in (options or []):
1486
+ status += " (High quality mode)"
1487
+ return svg, status
1488
+ else:
1489
+ return "", f"❌ Could not render reaction. The reaction format may not be supported by RDKit: {reaction_text[:100]}..."
1490
+
1491
+ def render_smiles_reaction(reaction_text, options):
 
 
 
 
1492
  if not reaction_text:
1493
  return "", "⚠️ Please enter a reaction in SMILES/SMARTS format."
1494
 
1495
+ svg = _render_smiles_to_svg(reaction_text)
1496
+ if svg:
1497
+ status = f"βœ… Successfully rendered SMILES reaction: {reaction_text[:100]}..."
1498
+ if "High quality" in (options or []):
1499
+ status += " (High quality mode)"
1500
+ return svg, status
1501
+ else:
1502
+ return "", f"❌ Could not parse reaction. Please check your SMILES/SMARTS format: {reaction_text[:100]}..."
 
 
 
1503
 
1504
  # Clear function
1505
  def clear_outputs():
1506
  return "", "", ""
1507
 
1508
  # Button handlers
1509
+ render_auto_btn.click(
1510
+ fn=render_auto_reaction,
1511
+ inputs=[reaction_input, deepseek_api_key, ai_options, render_mode],
1512
+ outputs=[reaction_svg_output, render_status],
1513
+ )
1514
+
1515
  render_nist_btn.click(
1516
  fn=render_nist_reaction,
1517
+ inputs=[reaction_input, ai_options],
1518
  outputs=[reaction_svg_output, render_status],
1519
  )
1520
 
1521
+ render_smiles_btn.click(
1522
+ fn=render_smiles_reaction,
1523
+ inputs=[reaction_input, ai_options],
1524
  outputs=[reaction_svg_output, render_status],
1525
  )
1526
 
requirements.txt CHANGED
@@ -10,3 +10,4 @@ fastapi
10
  uvicorn
11
  plotly
12
  pandas
 
 
10
  uvicorn
11
  plotly
12
  pandas
13
+ openai