spriambada3 commited on
Commit
d18fef3
·
1 Parent(s): 712f300

refactored

Browse files
Files changed (6) hide show
  1. cdss.py +31 -728
  2. editor.py +202 -0
  3. requirements.txt +0 -1
  4. rules_visualization.svg +0 -12
  5. simulator.py +361 -0
  6. validator.py +167 -0
cdss.py CHANGED
@@ -18,596 +18,23 @@ python app.py
18
  """
19
 
20
  from __future__ import annotations
21
- import os
22
- import random
23
  import time
24
- from dataclasses import asdict
25
- from typing import Dict, Any, Tuple, List
26
- from datetime import datetime
27
 
28
  import gradio as gr
29
  import pandas as pd
30
- import plotly.express as px
31
 
32
  from models import Vitals, PatientState
33
- from rules import rule_based_cdss
34
-
35
- GEMINI_MODEL_NAME = "gemini-2.5-pro"
36
- # --- Gemini setup (simplified) ---
37
- try:
38
- import google.generativeai as genai
39
-
40
- genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
41
- GEMINI_MODEL = genai.GenerativeModel(GEMINI_MODEL_NAME)
42
- GEMINI_ERR = None
43
- except Exception as e:
44
- GEMINI_MODEL, GEMINI_ERR = None, f"Gemini import/config error: {e}"
45
-
46
-
47
- # --- Data structures & Scenarios (Full list included) ---
48
-
49
-
50
- def scenario_A0_Normal() -> PatientState:
51
- return PatientState(
52
- "A0 Normal Case",
53
- "Mother",
54
- "Pemeriksaan rutin.",
55
- {"Hb": 12.5},
56
- Vitals(110, 70, 80, 16, 36.7, 99),
57
- )
58
-
59
-
60
- def scenario_A1_PPH() -> PatientState:
61
- return PatientState(
62
- "A1 PPH",
63
- "Mother",
64
- "30 menit postpartum; kehilangan darah ~900 ml.",
65
- {"Hb": 9},
66
- Vitals(90, 60, 120, 24, 36.8, 96),
67
- )
68
-
69
-
70
- def scenario_A2_Preeclampsia() -> PatientState:
71
- return PatientState(
72
- "A2 Preeklampsia",
73
- "Mother",
74
- "36 minggu; sakit kepala, pandangan kabur.",
75
- {"Proteinuria": "3+"},
76
- Vitals(165, 105, 98, 20, 36.9, 98),
77
- )
78
-
79
-
80
- def scenario_A3_MaternalSepsis() -> PatientState:
81
- return PatientState(
82
- "A3 Sepsis Maternal",
83
- "Mother",
84
- "POD2 pasca SC; luka purulen.",
85
- {"Leukosit": 17000},
86
- Vitals(95, 60, 110, 24, 39.0, 96),
87
- )
88
-
89
-
90
- def scenario_B1_Prematurity() -> PatientState:
91
- return PatientState(
92
- "B1 Prematuritas/BBLR",
93
- "Neonate",
94
- "34 minggu; berat 1900 g; hipotermia ringan; SpO2 borderline",
95
- {"BB": 1900, "UsiaGestasi_mgg": 34},
96
- Vitals(60, 35, 150, 50, 35.0, 90),
97
- )
98
-
99
-
100
- def scenario_B2_Asphyxia() -> PatientState:
101
- return PatientState(
102
- "B2 Asfiksia Perinatal",
103
- "Neonate",
104
- "APGAR 3 menit 1; tidak menangis >1 menit",
105
- {"APGAR_1m": 3},
106
- Vitals(55, 30, 80, 10, 36.5, 82),
107
- )
108
-
109
-
110
- def scenario_B3_NeonatalSepsis() -> PatientState:
111
- return PatientState(
112
- "B3 Sepsis Neonatal",
113
- "Neonate",
114
- "Hari ke-4; lemas, malas minum",
115
- {"CRP": 25, "Leukosit": 19000},
116
- Vitals(60, 35, 170, 60, 38.5, 93),
117
- )
118
-
119
-
120
- def scenario_C1_GynSurgComp() -> PatientState:
121
- return PatientState(
122
- "C1 Komplikasi Bedah Ginekologis",
123
- "Gyn",
124
- "Pasca histerektomi; nyeri perut bawah; urine output turun",
125
- {"UrineOutput_ml_hr": 10},
126
- Vitals(100, 65, 105, 20, 37.8, 98),
127
- )
128
-
129
-
130
- def scenario_C2_PostOpInfection() -> PatientState:
131
- return PatientState(
132
- "C2 Infeksi Pasca-Bedah",
133
- "Gyn",
134
- "Pasca kistektomi; luka bengkak & kemerahan; demam",
135
- {"Luka": "bengkak+kemerahan"},
136
- Vitals(105, 70, 108, 22, 38.0, 98),
137
- )
138
-
139
-
140
- def scenario_C3_DelayedGynCancer() -> PatientState:
141
- return PatientState(
142
- "C3 Keterlambatan Diagnostik Kanker Ginekologi",
143
- "Gyn",
144
- "45 th; perdarahan pascamenopause; Pap abnormal 6 bulan lalu tanpa tindak lanjut",
145
- {"PapSmear": "abnormal 6 bln lalu"},
146
- Vitals(120, 78, 86, 18, 36.8, 99),
147
- )
148
-
149
-
150
- SCENARIOS = {
151
- "A0": scenario_A0_Normal,
152
- "A1": scenario_A1_PPH,
153
- "A2": scenario_A2_Preeclampsia,
154
- "A3": scenario_A3_MaternalSepsis,
155
- "B1": scenario_B1_Prematurity,
156
- "B2": scenario_B2_Asphyxia,
157
- "B3": scenario_B3_NeonatalSepsis,
158
- "C1": scenario_C1_GynSurgComp,
159
- "C2": scenario_C2_PostOpInfection,
160
- "C3": scenario_C3_DelayedGynCancer,
161
- }
162
-
163
-
164
- # --- Simulation & CDSS Logic (simplified) ---
165
- def drift_vitals(state: PatientState) -> PatientState:
166
- v = state.vitals
167
- clamp = lambda val, lo, hi: max(lo, min(hi, val))
168
- drift_factor = 0 if state.scenario.startswith("A0") else 1
169
- v.hr = clamp(v.hr + random.randint(-2, 2) * drift_factor, 40, 200)
170
- v.sbp = clamp(v.sbp + random.randint(-2, 2) * drift_factor, 50, 220)
171
- v.rr = clamp(v.rr + random.randint(-1, 1) * drift_factor, 8, 80)
172
- state.vitals = v
173
- return state
174
-
175
-
176
- # --- Rule-based fallback (no AI or AI disabled) ---
177
-
178
-
179
- def gemini_cdss(state: PatientState) -> str:
180
- if not GEMINI_MODEL:
181
- return f"[CDSS AI ERROR] {GEMINI_ERR}"
182
- try:
183
- v = state.vitals
184
- prompt = f"CDSS for {state.scenario}. Vitals: SBP {v.sbp}/{v.dbp}, HR {v.hr}. Analyze risks, give concise steps in Indonesian."
185
- return GEMINI_MODEL.generate_content(prompt).text or "[CDSS AI] No response."
186
- except Exception as e:
187
- return f"[CDSS AI error] {e}"
188
-
189
-
190
- # --- Plotting & Data Helpers ---
191
- def create_vital_plot(
192
- df: pd.DataFrame, y_cols: List[str] | str, title: str, y_lim: List[int]
193
- ):
194
- """Creates a customized Plotly figure for a specific vital sign."""
195
- # Create an empty plot if there is no data to prevent errors
196
- if df.empty:
197
- fig = px.line(title=title)
198
- else:
199
- fig = px.line(df, x="timestamp", y=y_cols, title=title, markers=True)
200
- # Customize x-axis to show only first and last tick
201
- if len(df) > 1:
202
- fig.update_xaxes(
203
- tickvals=[df["timestamp"].iloc[0], df["timestamp"].iloc[-1]]
204
- )
205
-
206
- # Apply standard layout settings
207
- fig.update_layout(
208
- height=250,
209
- yaxis_range=y_lim,
210
- margin=dict(t=40, b=10, l=10, r=10), # Tighten margins
211
- )
212
- return fig
213
-
214
-
215
- def _row_from_state(ps: PatientState) -> Dict[str, Any]:
216
- return {"timestamp": datetime.now(), "scenario": ps.scenario, **asdict(ps.vitals)}
217
-
218
-
219
- def prepare_df_for_display(df: pd.DataFrame) -> pd.DataFrame:
220
- if df is None or df.empty:
221
- return pd.DataFrame(
222
- columns=[
223
- "timestamp",
224
- "scenario",
225
- "sbp",
226
- "dbp",
227
- "hr",
228
- "rr",
229
- "temp_c",
230
- "spo2",
231
- ]
232
- )
233
- df_display = df.copy()
234
- df_display["timestamp"] = pd.to_datetime(df_display["timestamp"])
235
- df_display = df_display.sort_values("timestamp")
236
- df_display["timestamp"] = df_display["timestamp"].dt.strftime("%Y-%m-%d %H:%M:%S")
237
- return df_display
238
-
239
-
240
- def generate_all_plots(df: pd.DataFrame):
241
- """Helper to generate all 5 plot figures from a dataframe."""
242
- df_display = prepare_df_for_display(df)
243
- bp_fig = create_vital_plot(
244
- df_display,
245
- y_cols=["sbp", "dbp"],
246
- title="Blood Pressure (mmHg)",
247
- y_lim=[40, 200],
248
- )
249
- hr_fig = create_vital_plot(
250
- df_display, y_cols="hr", title="Heart Rate (bpm)", y_lim=[40, 200]
251
- )
252
- rr_fig = create_vital_plot(
253
- df_display, y_cols="rr", title="Respiratory Rate (/min)", y_lim=[0, 70]
254
- )
255
- temp_fig = create_vital_plot(
256
- df_display, y_cols="temp_c", title="Temperature (°C)", y_lim=[34, 42]
257
- )
258
- spo2_fig = create_vital_plot(
259
- df_display, y_cols="spo2", title="Oxygen Saturation (%)", y_lim=[70, 101]
260
- )
261
- return df_display, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig
262
-
263
-
264
- # --- Gradio App Logic ---
265
- def process_and_update(
266
- ps: PatientState, history_df: pd.DataFrame, historic_text: str, cdss_on: bool
267
- ):
268
- """Centralized function to process state, update history, and generate all UI component outputs."""
269
- interpretation = gemini_cdss(ps) if cdss_on else rule_based_cdss(ps)
270
- new_row = _row_from_state(ps)
271
- history_df = pd.concat([history_df, pd.DataFrame([new_row])], ignore_index=True)
272
-
273
- df_for_table, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig = generate_all_plots(
274
- history_df
275
- )
276
-
277
- return (
278
- asdict(ps),
279
- *state_to_panels(ps),
280
- str(ps.labs), # For labs_text
281
- str(ps.labs), # For labs_show
282
- interpretation,
283
- history_df,
284
- df_for_table,
285
- historic_text.strip(),
286
- time.time(),
287
- bp_fig,
288
- hr_fig,
289
- rr_fig,
290
- temp_fig,
291
- spo2_fig,
292
- )
293
-
294
-
295
- def state_to_panels(state: PatientState) -> Tuple:
296
- v = state.vitals
297
- return (
298
- state.scenario,
299
- state.patient_type,
300
- state.notes,
301
- v.sbp,
302
- v.dbp,
303
- v.hr,
304
- v.rr,
305
- v.temp_c,
306
- v.spo2,
307
- )
308
-
309
-
310
- def inject_scenario(
311
- tag: str, cdss_on: bool, history_df: pd.DataFrame, historic_text: str
312
- ):
313
- ps = SCENARIOS[tag]()
314
- if historic_text: # Add a newline if text already exists
315
- historic_text += f"\n[{datetime.now().strftime('%H:%M:%S')}] Scenario Injected: {ps.scenario}"
316
- else:
317
- historic_text = (
318
- f"[{datetime.now().strftime('%H:%M:%S')}] Scenario Injected: {ps.scenario}"
319
- )
320
- return process_and_update(ps, history_df, historic_text, cdss_on)
321
-
322
-
323
- def manual_edit(
324
- sbp,
325
- dbp,
326
- hr,
327
- rr,
328
- temp_c,
329
- spo2,
330
- notes,
331
- labs_text,
332
- cdss_on,
333
- patient_type,
334
- current_state,
335
- history_df,
336
- historic_text,
337
- ):
338
- try:
339
- labs = eval(labs_text)
340
- except:
341
- labs = {"raw": labs_text}
342
- ps = PatientState(
343
- current_state.get("scenario", "Manual"),
344
- patient_type,
345
- notes,
346
- labs,
347
- Vitals(int(sbp), int(dbp), int(hr), int(rr), float(temp_c), int(spo2)),
348
- )
349
- if ps.notes and ps.notes.strip():
350
- historic_text += f"\n[{datetime.now().strftime('%H:%M:%S')}] {ps.notes}"
351
- return process_and_update(ps, history_df, historic_text, cdss_on)
352
-
353
-
354
- def tick_timer(cdss_on, current_state, history_df, historic_text):
355
- if not current_state:
356
- return [gr.update()] * 22
357
- ps = PatientState(**current_state)
358
- ps.vitals = Vitals(**ps.vitals)
359
- ps = drift_vitals(ps)
360
- return process_and_update(ps, history_df, historic_text, cdss_on)
361
-
362
-
363
- def load_csv(file, history_df: pd.DataFrame):
364
- try:
365
- if file is not None:
366
- df_new = pd.read_csv(file.name)
367
- df_new["timestamp"] = pd.to_datetime(df_new["timestamp"])
368
- history_df = (
369
- pd.concat([history_df, df_new], ignore_index=True)
370
- if not history_df.empty
371
- else df_new
372
- )
373
- except Exception as e:
374
- print(f"Error loading CSV: {e}")
375
- df_for_table, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig = generate_all_plots(
376
- history_df
377
- )
378
- return history_df, df_for_table, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig
379
-
380
-
381
- def countdown_tick(last_tick_ts: float):
382
- if not last_tick_ts:
383
- return "Next update in —"
384
- return f"Next update in {max(0, 30 - int(time.time() - last_tick_ts))}s"
385
-
386
-
387
- import json
388
- import ast
389
-
390
-
391
- def parse_rules():
392
- with open("rules.py", "r") as f:
393
- tree = ast.parse(f.read())
394
-
395
- rules = {"Mother": [], "Neonate": [], "Gyn": []}
396
-
397
- for node in ast.walk(tree):
398
- if isinstance(node, ast.FunctionDef) and node.name == "rule_based_cdss":
399
- for body_item in node.body:
400
- if (
401
- isinstance(body_item, ast.If)
402
- and isinstance(body_item.test, ast.Compare)
403
- and isinstance(body_item.test.left, ast.Attribute)
404
- and isinstance(body_item.test.left.value, ast.Name)
405
- and body_item.test.left.value.id == "state"
406
- and body_item.test.left.attr == "patient_type"
407
- and isinstance(body_item.test.ops[0], ast.Eq)
408
- and isinstance(body_item.test.comparators[0], ast.Constant)
409
- ):
410
-
411
- patient_type = body_item.test.comparators[0].value
412
- if patient_type in rules:
413
- for rule_node in body_item.body:
414
- if isinstance(rule_node, ast.If):
415
- conditions = ast.unparse(rule_node.test)
416
- alert = ""
417
- for item in rule_node.body:
418
- if (
419
- isinstance(item, ast.Expr)
420
- and isinstance(item.value, ast.Call)
421
- and hasattr(item.value.func, "value")
422
- and hasattr(item.value.func.value, "id")
423
- and item.value.func.value.id == "alerts"
424
- and item.value.func.attr == "append"
425
- and isinstance(item.value.args[0], ast.Constant)
426
- ):
427
- alert = item.value.args[0].value
428
- rules[patient_type].append(
429
- {"conditions": conditions, "alert": alert}
430
- )
431
- return rules
432
-
433
-
434
- def rules_to_dataframes(rules):
435
- dataframes = {}
436
- for patient_type, rules_list in rules.items():
437
- data = {"Conditions": [], "Alert": []}
438
- for rule in rules_list:
439
- data["Conditions"].append(rule["conditions"])
440
- data["Alert"].append(rule["alert"])
441
- df = pd.DataFrame(data)
442
- dataframes[patient_type] = df
443
- return dataframes
444
-
445
-
446
- def dataframes_to_rules(dfs):
447
- rules = {"Mother": [], "Neonate": [], "Gyn": []}
448
- for patient_type, df in dfs.items():
449
- if df is not None:
450
- for index, row in df.iterrows():
451
- if row["Conditions"] and row["Alert"]:
452
- rules[patient_type].append(
453
- {"conditions": row["Conditions"], "alert": row["Alert"]}
454
- )
455
- return rules
456
-
457
-
458
- def add_row(df):
459
- if df is None:
460
- df = pd.DataFrame(columns=["Conditions", "Alert"])
461
- df.loc[len(df)] = ["", ""]
462
- return df
463
-
464
-
465
- def save_rules(df_mother, df_neonate, df_gyn):
466
- dfs = {"Mother": df_mother, "Neonate": df_neonate, "Gyn": df_gyn}
467
- for patient_type, df in dfs.items():
468
- if not isinstance(df, pd.DataFrame):
469
- dfs[patient_type] = pd.DataFrame(df, columns=["Conditions", "Alert"])
470
-
471
- rules = dataframes_to_rules(dfs)
472
-
473
- with open("rules.py", "r") as f:
474
- tree = ast.parse(f.read())
475
-
476
- for node in ast.walk(tree):
477
- if isinstance(node, ast.FunctionDef) and node.name == "rule_based_cdss":
478
- node.body = []
479
- node.body.append(ast.parse("v = state.vitals").body[0])
480
- node.body.append(ast.parse("labs = state.labs").body[0])
481
- node.body.append(ast.parse("alerts = []").body[0])
482
-
483
- for patient_type, rule_list in rules.items():
484
- if_patient_type_body = []
485
- for rule in rule_list:
486
- conditions = (
487
- rule["conditions"].replace("\r", " ").replace("\n", " ")
488
- )
489
- if_rule_str = f"if {conditions}:\n alerts.append({json.dumps(rule['alert'])})"
490
- if_rule = ast.parse(if_rule_str).body[0]
491
- if_patient_type_body.append(if_rule)
492
-
493
- if if_patient_type_body:
494
- if_patient_type = ast.If(
495
- test=ast.Compare(
496
- left=ast.Attribute(
497
- value=ast.Name(id="state", ctx=ast.Load()),
498
- attr="patient_type",
499
- ctx=ast.Load(),
500
- ),
501
- ops=[ast.Eq()],
502
- comparators=[ast.Constant(value=patient_type)],
503
- ),
504
- body=if_patient_type_body,
505
- orelse=[],
506
- )
507
- node.body.append(if_patient_type)
508
-
509
- node.body.append(
510
- ast.parse(
511
- 'if not alerts:\n return "Tidak ada alert prioritas tinggi. Lanjutkan pemantauan dan dokumentasi."'
512
- ).body[0]
513
- )
514
- node.body.append(
515
- ast.parse(
516
- 'return "\\n- ".join(["ALERT:"] + alerts)', mode="single"
517
- ).body[0]
518
- )
519
-
520
- new_code = ast.unparse(tree)
521
- with open("rules.py", "w") as f:
522
- f.write(new_code)
523
-
524
- return "Rules saved successfully."
525
-
526
-
527
- def test_condition(
528
- patient_type, sbp, dbp, hr, rr, temp_c, spo2, labs_text, condition, alert_text
529
- ):
530
- """
531
- Tests a single condition against a manually defined patient state.
532
- """
533
- try:
534
- labs = json.loads(labs_text)
535
- except json.JSONDecodeError:
536
- return "Error: Invalid JSON in Labs field."
537
-
538
- vitals = Vitals(
539
- sbp=int(sbp),
540
- dbp=int(dbp),
541
- hr=int(hr),
542
- rr=int(rr),
543
- temp_c=float(temp_c),
544
- spo2=int(spo2),
545
- )
546
- state = PatientState(
547
- scenario="Validation",
548
- patient_type=patient_type,
549
- notes="",
550
- labs=labs,
551
- vitals=vitals,
552
- )
553
-
554
- # Dynamically create a rule function for testing
555
- rule_fnc_str = f"""
556
- def dynamic_rule(state):
557
- v = state.vitals
558
- labs = state.labs
559
- alerts = []
560
- if {condition}:
561
- alerts.append("{alert_text}")
562
- if not alerts:
563
- return "No alert triggered."
564
- return "- ".join(["ALERT:"] + alerts)
565
- """
566
- try:
567
- exec(rule_fnc_str, globals())
568
- result = dynamic_rule(state)
569
- return result
570
- except Exception as e:
571
- return f"Error in condition syntax: {e}"
572
-
573
-
574
- def add_rule_to_set(patient_type, condition, alert_text):
575
- """
576
- Adds the new rule to the rules.py file.
577
- """
578
- if not condition or not alert_text:
579
- return "Error: Condition and Alert text cannot be empty."
580
-
581
- try:
582
- with open("rules.py", "r") as f:
583
- tree = ast.parse(f.read())
584
-
585
- for node in ast.walk(tree):
586
- if isinstance(node, ast.FunctionDef) and node.name == "rule_based_cdss":
587
- for body_item in node.body:
588
- if (
589
- isinstance(body_item, ast.If)
590
- and hasattr(body_item.test, "comparators")
591
- and body_item.test.comparators
592
- and isinstance(body_item.test.comparators[0], ast.Constant)
593
- and body_item.test.comparators[0].value == patient_type
594
- ):
595
-
596
- new_rule_str = (
597
- f'if {condition}:\n alerts.append("{alert_text}")'
598
- )
599
- new_rule_node = ast.parse(new_rule_str).body[0]
600
- body_item.body.append(new_rule_node)
601
- break
602
-
603
- new_code = ast.unparse(tree)
604
- with open("rules.py", "w") as f:
605
- f.write(new_code)
606
-
607
- return f"Rule added to {patient_type} ruleset and saved to rules.py."
608
-
609
- except Exception as e:
610
- return f"Failed to add rule: {e}"
611
 
612
 
613
  # --- Build UI ---
@@ -622,148 +49,24 @@ with gr.Blocks(
622
  interpretation = gr.Textbox(label="CDSS Interpretation", lines=2, interactive=False)
623
 
624
  with gr.Tabs():
625
- with gr.TabItem("CDSS Simulator"):
626
- with gr.Accordion("History, Trends, and Data Loading", open=True):
627
- with gr.Row():
628
- with gr.Tabs():
629
- with gr.Tab("Blood Pressure"):
630
- bp_plot = gr.Plot()
631
- with gr.Tab("Heart Rate"):
632
- hr_plot = gr.Plot()
633
- with gr.Tab("Respiration"):
634
- rr_plot = gr.Plot()
635
- with gr.Tab("Temperature"):
636
- temp_plot = gr.Plot()
637
- with gr.Tab("SpO₂"):
638
- spo2_plot = gr.Plot()
639
-
640
- with gr.TabItem("Rule Editor"):
641
- with gr.Tabs():
642
- with gr.TabItem("Edit Rules"):
643
- gr.Markdown("## CDSS Rule Editor")
644
-
645
- initial_rules = parse_rules()
646
- initial_dfs = rules_to_dataframes(initial_rules)
647
-
648
- with gr.Tabs():
649
- with gr.Tab("Mother"):
650
- df_mother = gr.DataFrame(
651
- value=initial_dfs["Mother"],
652
- headers=["Conditions", "Alert"],
653
- interactive=True,
654
- row_count=(len(initial_dfs["Mother"]) + 1, "dynamic"),
655
- type="pandas",
656
- )
657
- add_mother_btn = gr.Button("➕ Add Mother Rule")
658
- add_mother_btn.click(
659
- add_row, inputs=df_mother, outputs=df_mother
660
- )
661
-
662
- with gr.Tab("Neonate"):
663
- df_neonate = gr.DataFrame(
664
- value=initial_dfs["Neonate"],
665
- headers=["Conditions", "Alert"],
666
- interactive=True,
667
- row_count=(len(initial_dfs["Neonate"]) + 1, "dynamic"),
668
- type="pandas",
669
- )
670
- add_neonate_btn = gr.Button("➕ Add Neonate Rule")
671
- add_neonate_btn.click(
672
- add_row, inputs=df_neonate, outputs=df_neonate
673
- )
674
-
675
- with gr.Tab("Gyn"):
676
- df_gyn = gr.DataFrame(
677
- value=initial_dfs["Gyn"],
678
- headers=["Conditions", "Alert"],
679
- interactive=True,
680
- row_count=(len(initial_dfs["Gyn"]) + 1, "dynamic"),
681
- type="pandas",
682
- )
683
- add_gyn_btn = gr.Button("➕ Add Gyn Rule")
684
- add_gyn_btn.click(add_row, inputs=df_gyn, outputs=df_gyn)
685
-
686
- save_button = gr.Button("💾 Save Rules")
687
- status_textbox = gr.Textbox(label="Status", interactive=False)
688
-
689
- save_button.click(
690
- save_rules,
691
- inputs=[df_mother, df_neonate, df_gyn],
692
- outputs=status_textbox,
693
- )
694
- with gr.TabItem("Rule Validator"):
695
- gr.Markdown("## Validate and Add New Rules")
696
- with gr.Row():
697
- with gr.Column():
698
- gr.Markdown("### 1. Define Patient State")
699
- patient_type_validate = gr.Radio(
700
- ["Mother", "Neonate", "Gyn"],
701
- label="Patient Type",
702
- value="Mother",
703
- )
704
- sbp_validate = gr.Number(label="SBP", value=120)
705
- dbp_validate = gr.Number(label="DBP", value=80)
706
- hr_validate = gr.Number(label="HR", value=80)
707
- rr_validate = gr.Number(label="RR", value=18)
708
- temp_c_validate = gr.Number(label="Temp (°C)", value=37.0)
709
- spo2_validate = gr.Number(label="SpO₂ (%)", value=98)
710
- labs_validate = gr.Textbox(
711
- label="Labs (JSON format)",
712
- value='{"Hb": 12.0}',
713
- lines=3,
714
- )
715
-
716
- with gr.Column():
717
- gr.Markdown("### 2. Define and Test Rule")
718
- condition_validate = gr.Textbox(
719
- label="Condition (Python expression)",
720
- value="v.sbp > 140",
721
- lines=3,
722
- )
723
- alert_validate = gr.Textbox(
724
- label="Alert Message",
725
- value="Preeclampsia suspected",
726
- lines=3,
727
- )
728
- test_button = gr.Button("Test Rule", variant="secondary")
729
- validation_result = gr.Textbox(
730
- label="Validation Result", interactive=False
731
- )
732
-
733
- gr.Markdown("### 3. Add Rule to Ruleset")
734
- add_rule_button = gr.Button(
735
- "Add Rule to Ruleset", variant="primary"
736
- )
737
- add_rule_status = gr.Textbox(
738
- label="Status", interactive=False
739
- )
740
-
741
- test_button.click(
742
- test_condition,
743
- inputs=[
744
- patient_type_validate,
745
- sbp_validate,
746
- dbp_validate,
747
- hr_validate,
748
- rr_validate,
749
- temp_c_validate,
750
- spo2_validate,
751
- labs_validate,
752
- condition_validate,
753
- alert_validate,
754
- ],
755
- outputs=validation_result,
756
- )
757
-
758
- add_rule_button.click(
759
- add_rule_to_set,
760
- inputs=[
761
- patient_type_validate,
762
- condition_validate,
763
- alert_validate,
764
- ],
765
- outputs=add_rule_status,
766
- )
767
 
768
  with gr.Row():
769
  with gr.Column(scale=2):
@@ -878,7 +181,7 @@ with gr.Blocks(
878
  gr.Timer(30.0).tick(tick_timer, timer_inputs, ui_outputs)
879
  gr.Timer(1.0).tick(countdown_tick, [last_tick_ts], [countdown_lbl])
880
 
881
- demo.load(inject_scenario, [gr.State("A0"), cdss_toggle], ui_outputs)
882
 
883
  if __name__ == "__main__":
884
- demo.launch()
 
18
  """
