mgbam commited on
Commit
4290ea7
·
verified ·
1 Parent(s): 305d993

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -353
app.py CHANGED
@@ -12,7 +12,7 @@ import torch
12
  from dotenv import load_dotenv
13
  from loguru import logger
14
  from huggingface_hub import login
15
- from openai import OpenAI
16
  from reportlab.pdfgen import canvas
17
  from transformers import (
18
  AutoTokenizer,
@@ -30,24 +30,17 @@ import PyPDF2
30
  # 1) ENVIRONMENT & LOGGING #
31
  ###############################################################################
32
 
33
- # Ensure spaCy model is downloaded (English Core Web)
34
- try:
35
- nlp = spacy.load("en_core_web_sm")
36
- except OSError:
37
- logger.info("Downloading SpaCy 'en_core_web_sm' model...")
38
- spacy.cli.download("en_core_web_sm")
39
- nlp = spacy.load("en_core_web_sm")
40
-
41
- # Logging
42
  logger.add("error_logs.log", rotation="1 MB", level="ERROR")
43
 
44
  # Load environment variables
45
  load_dotenv()
46
  HUGGINGFACE_TOKEN = os.getenv("HF_TOKEN")
47
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
48
- BIOPORTAL_API_KEY = os.getenv("BIOPORTAL_API_KEY") # For BioPortal integration
49
  ENTREZ_EMAIL = os.getenv("ENTREZ_EMAIL")
50
 
 
51
  if not HUGGINGFACE_TOKEN or not OPENAI_API_KEY:
52
  logger.error("Missing Hugging Face or OpenAI credentials.")
53
  raise ValueError("Missing credentials for Hugging Face or OpenAI.")
@@ -59,42 +52,52 @@ if not BIOPORTAL_API_KEY:
59
  # Hugging Face login
60
  login(HUGGINGFACE_TOKEN)
61
 
62
- # OpenAI
63
- client = OpenAI(api_key=OPENAI_API_KEY)
64
 
65
- # Device: CPU or GPU
66
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
67
  logger.info(f"Using device: {device}")
68
 
 
 
 
 
 
 
 
 
69
  ###############################################################################
70
  # 2) HUGGING FACE & TRANSLATION MODEL SETUP #
71
  ###############################################################################
72
 
73
- MODEL_NAME = "mgbam/bert-base-finetuned-mgbam"
 
74
  try:
75
- model = AutoModelForSequenceClassification.from_pretrained(
76
- MODEL_NAME, use_auth_token=HUGGINGFACE_TOKEN
77
  ).to(device)
78
- tokenizer = AutoTokenizer.from_pretrained(
79
- MODEL_NAME, use_auth_token=HUGGINGFACE_TOKEN
80
  )
81
  except Exception as e:
82
- logger.error(f"Model load error: {e}")
83
  raise
84
 
 
 
85
  try:
86
- translation_model_name = "Helsinki-NLP/opus-mt-en-fr"
87
  translation_model = MarianMTModel.from_pretrained(
88
- translation_model_name, use_auth_token=HUGGINGFACE_TOKEN
89
  ).to(device)
90
  translation_tokenizer = MarianTokenizer.from_pretrained(
91
- translation_model_name, use_auth_token=HUGGINGFACE_TOKEN
92
  )
93
  except Exception as e:
94
  logger.error(f"Translation model load error: {e}")
95
  raise
96
 
97
- # Language map for translation
98
  LANGUAGE_MAP: Dict[str, Tuple[str, str]] = {
99
  "English to French": ("en", "fr"),
100
  "French to English": ("fr", "en"),
@@ -155,6 +158,7 @@ def parse_pubmed_xml(xml_data: str) -> List[Dict[str, Any]]:
155
  ###############################################################################
156
 
157
  async def fetch_articles_by_nct_id(nct_id: str) -> Dict[str, Any]:
 
158
  params = {"query": nct_id, "format": "json"}
159
  async with httpx.AsyncClient() as client_http:
160
  try:
@@ -166,7 +170,7 @@ async def fetch_articles_by_nct_id(nct_id: str) -> Dict[str, Any]:
166
  return {"error": str(e)}
167
 
168
  async def fetch_articles_by_query(query_params: str) -> Dict[str, Any]:
169
- """Europe PMC query via JSON input."""
170
  parsed_params = safe_json_parse(query_params)
171
  if not parsed_params or not isinstance(parsed_params, dict):
172
  return {"error": "Invalid JSON."}
@@ -178,10 +182,11 @@ async def fetch_articles_by_query(query_params: str) -> Dict[str, Any]:
178
  resp.raise_for_status()
179
  return resp.json()
180
  except Exception as e:
181
- logger.error(f"Error fetching articles: {e}")
182
  return {"error": str(e)}
183
 
184
  async def fetch_pubmed_by_query(query_params: str) -> Dict[str, Any]:
 
185
  parsed_params = safe_json_parse(query_params)
186
  if not parsed_params or not isinstance(parsed_params, dict):
187
  return {"error": "Invalid JSON for PubMed."}
@@ -203,7 +208,7 @@ async def fetch_pubmed_by_query(query_params: str) -> Dict[str, Any]:
203
  if not id_list:
204
  return {"result": ""}
205
 
206
- # Fetch PubMed
207
  fetch_params = {
208
  "db": "pubmed",
209
  "id": ",".join(id_list),
@@ -218,6 +223,7 @@ async def fetch_pubmed_by_query(query_params: str) -> Dict[str, Any]:
218
  return {"error": str(e)}
219
 
220
  async def fetch_crossref_by_query(query_params: str) -> Dict[str, Any]:
 
221
  parsed_params = safe_json_parse(query_params)
222
  if not parsed_params or not isinstance(parsed_params, dict):
223
  return {"error": "Invalid JSON for Crossref."}
@@ -232,9 +238,8 @@ async def fetch_crossref_by_query(query_params: str) -> Dict[str, Any]:
232
 
233
  async def fetch_bioportal_by_query(query_params: str) -> Dict[str, Any]:
234
  """
235
- BioPortal fetch for medical ontologies/terminologies.
236
  Expects JSON like: {"q": "cancer"}
237
- See: https://data.bioontology.org/documentation
238
  """
239
  if not BIOPORTAL_API_KEY:
240
  return {"error": "No BioPortal API Key set."}
@@ -264,14 +269,14 @@ async def fetch_bioportal_by_query(query_params: str) -> Dict[str, Any]:
264
  ###############################################################################
265
 
266
  def summarize_text(text: str) -> str:
267
- """OpenAI GPT-3.5 summarization."""
268
  if not text.strip():
269
  return "No text provided for summarization."
270
  try:
271
- response = client.chat.completions.create(
272
  model="gpt-3.5-turbo",
273
  messages=[{"role": "user", "content": f"Summarize this clinical data:\n{text}"}],
274
- max_tokens=200,
275
  temperature=0.7,
276
  )
277
  return response.choices[0].message.content.strip()
@@ -280,33 +285,66 @@ def summarize_text(text: str) -> str:
280
  return "Summarization failed."
281
 
282
  def predict_outcome(text: str) -> Union[Dict[str, float], str]:
283
- """Predict outcomes (classification) using a fine-tuned BERT model."""
284
  if not text.strip():
285
  return "No text provided for prediction."
286
  try:
287
- inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
288
  inputs = {k: v.to(device) for k, v in inputs.items()}
289
  with torch.no_grad():
290
- outputs = model(**inputs)
291
  probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
292
- return {f"Label {i+1}": float(prob.item()) for i, prob in enumerate(probabilities)}
 
293
  except Exception as e:
294
  logger.error(f"Prediction error: {e}")
295
  return "Prediction failed."
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  def generate_report(text: str, filename: str = "clinical_report.pdf") -> Optional[str]:
298
- """Generate a professional PDF report from the text."""
299
  try:
300
  if not text.strip():
301
  logger.warning("No text provided for the report.")
302
  c = canvas.Canvas(filename)
303
- c.drawString(100, 750, "Clinical Research Report")
 
 
304
  lines = text.split("\n")
305
- y = 730
306
  for line in lines:
307
  if y < 50:
308
  c.showPage()
309
- y = 750
 
310
  c.drawString(100, y, line)
311
  y -= 15
312
  c.save()
@@ -317,7 +355,7 @@ def generate_report(text: str, filename: str = "clinical_report.pdf") -> Optiona
317
  return None
318
 
319
  def visualize_predictions(predictions: Dict[str, float]) -> alt.Chart:
320
- """Simple Altair bar chart to visualize classification probabilities."""
321
  data = pd.DataFrame(list(predictions.items()), columns=["Label", "Probability"])
322
  chart = (
323
  alt.Chart(data)
@@ -331,126 +369,253 @@ def visualize_predictions(predictions: Dict[str, float]) -> alt.Chart:
331
  )
332
  return chart
333
 
334
- def translate_text(text: str, translation_option: str) -> str:
335
- """Translate text between English and French via MarianMT."""
336
- if not text.strip():
337
- return "No text provided for translation."
338
- try:
339
- if translation_option not in LANGUAGE_MAP:
340
- return "Unsupported translation option."
341
- inputs = translation_tokenizer(text, return_tensors="pt", padding=True).to(device)
342
- translated_tokens = translation_model.generate(**inputs)
343
- return translation_tokenizer.decode(translated_tokens[0], skip_special_tokens=True)
344
- except Exception as e:
345
- logger.error(f"Translation error: {e}")
346
- return "Translation failed."
347
-
348
- def perform_named_entity_recognition(text: str) -> str:
349
- """NER using spaCy (en_core_web_sm)."""
350
- if not text.strip():
351
- return "No text provided for NER."
352
  try:
353
- doc = nlp(text)
354
- entities = [(ent.text, ent.label_) for ent in doc.ents]
355
- if not entities:
356
- return "No named entities found."
357
- return "\n".join(f"{t} -> {lbl}" for t, lbl in entities)
 
 
 
 
 
358
  except Exception as e:
359
- logger.error(f"NER error: {e}")
360
- return "NER failed."
361
 
362
  ###############################################################################
363
  # 7) FILE PARSING (TXT, PDF, CSV, XLS) #
364
  ###############################################################################
365
 
366
  def parse_pdf_file_as_str(file_up: gr.File) -> str:
367
- """Read PDF via PyPDF2. Attempt local path, else read from memory."""
368
- pdf_path = file_up.name
369
- if os.path.isfile(pdf_path):
370
- with open(pdf_path, "rb") as f:
371
- reader = PyPDF2.PdfReader(f)
372
- return "\n".join(page.extract_text() or "" for page in reader.pages)
373
- else:
374
- if not hasattr(file_up, "file"):
375
- raise ValueError("No .file attribute found for PDF.")
376
- pdf_bytes = file_up.file.read()
377
  reader = PyPDF2.PdfReader(io.BytesIO(pdf_bytes))
378
  return "\n".join(page.extract_text() or "" for page in reader.pages)
 
 
 
379
 
380
  def parse_text_file_as_str(file_up: gr.File) -> str:
381
- """Read .txt from path or fallback to memory."""
382
- path = file_up.name
383
- if os.path.isfile(path):
384
- with open(path, "rb") as f:
385
- return f.read().decode("utf-8", errors="replace")
386
- else:
387
- if not hasattr(file_up, "file"):
388
- raise ValueError("No .file attribute for TXT.")
389
- return file_up.file.read().decode("utf-8", errors="replace")
390
 
391
  def parse_csv_file_to_df(file_up: gr.File) -> pd.DataFrame:
392
- """
393
- Attempt multiple encodings for CSV: utf-8, utf-8-sig, latin1, ISO-8859-1.
394
- """
395
- path = file_up.name
396
- if os.path.isfile(path):
397
- for enc in ["utf-8", "utf-8-sig", "latin1", "ISO-8859-1"]:
398
- try:
399
- return pd.read_csv(path, encoding=enc)
400
- except UnicodeDecodeError:
401
- logger.warning(f"CSV parse failed (enc={enc}). Trying next...")
402
- except Exception as e:
403
- logger.warning(f"CSV parse error (enc={enc}): {e}")
404
- raise ValueError("Could not parse local CSV with known encodings.")
405
- else:
406
- if not hasattr(file_up, "file"):
407
- raise ValueError("No .file attribute for CSV.")
408
- raw_bytes = file_up.file.read()
409
- for enc in ["utf-8", "utf-8-sig", "latin1", "ISO-8859-1"]:
410
- try:
411
- text_decoded = raw_bytes.decode(enc, errors="replace")
412
- from io import StringIO
413
- return pd.read_csv(StringIO(text_decoded))
414
- except UnicodeDecodeError:
415
- logger.warning(f"CSV in-memory parse failed (enc={enc}). Next...")
416
- except Exception as e:
417
- logger.warning(f"In-memory CSV error (enc={enc}): {e}")
418
- raise ValueError("Could not parse in-memory CSV with known encodings.")
419
 
420
  def parse_excel_file_to_df(file_up: gr.File) -> pd.DataFrame:
421
- """Read Excel from local path or memory (openpyxl)."""
422
- path = file_up.name
423
- if os.path.isfile(path):
424
- return pd.read_excel(path, engine="openpyxl")
425
- else:
426
- if not hasattr(file_up, "file"):
427
- raise ValueError("No .file attribute for Excel.")
428
- excel_bytes = file_up.file.read()
429
- return pd.read_excel(io.BytesIO(excel_bytes), engine="openpyxl")
430
 
431
  ###############################################################################
432
  # 8) BUILDING THE GRADIO APP #
433
  ###############################################################################
434
 
435
- with gr.Blocks() as demo:
436
- gr.Markdown("# 🏥 AI-Driven Clinical Assistant (No EDA)")
437
- gr.Markdown("""
438
- **Highlights**:
439
- - **Summarize** clinical text (OpenAI GPT-3.5)
440
- - **Predict** with a specialized BERT-based model
441
- - **Translate** (English French)
442
- - **Named Entity Recognition** (spaCy)
443
- - **Fetch** from PubMed, Crossref, Europe PMC, and **BioPortal**
444
- - **Generate** professional PDF reports
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
- *Disclaimer*: This is a research demo, **not** a medical device.
447
- """)
 
448
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  with gr.Row():
450
- text_input = gr.Textbox(label="Input Text", lines=5, placeholder="Enter clinical text or notes...")
 
 
 
 
 
451
  file_input = gr.File(
452
- label="Upload File (txt/csv/xls/xlsx/pdf)",
453
- file_types=[".txt", ".csv", ".xls", ".xlsx", ".pdf"]
 
454
  )
455
 
456
  action = gr.Radio(
@@ -465,239 +630,84 @@ with gr.Blocks() as demo:
465
  "Fetch PubMed by Query",
466
  "Fetch Crossref by Query",
467
  "Fetch BioPortal by Query",
 
468
  ],
469
  label="Select an Action",
 
470
  )
 
471
  translation_option = gr.Dropdown(
472
  choices=list(LANGUAGE_MAP.keys()),
473
  label="Translation Option",
474
- value="English to French"
 
475
  )
 
476
  query_params_input = gr.Textbox(
477
- label="Query Params (JSON)",
478
- placeholder='{"term": "cancer"} or {"q": "cancer"} for BioPortal'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  )
480
- nct_id_input = gr.Textbox(label="NCT ID")
481
- report_filename_input = gr.Textbox(label="Report Filename", value="clinical_report.pdf")
482
- export_format = gr.Dropdown(choices=["None", "CSV", "JSON"], label="Export Format")
483
 
484
  # Outputs
485
- output_text = gr.Textbox(label="Output", lines=8)
 
 
 
 
 
486
  with gr.Row():
487
- output_chart = gr.Plot(label="Chart 1")
488
- output_chart2 = gr.Plot(label="Chart 2")
489
- output_file = gr.File(label="Generated File")
490
 
491
- submit_btn = gr.Button("Submit")
492
-
493
- ################################################################
494
- # 9) MAIN ACTION HANDLER (ASYNC) #
495
- ################################################################
496
- import traceback
497
-
498
- async def handle_action(
499
- action: str,
500
- txt: str,
501
- file_up: gr.File,
502
- translation_opt: str,
503
- query_str: str,
504
- nct_id: str,
505
- report_fn: str,
506
- exp_fmt: str
507
- ) -> Tuple[Optional[str], Optional[Any], Optional[Any], Optional[str]]:
508
- """
509
- Master function to handle user actions.
510
- Returns a 4-tuple mapped to (output_text, output_chart, output_chart2, output_file).
511
- """
512
- try:
513
- combined_text = txt.strip()
514
-
515
- # 1) If user uploaded a file, parse minimal text from .txt/.pdf here
516
- if file_up is not None:
517
- ext = os.path.splitext(file_up.name)[1].lower()
518
- if ext == ".txt":
519
- try:
520
- txt_data = parse_text_file_as_str(file_up)
521
- combined_text += "\n" + txt_data
522
- except Exception as e:
523
- return f"TXT parse error: {e}", None, None, None
524
- elif ext == ".pdf":
525
- try:
526
- pdf_data = parse_pdf_file_as_str(file_up)
527
- combined_text += "\n" + pdf_data
528
- except Exception as e:
529
- return f"PDF parse error: {e}", None, None, None
530
- # CSV and Excel are parsed *within* certain actions (e.g. Summarize)
531
-
532
- # 2) Branch by action
533
- if action == "Summarize":
534
- if file_up:
535
- fx = file_up.name.lower()
536
- if fx.endswith(".csv"):
537
- try:
538
- df_csv = parse_csv_file_to_df(file_up)
539
- combined_text += "\n" + df_csv.to_csv(index=False)
540
- except Exception as e:
541
- return f"CSV parse error (Summarize): {e}", None, None, None
542
- elif fx.endswith((".xls", ".xlsx")):
543
- try:
544
- df_xl = parse_excel_file_to_df(file_up)
545
- combined_text += "\n" + df_xl.to_csv(index=False)
546
- except Exception as e:
547
- return f"Excel parse error (Summarize): {e}", None, None, None
548
-
549
- summary = summarize_text(combined_text)
550
- return summary, None, None, None
551
-
552
- elif action == "Predict Outcome":
553
- if file_up:
554
- fx = file_up.name.lower()
555
- if fx.endswith(".csv"):
556
- try:
557
- df_csv = parse_csv_file_to_df(file_up)
558
- combined_text += "\n" + df_csv.to_csv(index=False)
559
- except Exception as e:
560
- return f"CSV parse error (Predict): {e}", None, None, None
561
- elif fx.endswith((".xls", ".xlsx")):
562
- try:
563
- df_xl = parse_excel_file_to_df(file_up)
564
- combined_text += "\n" + df_xl.to_csv(index=False)
565
- except Exception as e:
566
- return f"Excel parse error (Predict): {e}", None, None, None
567
-
568
- preds = predict_outcome(combined_text)
569
- if isinstance(preds, dict):
570
- chart = visualize_predictions(preds)
571
- return json.dumps(preds, indent=2), chart, None, None
572
- return preds, None, None, None
573
-
574
- elif action == "Generate Report":
575
- if file_up:
576
- fx = file_up.name.lower()
577
- if fx.endswith(".csv"):
578
- try:
579
- df_csv = parse_csv_file_to_df(file_up)
580
- combined_text += "\n" + df_csv.to_csv(index=False)
581
- except Exception as e:
582
- return f"CSV parse error (Report): {e}", None, None, None
583
- elif fx.endswith((".xls", ".xlsx")):
584
- try:
585
- df_xl = parse_excel_file_to_df(file_up)
586
- combined_text += "\n" + df_xl.to_csv(index=False)
587
- except Exception as e:
588
- return f"Excel parse error (Report): {e}", None, None, None
589
-
590
- path = generate_report(combined_text, report_fn)
591
- msg = f"Report generated: {path}" if path else "Report generation failed."
592
- return msg, None, None, path
593
-
594
- elif action == "Translate":
595
- if file_up:
596
- fx = file_up.name.lower()
597
- if fx.endswith(".csv"):
598
- try:
599
- df_csv = parse_csv_file_to_df(file_up)
600
- combined_text += "\n" + df_csv.to_csv(index=False)
601
- except Exception as e:
602
- return f"CSV parse error (Translate): {e}", None, None, None
603
- elif fx.endswith((".xls", ".xlsx")):
604
- try:
605
- df_xl = parse_excel_file_to_df(file_up)
606
- combined_text += "\n" + df_xl.to_csv(index=False)
607
- except Exception as e:
608
- return f"Excel parse error (Translate): {e}", None, None, None
609
-
610
- translated = translate_text(combined_text, translation_opt)
611
- return translated, None, None, None
612
-
613
- elif action == "Perform Named Entity Recognition":
614
- if file_up:
615
- fx = file_up.name.lower()
616
- if fx.endswith(".csv"):
617
- try:
618
- df_csv = parse_csv_file_to_df(file_up)
619
- combined_text += "\n" + df_csv.to_csv(index=False)
620
- except Exception as e:
621
- return f"CSV parse error (NER): {e}", None, None, None
622
- elif fx.endswith((".xls", ".xlsx")):
623
- try:
624
- df_xl = parse_excel_file_to_df(file_up)
625
- combined_text += "\n" + df_xl.to_csv(index=False)
626
- except Exception as e:
627
- return f"Excel parse error (NER): {e}", None, None, None
628
-
629
- ner_result = perform_named_entity_recognition(combined_text)
630
- return ner_result, None, None, None
631
-
632
- elif action == "Fetch Clinical Studies":
633
- if nct_id:
634
- result = await fetch_articles_by_nct_id(nct_id)
635
- elif query_str:
636
- result = await fetch_articles_by_query(query_str)
637
- else:
638
- return "Provide either an NCT ID or valid query parameters.", None, None, None
639
-
640
- articles = result.get("resultList", {}).get("result", [])
641
- if not articles:
642
- return "No articles found.", None, None, None
643
-
644
- formatted = "\n\n".join(
645
- f"Title: {a.get('title')}\nJournal: {a.get('journalTitle')} ({a.get('pubYear')})"
646
- for a in articles
647
- )
648
- return formatted, None, None, None
649
-
650
- elif action in ["Fetch PubMed Articles (Legacy)", "Fetch PubMed by Query"]:
651
- pubmed_result = await fetch_pubmed_by_query(query_str)
652
- xml_data = pubmed_result.get("result")
653
- if xml_data:
654
- articles = parse_pubmed_xml(xml_data)
655
- if not articles:
656
- return "No articles found.", None, None, None
657
- formatted = "\n\n".join(
658
- f"{a['Title']} - {a['Journal']} ({a['PublicationDate']})"
659
- for a in articles if a['Title']
660
- )
661
- return formatted if formatted else "No articles found.", None, None, None
662
- return "No articles found or error in fetching PubMed data.", None, None, None
663
-
664
- elif action == "Fetch Crossref by Query":
665
- crossref_result = await fetch_crossref_by_query(query_str)
666
- items = crossref_result.get("message", {}).get("items", [])
667
- if not items:
668
- return "No results found.", None, None, None
669
- crossref_formatted = "\n\n".join(
670
- f"Title: {it.get('title', ['No title'])[0]}, DOI: {it.get('DOI')}"
671
- for it in items
672
- )
673
- return crossref_formatted, None, None, None
674
-
675
- elif action == "Fetch BioPortal by Query":
676
- bp_result = await fetch_bioportal_by_query(query_str)
677
- collection = bp_result.get("collection", [])
678
- if not collection:
679
- return "No BioPortal results found.", None, None, None
680
- # Format listing
681
- formatted = "\n\n".join(
682
- f"Label: {col.get('prefLabel')}, ID: {col.get('@id')}"
683
- for col in collection
684
- )
685
- return formatted, None, None, None
686
-
687
- # Fallback
688
- return "Invalid action.", None, None, None
689
-
690
- except Exception as ex:
691
- # Catch all exceptions, log, and return traceback to 'output_text'
692
- tb_str = traceback.format_exc()
693
- logger.error(f"Exception in handle_action:\n{tb_str}")
694
- return f"Traceback:\n{tb_str}", None, None, None
695
-
696
  submit_btn.click(
697
- fn=handle_action,
698
- inputs=[action, text_input, file_input, translation_option, query_params_input, nct_id_input, report_filename_input, export_format],
 
 
699
  outputs=[output_text, output_chart, output_chart2, output_file],
700
  )
701
 
 
 
 
 
702
  # Launch the Gradio interface
703
  demo.launch(server_name="0.0.0.0", server_port=7860, share=True)
 
12
  from dotenv import load_dotenv
13
  from loguru import logger
14
  from huggingface_hub import login
15
+ import openai
16
  from reportlab.pdfgen import canvas
17
  from transformers import (
18
  AutoTokenizer,
 
30
  # 1) ENVIRONMENT & LOGGING #
31
  ###############################################################################
32
 
33
+ # Initialize Logging
 
 
 
 
 
 
 
 
34
  logger.add("error_logs.log", rotation="1 MB", level="ERROR")
35
 
36
  # Load environment variables
37
  load_dotenv()
38
  HUGGINGFACE_TOKEN = os.getenv("HF_TOKEN")
39
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
40
+ BIOPORTAL_API_KEY = os.getenv("BIOPORTAL_API_KEY")
41
  ENTREZ_EMAIL = os.getenv("ENTREZ_EMAIL")
42
 
43
+ # Validate API Keys
44
  if not HUGGINGFACE_TOKEN or not OPENAI_API_KEY:
45
  logger.error("Missing Hugging Face or OpenAI credentials.")
46
  raise ValueError("Missing credentials for Hugging Face or OpenAI.")
 
52
  # Hugging Face login
53
  login(HUGGINGFACE_TOKEN)
54
 
55
+ # OpenAI Initialization
56
+ openai.api_key = OPENAI_API_KEY
57
 
58
+ # Device Configuration
59
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
60
  logger.info(f"Using device: {device}")
61
 
62
+ # Ensure spaCy model is downloaded (English Core Web)
63
+ try:
64
+ nlp = spacy.load("en_core_web_sm")
65
+ except OSError:
66
+ logger.info("Downloading SpaCy 'en_core_web_sm' model...")
67
+ spacy.cli.download("en_core_web_sm")
68
+ nlp = spacy.load("en_core_web_sm")
69
+
70
  ###############################################################################
71
  # 2) HUGGING FACE & TRANSLATION MODEL SETUP #
72
  ###############################################################################
73
 
74
+ # Outcome Prediction Model (Fine-Tuned BERT)
75
+ OUTCOME_MODEL_NAME = "mgbam/bert-base-finetuned-mgbam"
76
  try:
77
+ outcome_model = AutoModelForSequenceClassification.from_pretrained(
78
+ OUTCOME_MODEL_NAME, use_auth_token=HUGGINGFACE_TOKEN
79
  ).to(device)
80
+ outcome_tokenizer = AutoTokenizer.from_pretrained(
81
+ OUTCOME_MODEL_NAME, use_auth_token=HUGGINGFACE_TOKEN
82
  )
83
  except Exception as e:
84
+ logger.error(f"Outcome Model load error: {e}")
85
  raise
86
 
87
+ # Translation Model (English ↔ French)
88
+ TRANSLATION_MODEL_NAME = "Helsinki-NLP/opus-mt-en-fr"
89
  try:
 
90
  translation_model = MarianMTModel.from_pretrained(
91
+ TRANSLATION_MODEL_NAME, use_auth_token=HUGGINGFACE_TOKEN
92
  ).to(device)
93
  translation_tokenizer = MarianTokenizer.from_pretrained(
94
+ TRANSLATION_MODEL_NAME, use_auth_token=HUGGINGFACE_TOKEN
95
  )
96
  except Exception as e:
97
  logger.error(f"Translation model load error: {e}")
98
  raise
99
 
100
+ # Language Mapping for Translation
101
  LANGUAGE_MAP: Dict[str, Tuple[str, str]] = {
102
  "English to French": ("en", "fr"),
103
  "French to English": ("fr", "en"),
 
158
  ###############################################################################
159
 
160
  async def fetch_articles_by_nct_id(nct_id: str) -> Dict[str, Any]:
161
+ """Fetch articles from Europe PMC using NCT ID."""
162
  params = {"query": nct_id, "format": "json"}
163
  async with httpx.AsyncClient() as client_http:
164
  try:
 
170
  return {"error": str(e)}
171
 
172
  async def fetch_articles_by_query(query_params: str) -> Dict[str, Any]:
173
+ """Fetch articles from Europe PMC based on query parameters."""
174
  parsed_params = safe_json_parse(query_params)
175
  if not parsed_params or not isinstance(parsed_params, dict):
176
  return {"error": "Invalid JSON."}
 
182
  resp.raise_for_status()
183
  return resp.json()
184
  except Exception as e:
185
+ logger.error(f"Error fetching Europe PMC articles: {e}")
186
  return {"error": str(e)}
187
 
188
  async def fetch_pubmed_by_query(query_params: str) -> Dict[str, Any]:
189
+ """Fetch articles from PubMed based on query parameters."""
190
  parsed_params = safe_json_parse(query_params)
191
  if not parsed_params or not isinstance(parsed_params, dict):
192
  return {"error": "Invalid JSON for PubMed."}
 
208
  if not id_list:
209
  return {"result": ""}
210
 
211
+ # Fetch PubMed Articles
212
  fetch_params = {
213
  "db": "pubmed",
214
  "id": ",".join(id_list),
 
223
  return {"error": str(e)}
224
 
225
  async def fetch_crossref_by_query(query_params: str) -> Dict[str, Any]:
226
+ """Fetch articles from Crossref based on query parameters."""
227
  parsed_params = safe_json_parse(query_params)
228
  if not parsed_params or not isinstance(parsed_params, dict):
229
  return {"error": "Invalid JSON for Crossref."}
 
238
 
239
  async def fetch_bioportal_by_query(query_params: str) -> Dict[str, Any]:
240
  """
241
+ Fetch ontology data from BioPortal based on query parameters.
242
  Expects JSON like: {"q": "cancer"}
 
243
  """
244
  if not BIOPORTAL_API_KEY:
245
  return {"error": "No BioPortal API Key set."}
 
269
  ###############################################################################
270
 
271
  def summarize_text(text: str) -> str:
272
+ """Summarize clinical text using OpenAI GPT-3.5."""
273
  if not text.strip():
274
  return "No text provided for summarization."
275
  try:
276
+ response = openai.ChatCompletion.create(
277
  model="gpt-3.5-turbo",
278
  messages=[{"role": "user", "content": f"Summarize this clinical data:\n{text}"}],
279
+ max_tokens=500,
280
  temperature=0.7,
281
  )
282
  return response.choices[0].message.content.strip()
 
285
  return "Summarization failed."
286
 
287
  def predict_outcome(text: str) -> Union[Dict[str, float], str]:
288
+ """Predict outcomes using a fine-tuned Hugging Face BERT model."""
289
  if not text.strip():
290
  return "No text provided for prediction."
291
  try:
292
+ inputs = outcome_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
293
  inputs = {k: v.to(device) for k, v in inputs.items()}
294
  with torch.no_grad():
295
+ outputs = outcome_model(**inputs)
296
  probabilities = torch.nn.functional.softmax(outputs.logits, dim=-1)[0]
297
+ labels = outcome_model.config.id2label
298
+ return {labels[i]: float(prob.item()) for i, prob in enumerate(probabilities)}
299
  except Exception as e:
300
  logger.error(f"Prediction error: {e}")
301
  return "Prediction failed."
302
 
303
+ def translate_text(text: str, translation_option: str) -> str:
304
+ """Translate text between English and French using MarianMT."""
305
+ if not text.strip():
306
+ return "No text provided for translation."
307
+ try:
308
+ if translation_option not in LANGUAGE_MAP:
309
+ return "Unsupported translation option."
310
+ inputs = translation_tokenizer(text, return_tensors="pt", padding=True).to(device)
311
+ translated_tokens = translation_model.generate(**inputs)
312
+ translated_text = translation_tokenizer.decode(translated_tokens[0], skip_special_tokens=True)
313
+ return translated_text
314
+ except Exception as e:
315
+ logger.error(f"Translation error: {e}")
316
+ return "Translation failed."
317
+
318
+ def perform_named_entity_recognition(text: str) -> str:
319
+ """Perform Named Entity Recognition using spaCy."""
320
+ if not text.strip():
321
+ return "No text provided for NER."
322
+ try:
323
+ doc = nlp(text)
324
+ entities = [(ent.text, ent.label_) for ent in doc.ents]
325
+ if not entities:
326
+ return "No named entities found."
327
+ return "\n".join(f"{t} -> {lbl}" for t, lbl in entities)
328
+ except Exception as e:
329
+ logger.error(f"NER error: {e}")
330
+ return "NER failed."
331
+
332
  def generate_report(text: str, filename: str = "clinical_report.pdf") -> Optional[str]:
333
+ """Generate a professional PDF report from the text using ReportLab."""
334
  try:
335
  if not text.strip():
336
  logger.warning("No text provided for the report.")
337
  c = canvas.Canvas(filename)
338
+ c.setFont("Helvetica-Bold", 16)
339
+ c.drawString(100, 800, "Clinical Research Report")
340
+ c.setFont("Helvetica", 12)
341
  lines = text.split("\n")
342
+ y = 780
343
  for line in lines:
344
  if y < 50:
345
  c.showPage()
346
+ c.setFont("Helvetica", 12)
347
+ y = 800
348
  c.drawString(100, y, line)
349
  y -= 15
350
  c.save()
 
355
  return None
356
 
357
  def visualize_predictions(predictions: Dict[str, float]) -> alt.Chart:
358
+ """Visualize prediction probabilities using Altair."""
359
  data = pd.DataFrame(list(predictions.items()), columns=["Label", "Probability"])
360
  chart = (
361
  alt.Chart(data)
 
369
  )
370
  return chart
371
 
372
+ def fetch_web_search(query: str) -> str:
373
+ """Use OpenAI to perform a web search and provide explanations."""
374
+ if not query.strip():
375
+ return "No query provided for web search."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  try:
377
+ response = openai.ChatCompletion.create(
378
+ model="gpt-3.5-turbo",
379
+ messages=[
380
+ {"role": "system", "content": "You are a helpful assistant that provides detailed explanations based on the latest research."},
381
+ {"role": "user", "content": f"Explain the following query using the latest research: {query}"},
382
+ ],
383
+ max_tokens=700,
384
+ temperature=0.7,
385
+ )
386
+ return response.choices[0].message.content.strip()
387
  except Exception as e:
388
+ logger.error(f"Web search error: {e}")
389
+ return "Web search failed."
390
 
391
  ###############################################################################
392
  # 7) FILE PARSING (TXT, PDF, CSV, XLS) #
393
  ###############################################################################
394
 
395
  def parse_pdf_file_as_str(file_up: gr.File) -> str:
396
+ """Extract text from a PDF file using PyPDF2."""
397
+ try:
398
+ pdf_bytes = file_up.read()
 
 
 
 
 
 
 
399
  reader = PyPDF2.PdfReader(io.BytesIO(pdf_bytes))
400
  return "\n".join(page.extract_text() or "" for page in reader.pages)
401
+ except Exception as e:
402
+ logger.error(f"PDF parse error: {e}")
403
+ return "Failed to extract text from PDF."
404
 
405
  def parse_text_file_as_str(file_up: gr.File) -> str:
406
+ """Extract text from a TXT file."""
407
+ try:
408
+ return file_up.read().decode("utf-8", errors="replace")
409
+ except Exception as e:
410
+ logger.error(f"TXT parse error: {e}")
411
+ return "Failed to extract text from TXT file."
 
 
 
412
 
413
  def parse_csv_file_to_df(file_up: gr.File) -> pd.DataFrame:
414
+ """Parse CSV file into a pandas DataFrame with multiple encoding attempts."""
415
+ try:
416
+ return pd.read_csv(io.StringIO(file_up.read().decode("utf-8", errors="replace")))
417
+ except UnicodeDecodeError:
418
+ try:
419
+ return pd.read_csv(io.StringIO(file_up.read().decode("latin1", errors="replace")))
420
+ except Exception as e:
421
+ logger.error(f"CSV parse error: {e}")
422
+ return pd.DataFrame()
423
+ except Exception as e:
424
+ logger.error(f"CSV parse error: {e}")
425
+ return pd.DataFrame()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
  def parse_excel_file_to_df(file_up: gr.File) -> pd.DataFrame:
428
+ """Parse Excel file into a pandas DataFrame."""
429
+ try:
430
+ return pd.read_excel(io.BytesIO(file_up.read()), engine="openpyxl")
431
+ except Exception as e:
432
+ logger.error(f"Excel parse error: {e}")
433
+ return pd.DataFrame()
 
 
 
434
 
435
  ###############################################################################
436
  # 8) BUILDING THE GRADIO APP #
437
  ###############################################################################
438
 
439
+ def format_articles(articles: List[Dict[str, Any]]) -> str:
440
+ """Format fetched articles into a readable string."""
441
+ formatted = ""
442
+ for article in articles:
443
+ title = article.get("title", "No Title")
444
+ journal = article.get("journalTitle", "No Journal")
445
+ pub_year = article.get("pubYear", "No Year")
446
+ formatted += f"Title: {title}\nJournal: {journal} ({pub_year})\n\n"
447
+ return formatted.strip()
448
+
449
+ def format_bioportal_results(collection: List[Dict[str, Any]]) -> str:
450
+ """Format BioPortal results into a readable string."""
451
+ formatted = ""
452
+ for col in collection:
453
+ label = col.get("prefLabel", "No Label")
454
+ ontology = col.get("ontology", {}).get("name", "No Ontology")
455
+ formatted += f"Label: {label}\nOntology: {ontology}\n\n"
456
+ return formatted.strip()
457
+
458
+ async def handle_action(
459
+ action: str,
460
+ txt: Optional[str],
461
+ file_up: Optional[gr.File],
462
+ translation_opt: Optional[str],
463
+ query_str: Optional[str],
464
+ nct_id: Optional[str],
465
+ report_fn: Optional[str],
466
+ exp_fmt: Optional[str]
467
+ ) -> Tuple[Optional[str], Optional[Any], Optional[Any], Optional[str]]:
468
+ """
469
+ Master function to handle user actions.
470
+ Returns a 4-tuple mapped to (output_text, output_chart, output_chart2, output_file).
471
+ """
472
+ try:
473
+ combined_text = txt.strip() if txt else ""
474
+
475
+ # 1) If user uploaded a file, parse text from it
476
+ if file_up is not None:
477
+ ext = os.path.splitext(file_up.name)[1].lower()
478
+ if ext == ".txt":
479
+ parsed_text = parse_text_file_as_str(file_up)
480
+ combined_text += "\n" + parsed_text
481
+ elif ext == ".pdf":
482
+ parsed_text = parse_pdf_file_as_str(file_up)
483
+ combined_text += "\n" + parsed_text
484
+ elif ext == ".csv":
485
+ df_csv = parse_csv_file_to_df(file_up)
486
+ combined_text += "\n" + df_csv.to_csv(index=False)
487
+ elif ext in [".xls", ".xlsx"]:
488
+ df_xl = parse_excel_file_to_df(file_up)
489
+ combined_text += "\n" + df_xl.to_csv(index=False)
490
+ else:
491
+ return "Unsupported file format.", None, None, None
492
+
493
+ # 2) Branch by action
494
+ if action == "Summarize":
495
+ summary = summarize_text(combined_text)
496
+ return summary, None, None, None
497
+
498
+ elif action == "Predict Outcome":
499
+ preds = predict_outcome(combined_text)
500
+ if isinstance(preds, dict):
501
+ chart = visualize_predictions(preds)
502
+ return json.dumps(preds, indent=2), chart, None, None
503
+ return preds, None, None, None
504
+
505
+ elif action == "Generate Report":
506
+ path = generate_report(combined_text, report_fn or "clinical_report.pdf")
507
+ msg = f"Report generated: {path}" if path else "Report generation failed."
508
+ return msg, None, None, path
509
+
510
+ elif action == "Translate":
511
+ translated = translate_text(combined_text, translation_opt or "English to French")
512
+ return translated, None, None, None
513
+
514
+ elif action == "Perform Named Entity Recognition":
515
+ ner_result = perform_named_entity_recognition(combined_text)
516
+ return ner_result, None, None, None
517
+
518
+ elif action == "Fetch Clinical Studies":
519
+ if nct_id:
520
+ result = await fetch_articles_by_nct_id(nct_id)
521
+ elif query_str:
522
+ result = await fetch_articles_by_query(query_str)
523
+ else:
524
+ return "Provide either an NCT ID or valid query parameters.", None, None, None
525
+
526
+ articles = result.get("resultList", {}).get("result", [])
527
+ if not articles:
528
+ return "No articles found.", None, None, None
529
+
530
+ formatted = format_articles(articles)
531
+ return formatted, None, None, None
532
+
533
+ elif action in ["Fetch PubMed Articles (Legacy)", "Fetch PubMed by Query"]:
534
+ pubmed_result = await fetch_pubmed_by_query(query_str or "")
535
+ xml_data = pubmed_result.get("result")
536
+ if xml_data:
537
+ articles = parse_pubmed_xml(xml_data)
538
+ if not articles:
539
+ return "No articles found.", None, None, None
540
+ formatted = "\n\n".join(
541
+ f"{a['Title']} - {a['Journal']} ({a['PublicationDate']})"
542
+ for a in articles if a['Title']
543
+ )
544
+ return formatted if formatted else "No articles found.", None, None, None
545
+ return "No articles found or error in fetching PubMed data.", None, None, None
546
+
547
+ elif action == "Fetch Crossref by Query":
548
+ crossref_result = await fetch_crossref_by_query(query_str or "")
549
+ items = crossref_result.get("message", {}).get("items", [])
550
+ if not items:
551
+ return "No results found.", None, None, None
552
+ crossref_formatted = "\n\n".join(
553
+ f"Title: {it.get('title', ['No title'])[0]}, DOI: {it.get('DOI')}"
554
+ for it in items
555
+ )
556
+ return crossref_formatted, None, None, None
557
+
558
+ elif action == "Fetch BioPortal by Query":
559
+ bp_result = await fetch_bioportal_by_query(query_str or "")
560
+ collection = bp_result.get("collection", [])
561
+ if not collection:
562
+ return "No BioPortal results found.", None, None, None
563
+ formatted = format_bioportal_results(collection)
564
+ return formatted, None, None, None
565
+
566
+ elif action == "Web Search Explanation":
567
+ explanation = fetch_web_search(combined_text)
568
+ return explanation, None, None, None
569
+
570
+ else:
571
+ return "Invalid action selected.", None, None, None
572
+
573
+ except Exception as ex:
574
+ # Catch all exceptions, log, and return traceback to 'output_text'
575
+ tb_str = traceback.format_exc()
576
+ logger.error(f"Exception in handle_action:\n{tb_str}")
577
+ return f"Traceback:\n{tb_str}", None, None, None
578
 
579
+ ###############################################################################
580
+ # 9) BUILDING THE GRADIO APP #
581
+ ###############################################################################
582
 
583
+ with gr.Blocks(css="""
584
+ .gradio-container {
585
+ background-color: #f5f5f5;
586
+ }
587
+ .gr-button-primary {
588
+ background-color: #4CAF50;
589
+ }
590
+ .gradio-tabs {
591
+ background-color: #ffffff;
592
+ }
593
+ """) as demo:
594
+ gr.Markdown("# 🏥 **AI-Driven Clinical Assistant**")
595
+ gr.Markdown("""
596
+ **Highlights**:
597
+ - **Summarize** clinical text (OpenAI GPT-3.5)
598
+ - **Predict** outcomes (Hugging Face fine-tuned model)
599
+ - **Translate** (English ↔ French)
600
+ - **Named Entity Recognition** (spaCy)
601
+ - **Fetch** from PubMed, Crossref, Europe PMC, and **BioPortal**
602
+ - **Generate** professional PDF reports
603
+ - **Web Search Explanations** (OpenAI)
604
+
605
+ *Disclaimer*: This is a research demo, **not** a medical device.
606
+ """)
607
+
608
  with gr.Row():
609
+ text_input = gr.Textbox(
610
+ label="Input Clinical Text",
611
+ lines=5,
612
+ placeholder="Enter clinical text, research notes, or queries...",
613
+ interactive=True
614
+ )
615
  file_input = gr.File(
616
+ label="Upload File",
617
+ file_types=[".txt", ".csv", ".xls", ".xlsx", ".pdf"],
618
+ interactive=True
619
  )
620
 
621
  action = gr.Radio(
 
630
  "Fetch PubMed by Query",
631
  "Fetch Crossref by Query",
632
  "Fetch BioPortal by Query",
633
+ "Web Search Explanation"
634
  ],
635
  label="Select an Action",
636
+ interactive=True
637
  )
638
+
639
  translation_option = gr.Dropdown(
640
  choices=list(LANGUAGE_MAP.keys()),
641
  label="Translation Option",
642
+ value="English to French",
643
+ interactive=True
644
  )
645
+
646
  query_params_input = gr.Textbox(
647
+ label="Query Parameters (JSON)",
648
+ placeholder='{"term": "cancer"} or {"q": "cancer"} for BioPortal',
649
+ interactive=True
650
+ )
651
+
652
+ nct_id_input = gr.Textbox(
653
+ label="NCT ID",
654
+ placeholder="Enter NCT ID (e.g., NCT00000000)",
655
+ interactive=True
656
+ )
657
+
658
+ report_filename_input = gr.Textbox(
659
+ label="Report Filename",
660
+ value="clinical_report.pdf",
661
+ interactive=True
662
+ )
663
+
664
+ exp_fmt = gr.Dropdown(
665
+ choices=["None", "CSV", "JSON"],
666
+ label="Export Format",
667
+ value="None",
668
+ interactive=True
669
  )
 
 
 
670
 
671
  # Outputs
672
+ output_text = gr.Textbox(
673
+ label="Output",
674
+ lines=20,
675
+ interactive=False
676
+ )
677
+
678
  with gr.Row():
679
+ output_chart = gr.Plot(label="Prediction Probabilities")
680
+ output_chart2 = gr.Plot(label="Additional Visualization") # Placeholder for future use
 
681
 
682
+ output_file = gr.File(label="Generated File", interactive=False)
683
+
684
+ submit_btn = gr.Button("Submit", variant="primary")
685
+
686
+ gr.Markdown("""
687
+ ---
688
+
689
+ ### **Important Disclaimers**
690
+
691
+ - **Not a Medical Device**: This tool is not intended to provide clinical diagnoses or final medical decisions. Always consult qualified healthcare professionals for clinical decisions.
692
+ - **AI/ML Limitations**: GPT-based summaries and classification models offer powerful insights but may generate incomplete or inaccurate results. Always verify AI-generated content.
693
+ - **Credential Security**: Ensure the security of your API keys (`OPENAI_API_KEY`, `HF_TOKEN`, `BIOPORTAL_API_KEY`) to safely access external services.
694
+ - **Data Privacy**: If handling real patient data, ensure compliance with applicable data protection regulations (e.g., HIPAA, GDPR).
695
+
696
+ ---
697
+ """)
698
+
699
+ # Connect the submit button to the action handler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  submit_btn.click(
701
+ fn=lambda action, txt, file_up, trans_opt, query, nct_id, report_fn, exp_fm: asyncio.run(
702
+ handle_action(action, txt, file_up, trans_opt, query, nct_id, report_fn, exp_fm)
703
+ ),
704
+ inputs=[action, text_input, file_input, translation_option, query_params_input, nct_id_input, report_filename_input, exp_fmt],
705
  outputs=[output_text, output_chart, output_chart2, output_file],
706
  )
707
 
708
+ ###############################################################################
709
+ # 10) LAUNCHING THE GRADIO APP #
710
+ ###############################################################################
711
+
712
  # Launch the Gradio interface
713
  demo.launch(server_name="0.0.0.0", server_port=7860, share=True)