bisonnetworking commited on
Commit
3c97881
·
verified ·
1 Parent(s): 6ec5223

Update medication search and patient-friendly copy

Browse files
assets/apple.css CHANGED
@@ -358,6 +358,13 @@ body,
358
  opacity: 1 !important;
359
  }
360
 
 
 
 
 
 
 
 
361
  .button-row {
362
  gap: 12px;
363
  margin-top: 12px;
 
358
  opacity: 1 !important;
359
  }
360
 
361
+ .medication-picker .options[role="listbox"] {
362
+ max-height: min(360px, calc(100vh - 160px)) !important;
363
+ overflow-y: auto !important;
364
+ overscroll-behavior: contain;
365
+ scrollbar-width: thin;
366
+ }
367
+
368
  .button-row {
369
  gap: 12px;
370
  margin-top: 12px;
data/medications/rxterms_medications_common.json ADDED
The diff for this file is too large to render. See raw diff
 
shared_app.py CHANGED
@@ -14,8 +14,13 @@ from src.model_catalog import (
14
  )
15
  from src.prompts import build_prep_report_prompt
16
  from src.processor import validate_inputs, parse_prep_report
17
- from src.medications import load_medication_choices, medication_index_summary
 
 
 
 
18
  from config_loader import load_settings
 
19
 
20
  settings = load_settings()
21
 
@@ -216,14 +221,14 @@ def run_inference(
216
  return parse_prep_report(report_raw)
217
 
218
 
219
- def add_medication_entry(medication_name: str, instructions: str, current_medications: str):
220
  """Append a selected or custom medication line to the medication list."""
221
  medication_name = (medication_name or "").strip()
222
  instructions = (instructions or "").strip()
223
  current_medications = (current_medications or "").strip()
224
 
225
  if not medication_name and not instructions:
226
- return current_medications, "", "", None
227
 
228
  if medication_name and instructions:
229
  entry = f"{medication_name} - {instructions}"
@@ -231,7 +236,22 @@ def add_medication_entry(medication_name: str, instructions: str, current_medica
231
  entry = medication_name or instructions
232
 
233
  updated = f"{current_medications}\n{entry}".strip() if current_medications else entry
234
- return updated, "", "", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
 
237
  def populate_medication_name(selected_medication: str, current_name: str):
@@ -239,6 +259,11 @@ def populate_medication_name(selected_medication: str, current_name: str):
239
  return (selected_medication or current_name or "").strip()
240
 
241
 
 
 
 
 
 
242
  def create_ui() -> gr.Blocks:
243
  app_cfg = settings.get("app", {})
244
  model_cfg = settings.get("model", {})
@@ -257,7 +282,7 @@ def create_ui() -> gr.Blocks:
257
  temperature = _temperature_choice_value(
258
  selected_model_cfg.get("temperature", model_cfg.get("temperature", 0.3))
259
  )
260
- medication_choices = load_medication_choices()
261
  medication_summary = medication_index_summary()
262
  deployment = app_cfg.get("deployment", "local")
263
  is_local_deployment = deployment == "local"
@@ -312,8 +337,7 @@ def create_ui() -> gr.Blocks:
312
  gr.HTML(
313
  """
314
  <div class="pane-heading">
315
- <p class="section-kicker">Input</p>
316
- <h2>Appointment details</h2>
317
  </div>
318
  """
319
  )
@@ -340,7 +364,7 @@ def create_ui() -> gr.Blocks:
340
  medication_picker = gr.Dropdown(
341
  label="Find a Medication",
342
  show_label=False,
343
- choices=medication_choices,
344
  value=None,
345
  filterable=True,
346
  allow_custom_value=True,
@@ -350,7 +374,7 @@ def create_ui() -> gr.Blocks:
350
  medication_name = gr.Textbox(
351
  label="Medication Name",
352
  show_label=False,
353
- placeholder="Select from RxTerms above, or type a medication, vitamin, or supplement",
354
  lines=1,
355
  max_lines=2,
356
  elem_classes=["apple-input"],
@@ -394,8 +418,7 @@ def create_ui() -> gr.Blocks:
394
  gr.HTML(
395
  """
396
  <div class="pane-heading">
397
- <p class="section-kicker">Output</p>
398
- <h2>Prep report</h2>
399
  </div>
400
  """
401
  )
@@ -512,6 +535,17 @@ def create_ui() -> gr.Blocks:
512
  outputs=[model_status, download_model_btn],
513
  api_visibility="private",
514
  )
 
 
 
 
 
 
 
 
 
 
 
515
  medication_picker.change(
516
  fn=populate_medication_name,
517
  inputs=[medication_picker, medication_name],
@@ -520,13 +554,13 @@ def create_ui() -> gr.Blocks:
520
  )
521
  add_medication_btn.click(
522
  fn=add_medication_entry,
523
- inputs=[medication_name, medication_instructions, medications],
524
  outputs=[medications, medication_name, medication_instructions, medication_picker],
525
  api_visibility="private",
526
  )
527
  clear_btn.click(
528
- fn=lambda: ("", "", "", "", "", None, DEFAULT_OUTPUT, DEFAULT_OUTPUT, DEFAULT_OUTPUT),
529
- inputs=[],
530
  outputs=[
531
  symptoms,
532
  notes,
 
14
  )
15
  from src.prompts import build_prep_report_prompt
16
  from src.processor import validate_inputs, parse_prep_report
17
+ from src.medications import (
18
+ filter_medication_choices,
19
+ load_medication_choices,
20
+ medication_index_summary,
21
+ )
22
  from config_loader import load_settings
23
+ import gradio as gr
24
 
25
  settings = load_settings()
26
 
 
221
  return parse_prep_report(report_raw)
222
 
223
 
224
+ def add_medication_entry(medication_name: str, instructions: str, current_medications: str, all_choices: list = None):
225
  """Append a selected or custom medication line to the medication list."""
226
  medication_name = (medication_name or "").strip()
227
  instructions = (instructions or "").strip()
228
  current_medications = (current_medications or "").strip()
229
 
230
  if not medication_name and not instructions:
231
+ return current_medications, "", "", gr.update(choices=all_choices or [], value=None)
232
 
233
  if medication_name and instructions:
234
  entry = f"{medication_name} - {instructions}"
 
236
  entry = medication_name or instructions
237
 
238
  updated = f"{current_medications}\n{entry}".strip() if current_medications else entry
239
+ return updated, "", "", gr.update(choices=all_choices or [], value=None)
240
+
241
+
242
+ def clear_all_inputs(all_choices: list):
243
+ """Clear all input fields and reset medication picker."""
244
+ return (
245
+ "", # symptoms
246
+ "", # notes
247
+ "", # medications
248
+ "", # medication_name
249
+ "", # medication_instructions
250
+ gr.update(choices=all_choices or [], value=None), # medication_picker
251
+ DEFAULT_OUTPUT, # timeline_output
252
+ DEFAULT_OUTPUT, # questions_output
253
+ DEFAULT_OUTPUT, # relevant_output
254
+ )
255
 
256
 
257
  def populate_medication_name(selected_medication: str, current_name: str):
 
259
  return (selected_medication or current_name or "").strip()
260
 
261
 
262
+ def filter_medication_picker_choices(all_choices: list[str], evt: gr.KeyUpData):
263
+ """Return fuzzy medication choices using the dropdown's current typed text."""
264
+ return gr.update(choices=filter_medication_choices(evt.input_value, all_choices))
265
+
266
+
267
  def create_ui() -> gr.Blocks:
268
  app_cfg = settings.get("app", {})
269
  model_cfg = settings.get("model", {})
 
282
  temperature = _temperature_choice_value(
283
  selected_model_cfg.get("temperature", model_cfg.get("temperature", 0.3))
284
  )
285
+ all_medication_choices = load_medication_choices()
286
  medication_summary = medication_index_summary()
287
  deployment = app_cfg.get("deployment", "local")
288
  is_local_deployment = deployment == "local"
 
337
  gr.HTML(
338
  """
339
  <div class="pane-heading">
340
+ <h2>Tell us about your visit</h2>
 
341
  </div>
342
  """
343
  )
 
364
  medication_picker = gr.Dropdown(
365
  label="Find a Medication",
366
  show_label=False,
367
+ choices=all_medication_choices,
368
  value=None,
369
  filterable=True,
370
  allow_custom_value=True,
 
374
  medication_name = gr.Textbox(
375
  label="Medication Name",
376
  show_label=False,
377
+ placeholder="Select a medication above, or type any medication, vitamin, or supplement",
378
  lines=1,
379
  max_lines=2,
380
  elem_classes=["apple-input"],
 
418
  gr.HTML(
419
  """
420
  <div class="pane-heading">
421
+ <h2>Your appointment prep</h2>
 
422
  </div>
423
  """
424
  )
 
535
  outputs=[model_status, download_model_btn],
536
  api_visibility="private",
537
  )
538
+ # Create a state variable to hold all medication choices for filtering
539
+ medication_choices_state = gr.State(all_medication_choices)
540
+
541
+ medication_picker.key_up(
542
+ fn=filter_medication_picker_choices,
543
+ inputs=[medication_choices_state],
544
+ outputs=[medication_picker],
545
+ show_progress="hidden",
546
+ trigger_mode="always_last",
547
+ api_visibility="private",
548
+ )
549
  medication_picker.change(
550
  fn=populate_medication_name,
551
  inputs=[medication_picker, medication_name],
 
554
  )
555
  add_medication_btn.click(
556
  fn=add_medication_entry,
557
+ inputs=[medication_name, medication_instructions, medications, medication_choices_state],
558
  outputs=[medications, medication_name, medication_instructions, medication_picker],
559
  api_visibility="private",
560
  )
561
  clear_btn.click(
562
+ fn=clear_all_inputs,
563
+ inputs=[medication_choices_state],
564
  outputs=[
565
  symptoms,
566
  notes,
src/medications.py CHANGED
@@ -5,12 +5,17 @@ Medication autocomplete data helpers.
5
  from __future__ import annotations
6
 
7
  import json
 
 
8
  from functools import lru_cache
9
  from pathlib import Path
10
 
11
 
12
  ROOT_DIR = Path(__file__).resolve().parents[1]
13
  INDEX_PATH = ROOT_DIR / "data" / "medications" / "rxterms_medications.json"
 
 
 
14
 
15
 
16
  COMMON_SUPPLEMENTS = [
@@ -35,11 +40,14 @@ COMMON_SUPPLEMENTS = [
35
 
36
  @lru_cache(maxsize=1)
37
  def load_medication_choices() -> list[str]:
38
- """Return local RxTerms choices plus common supplement fallback choices."""
39
  choices: list[str] = []
40
 
41
- if INDEX_PATH.exists():
42
- with INDEX_PATH.open("r", encoding="utf-8") as handle:
 
 
 
43
  payload = json.load(handle)
44
  choices.extend(item["label"] for item in payload.get("medications", []))
45
 
@@ -52,13 +60,99 @@ def load_medication_choices() -> list[str]:
52
  return choices
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  def medication_index_summary() -> str:
56
- if not INDEX_PATH.exists():
57
- return "Medication autocomplete index is not installed."
 
 
 
58
 
59
- with INDEX_PATH.open("r", encoding="utf-8") as handle:
60
  payload = json.load(handle)
 
 
 
 
 
 
61
  return (
62
- f"{payload.get('source', 'RxTerms')} {payload.get('release', '')} "
63
- f"({payload.get('count', 0):,} medication choices)"
64
  )
 
5
  from __future__ import annotations
6
 
7
  import json
8
+ import re
9
+ from difflib import SequenceMatcher
10
  from functools import lru_cache
11
  from pathlib import Path
12
 
13
 
14
  ROOT_DIR = Path(__file__).resolve().parents[1]
15
  INDEX_PATH = ROOT_DIR / "data" / "medications" / "rxterms_medications.json"
16
+ COMMON_INDEX_PATH = ROOT_DIR / "data" / "medications" / "rxterms_medications_common.json"
17
+ DEFAULT_MEDICATION_SEARCH_LIMIT = 120
18
+ _NON_WORD_RE = re.compile(r"[^0-9a-z]+")
19
 
20
 
21
  COMMON_SUPPLEMENTS = [
 
40
 
41
  @lru_cache(maxsize=1)
42
  def load_medication_choices() -> list[str]:
43
+ """Return local RxTerms choices (pruned to common medications) plus common supplement fallback choices."""
44
  choices: list[str] = []
45
 
46
+ # Use pruned common medications list if available, otherwise fall back to full list
47
+ index_to_use = COMMON_INDEX_PATH if COMMON_INDEX_PATH.exists() else INDEX_PATH
48
+
49
+ if index_to_use.exists():
50
+ with index_to_use.open("r", encoding="utf-8") as handle:
51
  payload = json.load(handle)
52
  choices.extend(item["label"] for item in payload.get("medications", []))
53
 
 
60
  return choices
61
 
62
 
63
+ def _normalize_search_text(value: str) -> str:
64
+ normalized = _NON_WORD_RE.sub(" ", value.casefold())
65
+ return " ".join(normalized.split())
66
+
67
+
68
+ def _compact_search_text(value: str) -> str:
69
+ return _normalize_search_text(value).replace(" ", "")
70
+
71
+
72
+ def _best_sequence_ratio(query: str, choice: str) -> float:
73
+ search_terms = {_compact_search_text(choice)}
74
+ search_terms.update(
75
+ _compact_search_text(part)
76
+ for part in re.split(r"[\s()/,-]+", choice)
77
+ if len(part) >= 4
78
+ )
79
+ search_terms.discard("")
80
+ if not search_terms:
81
+ return 0.0
82
+
83
+ return max(SequenceMatcher(None, query, term).ratio() for term in search_terms)
84
+
85
+
86
+ def _medication_match_score(query: str, choice: str) -> tuple[float, ...] | None:
87
+ normalized_query = _normalize_search_text(query)
88
+ compact_query = normalized_query.replace(" ", "")
89
+ normalized_choice = _normalize_search_text(choice)
90
+ compact_choice = normalized_choice.replace(" ", "")
91
+
92
+ if not normalized_query:
93
+ return (0, 0)
94
+
95
+ if normalized_choice.startswith(normalized_query):
96
+ return (0, len(normalized_choice))
97
+
98
+ words = normalized_choice.split()
99
+ word_prefix_positions = [
100
+ index for index, word in enumerate(words) if word.startswith(normalized_query)
101
+ ]
102
+ if word_prefix_positions:
103
+ return (1, word_prefix_positions[0], len(normalized_choice))
104
+
105
+ substring_index = normalized_choice.find(normalized_query)
106
+ if substring_index != -1:
107
+ return (2, substring_index, len(normalized_choice))
108
+
109
+ compact_index = compact_choice.find(compact_query)
110
+ if compact_index != -1:
111
+ return (3, compact_index, len(compact_choice))
112
+
113
+ sequence_ratio = _best_sequence_ratio(compact_query, choice)
114
+ if sequence_ratio >= 0.72:
115
+ return (4, -sequence_ratio, len(compact_choice))
116
+
117
+ return None
118
+
119
+
120
+ def filter_medication_choices(
121
+ query: str,
122
+ choices: list[str],
123
+ limit: int | None = DEFAULT_MEDICATION_SEARCH_LIMIT,
124
+ ) -> list[str]:
125
+ """Return case-insensitive substring and fuzzy medication matches."""
126
+ if not query or not query.strip():
127
+ return list(choices)
128
+
129
+ matches = []
130
+ for index, choice in enumerate(choices):
131
+ score = _medication_match_score(query, choice)
132
+ if score is not None:
133
+ matches.append((score, index, choice))
134
+
135
+ matches.sort()
136
+ filtered = [choice for _score, _index, choice in matches]
137
+ return filtered[:limit] if limit is not None else filtered
138
+
139
+
140
  def medication_index_summary() -> str:
141
+ # Prefer common index if available
142
+ index_to_use = COMMON_INDEX_PATH if COMMON_INDEX_PATH.exists() else INDEX_PATH
143
+
144
+ if not index_to_use.exists():
145
+ return "You can type any medication, vitamin, or supplement below."
146
 
147
+ with index_to_use.open("r", encoding="utf-8") as handle:
148
  payload = json.load(handle)
149
+ count = int(payload.get("count", 0))
150
+ if count >= 2000:
151
+ return (
152
+ "Over 2,000 common medications are included. "
153
+ "You can also type any medication, vitamin, or supplement below."
154
+ )
155
  return (
156
+ "Common medications are included. "
157
+ "You can also type any medication, vitamin, or supplement below."
158
  )