19
 
20
  from __future__ import annotations
 
 
21
  import time
 
 
 
22
 
23
  import gradio as gr
24
  import pandas as pd
 
25
 
26
  from models import Vitals, PatientState
27
+ from simulator import (
28
+ simulator_ui,
29
+ inject_scenario,
30
+ manual_edit,
31
+ tick_timer,
32
+ load_csv,
33
+ countdown_tick,
34
+ SCENARIOS,
35
+ )
36
+ from editor import editor_ui, save_rules
37
+ from validator import validator_ui, test_condition, add_rule_to_set
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
 
40
  # --- Build UI ---
 
49
  interpretation = gr.Textbox(label="CDSS Interpretation", lines=2, interactive=False)
50
 
51
  with gr.Tabs():
52
+ bp_plot, hr_plot, rr_plot, temp_plot, spo2_plot = simulator_ui()
53
+ (df_mother, df_neonate, df_gyn, save_button, status_textbox) = editor_ui()
54
+ (
55
+ patient_type_validate,
56
+ sbp_validate,
57
+ dbp_validate,
58
+ hr_validate,
59
+ rr_validate,
60
+ temp_c_validate,
61
+ spo2_validate,
62
+ labs_validate,
63
+ condition_validate,
64
+ alert_validate,
65
+ test_button,
66
+ validation_result,
67
+ add_rule_button,
68
+ add_rule_status,
69
+ ) = validator_ui()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  with gr.Row():
72
  with gr.Column(scale=2):
 
