karthikmulugu08 commited on
Commit
d5f2659
·
verified ·
1 Parent(s): 04f7846

Fix: flexible medication/allergy extraction for real-world document formats

Browse files
Files changed (1) hide show
  1. safety_checker.py +81 -25
safety_checker.py CHANGED
@@ -89,7 +89,8 @@ _SECTION_HEADER = re.compile(
89
  def _extract_lines(context: str) -> tuple[List[str], List[str]]:
90
  """
91
  Pull medication and allergy lines directly from document text using regex.
92
- Finds ALL MEDICATIONS/ALLERGIES sections per document (not just the first).
 
93
  """
94
  meds: List[str] = []
95
  allgs: List[str] = []
@@ -97,35 +98,90 @@ def _extract_lines(context: str) -> tuple[List[str], List[str]]:
97
  seen_a: set = set()
98
 
99
  def _clean(line: str) -> str:
100
- return re.sub(r'^[\d\.\-\*\s]+', '', line).strip()
101
 
102
  def _is_noise(line: str) -> bool:
103
  """Filter out section headers accidentally captured."""
104
  return bool(_SECTION_HEADER.match(line)) or len(line) < 3
105
 
106
- # Find all MEDICATIONS sections (there may be multiple per document)
107
- for m in re.finditer(
108
- r'MEDICATIONS?[^\n]*\n((?:[ \t]*[-\d*].*\n?){1,20})',
109
- context, re.IGNORECASE
110
- ):
111
- for line in m.group(1).splitlines():
112
- line = _clean(line)
113
- key = line.lower()[:30]
114
- if line and not _is_noise(line) and key not in seen_m:
115
- seen_m.add(key)
116
- meds.append(line)
117
-
118
- # Find all ALLERGIES sections
119
- for m in re.finditer(
120
- r'ALLERGIES?[^\n]*\n((?:[ \t]*[-\d*].*\n?){1,10})',
121
- context, re.IGNORECASE
122
- ):
123
- for line in m.group(1).splitlines():
124
- line = _clean(line)
125
- key = line.lower()[:30]
126
- if line and not _is_noise(line) and key not in seen_a:
127
- seen_a.add(key)
128
- allgs.append(line)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  return meds, allgs
131
 
 
89
  def _extract_lines(context: str) -> tuple[List[str], List[str]]:
90
  """
91
  Pull medication and allergy lines directly from document text using regex.
92
+ Covers many real-world clinical document formats, not just standard headers.
93
+ Finds ALL matching sections per document (not just the first).
94
  """
95
  meds: List[str] = []
96
  allgs: List[str] = []
 
98
  seen_a: set = set()
99
 
100
  def _clean(line: str) -> str:
101
+ return re.sub(r'^[\d\.\-\*\•\s]+', '', line).strip()
102
 
103
  def _is_noise(line: str) -> bool:
104
  """Filter out section headers accidentally captured."""
105
  return bool(_SECTION_HEADER.match(line)) or len(line) < 3
106
 
107
+ # ── Medication section headers covers common real-world formats ──────────
108
+ # Examples: "MEDICATIONS:", "Current Medications:", "Active Medications:",
109
+ # "Medication List:", "Rx:", "Prescriptions:", "Drug List:",
110
+ # "Home Medications:", "Discharge Medications:", "Outpatient Meds:"
111
+ MED_HEADER = re.compile(
112
+ r'(?:current\s+|active\s+|home\s+|discharge\s+|outpatient\s+|admission\s+|'
113
+ r'inpatient\s+|chronic\s+|long.?term\s+)?'
114
+ r'(?:medications?|meds?|medicines?|drugs?|prescriptions?|rx)'
115
+ r'(?:\s+list)?'
116
+ r'[^\n]{0,30}\n',
117
+ re.IGNORECASE,
118
+ )
119
+
120
+ # ── Allergy section headers — covers common real-world formats ─────────────
121
+ # Examples: "ALLERGIES:", "Drug Allergies:", "Known Allergies:",
122
+ # "Allergy List:", "Sensitivities:", "Adverse Reactions:",
123
+ # "Drug Intolerances:", "NKA" (no known allergies)
124
+ ALLERGY_HEADER = re.compile(
125
+ r'(?:drug\s+|food\s+|environmental\s+|known\s+|reported\s+)?'
126
+ r'(?:allergies?|allergy|sensitivities|sensitivity|intolerances?|'
127
+ r'adverse\s+reactions?|hypersensitivities?)'
128
+ r'(?:\s+list)?'
129
+ r'[^\n]{0,30}\n',
130
+ re.IGNORECASE,
131
+ )
132
+
133
+ # ── Also catch inline bullet/colon lists after a header on the same line ───
134
+ # e.g. "Allergies: Penicillin (rash), Sulfa"
135
+ ALLERGY_INLINE = re.compile(
136
+ r'(?:drug\s+|known\s+)?(?:allergies?|allergy|sensitivities?|adverse\s+reactions?)'
137
+ r'\s*:\s*([^\n]{3,200})',
138
+ re.IGNORECASE,
139
+ )
140
+ MED_INLINE = re.compile(
141
+ r'(?:current\s+|active\s+|home\s+)?(?:medications?|meds?|rx)'
142
+ r'\s*:\s*([^\n]{3,200})',
143
+ re.IGNORECASE,
144
+ )
145
+
146
+ def _harvest(pattern, context, seen, out, max_lines=20):
147
+ """Find all sections matching pattern and extract bullet/numbered lines."""
148
+ for match in re.finditer(pattern, context):
149
+ # Grab the block of lines after the header
150
+ start = match.end()
151
+ block = context[start:start + 1200]
152
+ count = 0
153
+ for line in block.splitlines():
154
+ if count >= max_lines:
155
+ break
156
+ # Stop if we hit another section header (blank-ish line or ALL CAPS heading)
157
+ stripped = line.strip()
158
+ if not stripped:
159
+ continue
160
+ if re.match(r'^[A-Z][A-Z\s]{4,}:', stripped) and not re.match(r'^[A-Z][a-z]', stripped):
161
+ break
162
+ cleaned = _clean(stripped)
163
+ key = cleaned.lower()[:30]
164
+ if cleaned and not _is_noise(cleaned) and key not in seen:
165
+ seen.add(key)
166
+ out.append(cleaned)
167
+ count += 1
168
+
169
+ def _harvest_inline(pattern, context, seen, out):
170
+ """Catch single-line format: 'Allergies: Penicillin, Sulfa'"""
171
+ for match in re.finditer(pattern, context):
172
+ raw = match.group(1)
173
+ # Split on commas or semicolons
174
+ for part in re.split(r'[,;]', raw):
175
+ cleaned = _clean(part.strip())
176
+ key = cleaned.lower()[:30]
177
+ if cleaned and not _is_noise(cleaned) and key not in seen:
178
+ seen.add(key)
179
+ out.append(cleaned)
180
+
181
+ _harvest(MED_HEADER, context, seen_m, meds, max_lines=20)
182
+ _harvest(ALLERGY_HEADER, context, seen_a, allgs, max_lines=10)
183
+ _harvest_inline(MED_INLINE, context, seen_m, meds)
184
+ _harvest_inline(ALLERGY_INLINE, context, seen_a, allgs)
185
 
186
  return meds, allgs
187