181
  gr.Timer(30.0).tick(tick_timer, timer_inputs, ui_outputs)
182
  gr.Timer(1.0).tick(countdown_tick, [last_tick_ts], [countdown_lbl])
183
 
184
+ demo.load(inject_scenario, [gr.State("A0"), cdss_toggle, history_df, historic_text], ui_outputs)
185
 
186
  if __name__ == "__main__":
187
+ demo.launch()
editor.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CDSS Rule Editor Component
3
+ """
4
+
5
+ import gradio as gr
6
+ import pandas as pd
7
+ import json
8
+ import ast
9
+
10
+
11
+ def parse_rules():
12
+ with open("rules.py", "r") as f:
13
+ tree = ast.parse(f.read())
14
+
15
+ rules = {"Mother": [], "Neonate": [], "Gyn": []}
16
+
17
+ for node in ast.walk(tree):
18
+ if isinstance(node, ast.FunctionDef) and node.name == "rule_based_cdss":
19
+ for body_item in node.body:
20
+ if (
21
+ isinstance(body_item, ast.If)
22
+ and isinstance(body_item.test, ast.Compare)
23
+ and isinstance(body_item.test.left, ast.Attribute)
24
+ and isinstance(body_item.test.left.value, ast.Name)
25
+ and body_item.test.left.value.id == "state"
26
+ and body_item.test.left.attr == "patient_type"
27
+ and isinstance(body_item.test.ops[0], ast.Eq)
28
+ and isinstance(body_item.test.comparators[0], ast.Constant)
29
+ ):
30
+
31
+ patient_type = body_item.test.comparators[0].value
32
+ if patient_type in rules:
33
+ for rule_node in body_item.body:
34
+ if isinstance(rule_node, ast.If):
35
+ conditions = ast.unparse(rule_node.test)
36
+ alert = ""
37
+ for item in rule_node.body:
38
+ if (
39
+ isinstance(item, ast.Expr)
40
+ and isinstance(item.value, ast.Call)
41
+ and hasattr(item.value.func, "value")
42
+ and hasattr(item.value.func.value, "id")
43
+ and item.value.func.value.id == "alerts"
44
+ and item.value.func.attr == "append"
45
+ and isinstance(item.value.args[0], ast.Constant)
46
+ ):
47
+ alert = item.value.args[0].value
48
+ rules[patient_type].append(
49
+ {"conditions": conditions, "alert": alert}
50
+ )
51
+ return rules
52
+
53
+
54
+ def rules_to_dataframes(rules):
55
+ dataframes = {}
56
+ for patient_type, rules_list in rules.items():
57
+ data = {"Conditions": [], "Alert": []}
58
+ for rule in rules_list:
59
+ data["Conditions"].append(rule["conditions"])
60
+ data["Alert"].append(rule["alert"])
61
+ df = pd.DataFrame(data)
62
+ dataframes[patient_type] = df
63
+ return dataframes
64
+
65
+
66
+ def dataframes_to_rules(dfs):
67
+ rules = {"Mother": [], "Neonate": [], "Gyn": []}
68
+ for patient_type, df in dfs.items():
69
+ if df is not None:
70
+ for index, row in df.iterrows():
71
+ if row["Conditions"] and row["Alert"]:
72
+ rules[patient_type].append(
73
+ {"conditions": row["Conditions"], "alert": row["Alert"]}
74
+ )
75
+ return rules
76
+
77
+
78
+ def add_row(df):
79
+ if df is None:
80
+ df = pd.DataFrame(columns=["Conditions", "Alert"])
81
+ df.loc[len(df)] = ["", ""]
82
+ return df
83
+
84
+
85
+ def save_rules(df_mother, df_neonate, df_gyn):
86
+ dfs = {"Mother": df_mother, "Neonate": df_neonate, "Gyn": df_gyn}
87
+ for patient_type, df in dfs.items():
88
+ if not isinstance(df, pd.DataFrame):
89
+ dfs[patient_type] = pd.DataFrame(df, columns=["Conditions", "Alert"])
90
+
91
+ rules = dataframes_to_rules(dfs)
92
+
93
+ with open("rules.py", "r") as f:
94
+ tree = ast.parse(f.read())
95
+
96
+ for node in ast.walk(tree):
97
+ if isinstance(node, ast.FunctionDef) and node.name == "rule_based_cdss":
98
+ node.body = []
99
+ node.body.append(ast.parse("v = state.vitals").body[0])
100
+ node.body.append(ast.parse("labs = state.labs").body[0])
101
+ node.body.append(ast.parse("alerts = []").body[0])
102
+
103
+ for patient_type, rule_list in rules.items():
104
+ if_patient_type_body = []
105
+ for rule in rule_list:
106
+ conditions = (
107
+ rule["conditions"].replace("\r", " ").replace("\n", " ")
108
+ )
109
+ if_rule_str = f"if {conditions}:\n alerts.append({json.dumps(rule['alert'])})"
110
+ if_rule = ast.parse(if_rule_str).body[0]
111
+ if_patient_type_body.append(if_rule)
112
+
113
+ if if_patient_type_body:
114
+ if_patient_type = ast.If(
115
+ test=ast.Compare(
116
+ left=ast.Attribute(
117
+ value=ast.Name(id="state", ctx=ast.Load()),
118
+ attr="patient_type",
119
+ ctx=ast.Load(),
120
+ ),
121
+ ops=[ast.Eq()],
122
+ comparators=[ast.Constant(value=patient_type)],
123
+ ),
124
+ body=if_patient_type_body,
125
+ orelse=[],
126
+ )
127
+ node.body.append(if_patient_type)
128
+
129
+ node.body.append(
130
+ ast.parse(
131
+ 'if not alerts:\\n return "Tidak ada alert prioritas tinggi. Lanjutkan pemantauan dan dokumentasi."'
132
+ ).body[0]
133
+ )
134
+ node.body.append(
135
+ ast.parse(
136
+ 'return "\\n- ".join(["ALERT:"] + alerts)', mode="single"
137
+ ).body[0]
138
+ )
139
+
140
+ new_code = ast.unparse(tree)
141
+ with open("rules.py", "w") as f:
142
+ f.write(new_code)
143
+
144
+ return "Rules saved successfully."
145
+
146
+
147
+ def editor_ui():
148
+ with gr.TabItem("Rule Editor"):
149
+ with gr.Tabs():
150
+ with gr.TabItem("Edit Rules"):
151
+ gr.Markdown("## CDSS Rule Editor")
152
+
153
+ initial_rules = parse_rules()
154
+ initial_dfs = rules_to_dataframes(initial_rules)
155
+
156
+ with gr.Tabs():
157
+ with gr.Tab("Mother"):
158
+ df_mother = gr.DataFrame(
159
+ value=initial_dfs["Mother"],
160
+ headers=["Conditions", "Alert"],
161
+ interactive=True,
162
+ row_count=(len(initial_dfs["Mother"]) + 1, "dynamic"),
163
+ type="pandas",
164
+ )
165
+ add_mother_btn = gr.Button("➕ Add Mother Rule")
166
+ add_mother_btn.click(
167
+ add_row, inputs=df_mother, outputs=df_mother
168
+ )
169
+
170
+ with gr.Tab("Neonate"):
171
+ df_neonate = gr.DataFrame(
172
+ value=initial_dfs["Neonate"],
173
+ headers=["Conditions", "Alert"],
174
+ interactive=True,
175
+ row_count=(len(initial_dfs["Neonate"]) + 1, "dynamic"),
176
+ type="pandas",
177
+ )
178
+ add_neonate_btn = gr.Button("➕ Add Neonate Rule")
179
+ add_neonate_btn.click(
180
+ add_row, inputs=df_neonate, outputs=df_neonate
181
+ )
182
+
183
+ with gr.Tab("Gyn"):
184
+ df_gyn = gr.DataFrame(
185
+ value=initial_dfs["Gyn"],
186
+ headers=["Conditions", "Alert"],
187
+ interactive=True,
188
+ row_count=(len(initial_dfs["Gyn"]) + 1, "dynamic"),
189
+ type="pandas",
190
+ )
191
+ add_gyn_btn = gr.Button("➕ Add Gyn Rule")
192
+ add_gyn_btn.click(add_row, inputs=df_gyn, outputs=df_gyn)
193
+
194
+ save_button = gr.Button("💾 Save Rules")
195
+ status_textbox = gr.Textbox(label="Status", interactive=False)
196
+
197
+ save_button.click(
198
+ save_rules,
199
+ inputs=[df_mother, df_neonate, df_gyn],
200
+ outputs=status_textbox,
201
+ )
202
+ return df_mother, df_neonate, df_gyn, save_button, status_textbox
requirements.txt CHANGED
@@ -1,7 +1,6 @@
1
  fastapi
2
  uvicorn
3
  gradio
4
- graphviz
5
  google-generativeai
6
  pandas
7
  plotly
 
1
  fastapi
2
  uvicorn
3
  gradio
 
4
  google-generativeai
5
  pandas
6
  plotly
rules_visualization.svg DELETED
simulator.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CDSS Simulator Component
3
+ """
4
+ import random
5
+ import time
6
+ from dataclasses import asdict
7
+ from typing import Dict, Any, Tuple, List
8
+ from datetime import datetime
9
+
10
+ import gradio as gr
11
+ import pandas as pd
12
+ import plotly.express as px
13
+
14
+ from models import Vitals, PatientState
15
+ from rules import rule_based_cdss
16
+
17
+ # --- Gemini setup (simplified) ---
18
+ try:
19
+ import google.generativeai as genai
20
+ import os
21
+ genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
22
+ GEMINI_MODEL = genai.GenerativeModel("gemini-1.5-pro")
23
+ GEMINI_ERR = None
24
+ except Exception as e:
25
+ GEMINI_MODEL, GEMINI_ERR = None, f"Gemini import/config error: {e}"
26
+
27
+
28
+ # --- Data structures & Scenarios (Full list included) ---
29
+
30
+ def scenario_A0_Normal() -> PatientState:
31
+ return PatientState(
32
+ "A0 Normal Case",
33
+ "Mother",
34
+ "Pemeriksaan rutin.",
35
+ {"Hb": 12.5},
36
+ Vitals(110, 70, 80, 16, 36.7, 99),
37
+ )
38
+
39
+ def scenario_A1_PPH() -> PatientState:
40
+ return PatientState(
41
+ "A1 PPH",
42
+ "Mother",
43
+ "30 menit postpartum; kehilangan darah ~900 ml.",
44
+ {"Hb": 9},
45
+ Vitals(90, 60, 120, 24, 36.8, 96),
46
+ )
47
+
48
+ def scenario_A2_Preeclampsia() -> PatientState:
49
+ return PatientState(
50
+ "A2 Preeklampsia",
51
+ "Mother",
52
+ "36 minggu; sakit kepala, pandangan kabur.",
53
+ {"Proteinuria": "3+"},
54
+ Vitals(165, 105, 98, 20, 36.9, 98),
55
+ )
56
+
57
+ def scenario_A3_MaternalSepsis() -> PatientState:
58
+ return PatientState(
59
+ "A3 Sepsis Maternal",
60
+ "Mother",
61
+ "POD2 pasca SC; luka purulen.",
62
+ {"Leukosit": 17000},
63
+ Vitals(95, 60, 110, 24, 39.0, 96),
64
+ )
65
+
66
+ def scenario_B1_Prematurity() -> PatientState:
67
+ return PatientState(
68
+ "B1 Prematuritas/BBLR",
69
+ "Neonate",
70
+ "34 minggu; berat 1900 g; hipotermia ringan; SpO2 borderline",
71
+ {"BB": 1900, "UsiaGestasi_mgg": 34},
72
+ Vitals(60, 35, 150, 50, 35.0, 90),
73
+ )
74
+
75
+ def scenario_B2_Asphyxia() -> PatientState:
76
+ return PatientState(
77
+ "B2 Asfiksia Perinatal",
78
+ "Neonate",
79
+ "APGAR 3 menit 1; tidak menangis >1 menit",
80
+ {"APGAR_1m": 3},
81
+ Vitals(55, 30, 80, 10, 36.5, 82),
82
+ )
83
+
84
+ def scenario_B3_NeonatalSepsis() -> PatientState:
85
+ return PatientState(
86
+ "B3 Sepsis Neonatal",
87
+ "Neonate",
88
+ "Hari ke-4; lemas, malas minum",
89
+ {"CRP": 25, "Leukosit": 19000},
90
+ Vitals(60, 35, 170, 60, 38.5, 93),
91
+ )
92
+
93
+ def scenario_C1_GynSurgComp() -> PatientState:
94
+ return PatientState(
95
+ "C1 Komplikasi Bedah Ginekologis",
96
+ "Gyn",
97
+ "Pasca histerektomi; nyeri perut bawah; urine output turun",
98
+ {"UrineOutput_ml_hr": 10},
99
+ Vitals(100, 65, 105, 20, 37.8, 98),
100
+ )
101
+
102
+ def scenario_C2_PostOpInfection() -> PatientState:
103
+ return PatientState(
104
+ "C2 Infeksi Pasca-Bedah",
105
+ "Gyn",
106
+ "Pasca kistektomi; luka bengkak & kemerahan; demam",
107
+ {"Luka": "bengkak+kemerahan"},
108
+ Vitals(105, 70, 108, 22, 38.0, 98),
109
+ )
110
+
111
+ def scenario_C3_DelayedGynCancer() -> PatientState:
112
+ return PatientState(
113
+ "C3 Keterlambatan Diagnostik Kanker Ginekologi",
114
+ "Gyn",
115
+ "45 th; perdarahan pascamenopause; Pap abnormal 6 bulan lalu tanpa tindak lanjut",
116
+ {"PapSmear": "abnormal 6 bln lalu"},
117
+ Vitals(120, 78, 86, 18, 36.8, 99),
118
+ )
119
+
120
+ SCENARIOS = {
121
+ "A0": scenario_A0_Normal,
122
+ "A1": scenario_A1_PPH,
123
+ "A2": scenario_A2_Preeclampsia,
124
+ "A3": scenario_A3_MaternalSepsis,
125
+ "B1": scenario_B1_Prematurity,
126
+ "B2": scenario_B2_Asphyxia,
127
+ "B3": scenario_B3_NeonatalSepsis,
128
+ "C1": scenario_C1_GynSurgComp,
129
+ "C2": scenario_C2_PostOpInfection,
130
+ "C3": scenario_C3_DelayedGynCancer,
131
+ }
132
+
133
+
134
+ # --- Simulation & CDSS Logic (simplified) ---
135
+ def drift_vitals(state: PatientState) -> PatientState:
136
+ v = state.vitals
137
+ clamp = lambda val, lo, hi: max(lo, min(hi, val))
138
+ drift_factor = 0 if state.scenario.startswith("A0") else 1
139
+ v.hr = clamp(v.hr + random.randint(-2, 2) * drift_factor, 40, 200)
140
+ v.sbp = clamp(v.sbp + random.randint(-2, 2) * drift_factor, 50, 220)
141
+ v.rr = clamp(v.rr + random.randint(-1, 1) * drift_factor, 8, 80)
142
+ state.vitals = v
143
+ return state
144
+
145
+
146
+ # --- Rule-based fallback (no AI or AI disabled) ---
147
+
148
+ def gemini_cdss(state: PatientState) -> str:
149
+ if not GEMINI_MODEL:
150
+ return f"[CDSS AI ERROR] {GEMINI_ERR}"
151
+ try:
152
+ v = state.vitals
153
+ prompt = f"CDSS for {state.scenario}. Vitals: SBP {v.sbp}/{v.dbp}, HR {v.hr}. Analyze risks, give concise steps in Indonesian."
154
+ return GEMINI_MODEL.generate_content(prompt).text or "[CDSS AI] No response."
155
+ except Exception as e:
156
+ return f"[CDSS AI error] {e}"
157
+
158
+
159
+ # --- Plotting & Data Helpers ---
160
+ def create_vital_plot(
161
+ df: pd.DataFrame, y_cols: List[str] | str, title: str, y_lim: List[int]
162
+ ):
163
+ """Creates a customized Plotly figure for a specific vital sign."""
164
+ # Create an empty plot if there is no data to prevent errors
165
+ if df.empty:
166
+ fig = px.line(title=title)
167
+ else:
168
+ fig = px.line(df, x="timestamp", y=y_cols, title=title, markers=True)
169
+ # Customize x-axis to show only first and last tick
170
+ if len(df) > 1:
171
+ fig.update_xaxes(
172
+ tickvals=[df["timestamp"].iloc[0], df["timestamp"].iloc[-1]]
173
+ )
174
+
175
+ # Apply standard layout settings
176
+ fig.update_layout(
177
+ height=250,
178
+ yaxis_range=y_lim,
179
+ margin=dict(t=40, b=10, l=10, r=10), # Tighten margins
180
+ )
181
+ return fig
182
+
183
+ def _row_from_state(ps: PatientState) -> Dict[str, Any]:
184
+ return {"timestamp": datetime.now(), "scenario": ps.scenario, **asdict(ps.vitals)}
185
+
186
+ def prepare_df_for_display(df: pd.DataFrame) -> pd.DataFrame:
187
+ if df is None or df.empty:
188
+ return pd.DataFrame(
189
+ columns=[
190
+ "timestamp",
191
+ "scenario",
192
+ "sbp",
193
+ "dbp",
194
+ "hr",
195
+ "rr",
196
+ "temp_c",
197
+ "spo2",
198
+ ]
199
+ )
200
+ df_display = df.copy()
201
+ df_display["timestamp"] = pd.to_datetime(df_display["timestamp"])
202
+ df_display = df_display.sort_values("timestamp")
203
+ df_display["timestamp"] = df_display["timestamp"].dt.strftime("%Y-%m-%d %H:%M:%S")
204
+ return df_display
205
+
206
+ def generate_all_plots(df: pd.DataFrame):
207
+ """Helper to generate all 5 plot figures from a dataframe."""
208
+ df_display = prepare_df_for_display(df)
209
+ bp_fig = create_vital_plot(
210
+ df_display,
211
+ y_cols=["sbp", "dbp"],
212
+ title="Blood Pressure (mmHg)",
213
+ y_lim=[40, 200],
214
+ )
215
+ hr_fig = create_vital_plot(
216
+ df_display, y_cols="hr", title="Heart Rate (bpm)", y_lim=[40, 200]
217
+ )
218
+ rr_fig = create_vital_plot(
219
+ df_display, y_cols="rr", title="Respiratory Rate (/min)", y_lim=[0, 70]
220
+ )
221
+ temp_fig = create_vital_plot(
222
+ df_display, y_cols="temp_c", title="Temperature (°C)", y_lim=[34, 42]
223
+ )
224
+ spo2_fig = create_vital_plot(
225
+ df_display, y_cols="spo2", title="Oxygen Saturation (%)", y_lim=[70, 101]
226
+ )
227
+ return df_display, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig
228
+
229
+
230
+ # --- Gradio App Logic ---
231
+ def process_and_update(
232
+ ps: PatientState, history_df: pd.DataFrame, historic_text: str, cdss_on: bool
233
+ ):
234
+ """Centralized function to process state, update history, and generate all UI component outputs."""
235
+ interpretation = gemini_cdss(ps) if cdss_on else rule_based_cdss(ps)
236
+ new_row = _row_from_state(ps)
237
+ history_df = pd.concat([history_df, pd.DataFrame([new_row])], ignore_index=True)
238
+
239
+ df_for_table, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig = generate_all_plots(
240
+ history_df
241
+ )
242
+
243
+ return (
244
+ asdict(ps),
245
+ *state_to_panels(ps),
246
+ str(ps.labs), # For labs_text
247
+ str(ps.labs), # For labs_show
248
+ interpretation,
249
+ history_df,
250
+ df_for_table,
251
+ historic_text.strip(),
252
+ time.time(),
253
+ bp_fig,
254
+ hr_fig,
255
+ rr_fig,
256
+ temp_fig,
257
+ spo2_fig,
258
+ )
259
+
260
+ def state_to_panels(state: PatientState) -> Tuple:
261
+ v = state.vitals
262
+ return (
263
+ state.scenario,
264
+ state.patient_type,
265
+ state.notes,
266
+ v.sbp,
267
+ v.dbp,
268
+ v.hr,
269
+ v.rr,
270
+ v.temp_c,
271
+ v.spo2,
272
+ )
273
+
274
+ def inject_scenario(
275
+ tag: str, cdss_on: bool, history_df: pd.DataFrame, historic_text: str
276
+ ):
277
+ ps = SCENARIOS[tag]()
278
+ if historic_text: # Add a newline if text already exists
279
+ historic_text += f"\n[{datetime.now().strftime('%H:%M:%S')}] Scenario Injected: {ps.scenario}"
280
+ else:
281
+ historic_text = (
282
+ f"[{datetime.now().strftime('%H:%M:%S')}] Scenario Injected: {ps.scenario}"
283
+ )
284
+ return process_and_update(ps, history_df, historic_text, cdss_on)
285
+
286
+ def manual_edit(
287
+ sbp,
288
+ dbp,
289
+ hr,
290
+ rr,
291
+ temp_c,
292
+ spo2,
293
+ notes,
294
+ labs_text,
295
+ cdss_on,
296
+ patient_type,
297
+ current_state,
298
+ history_df,
299
+ historic_text,
300
+ ):
301
+ try:
302
+ labs = eval(labs_text)
303
+ except:
304
+ labs = {"raw": labs_text}
305
+ ps = PatientState(
306
+ current_state.get("scenario", "Manual"),
307
+ patient_type,
308
+ notes,
309
+ labs,
310
+ Vitals(int(sbp), int(dbp), int(hr), int(rr), float(temp_c), int(spo2)),
311
+ )
312
+ if ps.notes and ps.notes.strip():
313
+ historic_text += f"\n[{datetime.now().strftime('%H:%M:%S')}] {ps.notes}"
314
+ return process_and_update(ps, history_df, historic_text, cdss_on)
315
+
316
+ def tick_timer(cdss_on, current_state, history_df, historic_text):
317
+ if not current_state:
318
+ return [gr.update()] * 22
319
+ ps = PatientState(**current_state)
320
+ ps.vitals = Vitals(**ps.vitals)
321
+ ps = drift_vitals(ps)
322
+ return process_and_update(ps, history_df, historic_text, cdss_on)
323
+
324
+ def load_csv(file, history_df: pd.DataFrame):
325
+ try:
326
+ if file is not None:
327
+ df_new = pd.read_csv(file.name)
328
+ df_new["timestamp"] = pd.to_datetime(df_new["timestamp"])
329
+ history_df = (
330
+ pd.concat([history_df, df_new], ignore_index=True)
331
+ if not history_df.empty
332
+ else df_new
333
+ )
334
+ except Exception as e:
335
+ print(f"Error loading CSV: {e}")
336
+ df_for_table, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig = generate_all_plots(
337
+ history_df
338
+ )
339
+ return history_df, df_for_table, bp_fig, hr_fig, rr_fig, temp_fig, spo2_fig
340
+
341
+ def countdown_tick(last_tick_ts: float):
342
+ if not last_tick_ts:
343
+ return "Next update in —"
344
+ return f"Next update in {max(0, 30 - int(time.time() - last_tick_ts))}s"
345
+
346
+ def simulator_ui():
347
+ with gr.TabItem("CDSS Simulator"):
348
+ with gr.Accordion("History, Trends, and Data Loading", open=True):
349
+ with gr.Row():
350
+ with gr.Tabs():
351
+ with gr.Tab("Blood Pressure"):
352
+ bp_plot = gr.Plot()
353
+ with gr.Tab("Heart Rate"):
354
+ hr_plot = gr.Plot()
355
+ with gr.Tab("Respiration"):
356
+ rr_plot = gr.Plot()
357
+ with gr.Tab("Temperature"):
358
+ temp_plot = gr.Plot()
359
+ with gr.Tab("SpO₂"):
360
+ spo2_plot = gr.Plot()
361
+ return bp_plot, hr_plot, rr_plot, temp_plot, spo2_plot
validator.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CDSS Rule Validator Component
3
+ """
4
+ import gradio as gr
5
+ import json
6
+ import ast
7
+ from models import Vitals, PatientState
8
+
9
+ def test_condition(
10
+ patient_type, sbp, dbp, hr, rr, temp_c, spo2, labs_text, condition, alert_text
11
+ ):
12
+ """
13
+ Tests a single condition against a manually defined patient state.
14
+ """
15
+ try:
16
+ labs = json.loads(labs_text)
17
+ except json.JSONDecodeError:
18
+ return "Error: Invalid JSON in Labs field."
19
+
20
+ vitals = Vitals(
21
+ sbp=int(sbp),
22
+ dbp=int(dbp),
23
+ hr=int(hr),
24
+ rr=int(rr),
25
+ temp_c=float(temp_c),
26
+ spo2=int(spo2),
27
+ )
28
+ state = PatientState(
29
+ scenario="Validation",
30
+ patient_type=patient_type,
31
+ notes="",
32
+ labs=labs,
33
+ vitals=vitals,
34
+ )
35
+
36
+ # Dynamically create a rule function for testing
37
+ rule_fnc_str = f"""
38
+ def dynamic_rule(state):
39
+ v = state.vitals
40
+ labs = state.labs
41
+ alerts = []
42
+ if {condition}:
43
+ alerts.append("{alert_text}")
44
+ if not alerts:
45
+ return "No alert triggered."
46
+ return "- ".join(["ALERT:"] + alerts)
47
+ """
48
+ try:
49
+ exec(rule_fnc_str, globals())
50
+ result = dynamic_rule(state)
51
+ return result
52
+ except Exception as e:
53
+ return f"Error in condition syntax: {e}"
54
+
55
+ def add_rule_to_set(patient_type, condition, alert_text):
56
+ """
57
+ Adds the new rule to the rules.py file.
58
+ """
59
+ if not condition or not alert_text:
60
+ return "Error: Condition and Alert text cannot be empty."
61
+
62
+ try:
63
+ with open("rules.py", "r") as f:
64
+ tree = ast.parse(f.read())
65
+
66
+ for node in ast.walk(tree):
67
+ if isinstance(node, ast.FunctionDef) and node.name == "rule_based_cdss":
68
+ for body_item in node.body:
69
+ if (
70
+ isinstance(body_item, ast.If)
71
+ and hasattr(body_item.test, "comparators")
72
+ and body_item.test.comparators
73
+ and isinstance(body_item.test.comparators[0], ast.Constant)
74
+ and body_item.test.comparators[0].value == patient_type
75
+ ):
76
+
77
+ new_rule_str = (
78
+ f'if {condition}:\n alerts.append("{alert_text}")'
79
+ )
80
+ new_rule_node = ast.parse(new_rule_str).body[0]
81
+ body_item.body.append(new_rule_node)
82
+ break
83
+
84
+ new_code = ast.unparse(tree)
85
+ with open("rules.py", "w") as f:
86
+ f.write(new_code)
87
+
88
+ return f"Rule added to {patient_type} ruleset and saved to rules.py."
89
+
90
+ except Exception as e:
91
+ return f"Failed to add rule: {e}"
92
+
93
+ def validator_ui():
94
+ with gr.TabItem("Rule Validator"):
95
+ gr.Markdown("## Validate and Add New Rules")
96
+ with gr.Row():
97
+ with gr.Column():
98
+ gr.Markdown("### 1. Define Patient State")
99
+ patient_type_validate = gr.Radio(
100
+ ["Mother", "Neonate", "Gyn"],
101
+ label="Patient Type",
102
+ value="Mother",
103
+ )
104
+ sbp_validate = gr.Number(label="SBP", value=120)
105
+ dbp_validate = gr.Number(label="DBP", value=80)
106
+ hr_validate = gr.Number(label="HR", value=80)
107
+ rr_validate = gr.Number(label="RR", value=18)
108
+ temp_c_validate = gr.Number(label="Temp (°C)", value=37.0)
109
+ spo2_validate = gr.Number(label="SpO₂ (%)", value=98)
110
+ labs_validate = gr.Textbox(
111
+ label="Labs (JSON format)",
112
+ value='{"Hb": 12.0}',
113
+ lines=3,
114
+ )
115
+
116
+ with gr.Column():
117
+ gr.Markdown("### 2. Define and Test Rule")
118
+ condition_validate = gr.Textbox(
119
+ label="Condition (Python expression)",
120
+ value="v.sbp > 140",
121
+ lines=3,
122
+ )
123
+ alert_validate = gr.Textbox(
124
+ label="Alert Message",
125
+ value="Preeclampsia suspected",
126
+ lines=3,
127
+ )
128
+ test_button = gr.Button("Test Rule", variant="secondary")
129
+ validation_result = gr.Textbox(
130
+ label="Validation Result", interactive=False
131
+ )
132
+
133
+ gr.Markdown("### 3. Add Rule to Ruleset")
134
+ add_rule_button = gr.Button(
135
+ "Add Rule to Ruleset", variant="primary"
136
+ )
137
+ add_rule_status = gr.Textbox(
138
+ label="Status", interactive=False
139
+ )
140
+
141
+ test_button.click(
142
+ test_condition,
143
+ inputs=[
144
+ patient_type_validate,
145
+ sbp_validate,
146
+ dbp_validate,
147
+ hr_validate,
148
+ rr_validate,
149
+ temp_c_validate,
150
+ spo2_validate,
151
+ labs_validate,
152
+ condition_validate,
153
+ alert_validate,
154
+ ],
155
+ outputs=validation_result,
156
+ )
157
+
158
+ add_rule_button.click(
159
+ add_rule_to_set,
160
+ inputs=[
161
+ patient_type_validate,
162
+ condition_validate,
163
+ alert_validate,
164
+ ],
165
+ outputs=add_rule_status,
166
+ )
167
+ return patient_type_validate, sbp_validate, dbp_validate, hr_validate, rr_validate, temp_c_validate, spo2_validate, labs_validate, condition_validate, alert_validate, test_button, validation_result, add_rule_button, add_rule_status