visproj commited on
Commit
2b51f40
·
verified ·
1 Parent(s): d78bfb0

Update providers/openfda_provider.py

Browse files
Files changed (1) hide show
  1. providers/openfda_provider.py +573 -573
providers/openfda_provider.py CHANGED
@@ -1,573 +1,573 @@
1
- """OpenFDA API Provider for adverse events, drug labels, recalls, and more."""
2
-
3
- import re
4
- import logging
5
- from typing import List, Dict, Any, Callable, Optional
6
- import httpx
7
- from functools import lru_cache
8
-
9
- from ..core.base_provider import BaseProvider
10
- from ..core.decorators import safe_json_return, with_retry
11
- from . import register_provider
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- # OpenFDA API endpoints
16
- OPENFDA_DRUG_EVENT = "https://api.fda.gov/drug/event.json"
17
- OPENFDA_DRUG_LABEL = "https://api.fda.gov/drug/label.json"
18
- OPENFDA_DRUG_NDC = "https://api.fda.gov/drug/ndc.json"
19
- OPENFDA_DRUG_ENFORCEMENT = "https://api.fda.gov/drug/enforcement.json"
20
- OPENFDA_DRUG_DRUGSFDA = "https://api.fda.gov/drug/drugsfda.json"
21
- OPENFDA_DEVICE_EVENT = "https://api.fda.gov/device/event.json"
22
- OPENFDA_DEVICE_ENFORCEMENT = "https://api.fda.gov/device/enforcement.json"
23
- OPENFDA_DEVICE_CLASSIFICATION = "https://api.fda.gov/device/classification.json"
24
- OPENFDA_DEVICE_510K = "https://api.fda.gov/device/510k.json"
25
- OPENFDA_DEVICE_PMA = "https://api.fda.gov/device/pma.json"
26
- OPENFDA_FOOD_ENFORCEMENT = "https://api.fda.gov/food/enforcement.json"
27
- OPENFDA_FOOD_EVENT = "https://api.fda.gov/food/event.json"
28
-
29
-
30
- def normalize_drug_name(raw_name: str) -> str:
31
- """Extract base drug name from full medication name."""
32
- if not raw_name:
33
- return ""
34
-
35
- cleaned = raw_name
36
- patterns = [
37
- r'\s+\d+(?:\.\d+)?\s*(MG|MCG|ML|G|%)',
38
- r'\s+Oral\s+',
39
- r'\s+Tablet\s*',
40
- r'\s+Capsule\s*',
41
- r'\s+Injectable\s*',
42
- r'\s+Solution\s*',
43
- r'\s+Suspension\s*'
44
- ]
45
-
46
- for pattern in patterns:
47
- cleaned = re.split(pattern, cleaned, flags=re.IGNORECASE)[0]
48
-
49
- base_name = cleaned.strip().split()[0] if cleaned.strip() else cleaned.strip()
50
- return base_name
51
-
52
-
53
- @register_provider("openfda")
54
- class OpenFDAProvider(BaseProvider):
55
- """Provider for OpenFDA APIs."""
56
-
57
- def __init__(self, client: httpx.AsyncClient, api_key: Optional[str] = None):
58
- super().__init__("openfda", client)
59
- self.api_key = api_key
60
-
61
- async def initialize(self) -> None:
62
- """Initialize OpenFDA provider."""
63
- logger.info("OpenFDA provider initialized")
64
-
65
- def get_tools(self) -> List[Callable]:
66
- """Return all OpenFDA tools."""
67
- return [
68
- self.openfda_get_adverse_event_summary,
69
- self.openfda_fetch_adverse_events,
70
- self.openfda_top_reactions,
71
- self.openfda_search_drug_labels,
72
- self.openfda_search_ndc,
73
- self.openfda_search_drug_recalls,
74
- self.openfda_search_drugs_fda,
75
- self.openfda_search_device_events,
76
- self.openfda_search_device_recalls,
77
- self.openfda_search_device_classifications,
78
- self.openfda_search_510k,
79
- self.openfda_search_pma,
80
- self.openfda_search_food_recalls,
81
- self.openfda_search_food_events,
82
- ]
83
-
84
- def _build_params(self, search: str, limit: int = 10) -> Dict[str, Any]:
85
- """Build query parameters with optional API key."""
86
- params = {"search": search, "limit": min(limit, 100)}
87
- if self.api_key:
88
- params["api_key"] = self.api_key
89
- return params
90
-
91
- @with_retry
92
- async def _fetch_fda_data(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
93
- """Fetch data from OpenFDA API with retry logic."""
94
- response = await self.client.get(url, params=params)
95
- response.raise_for_status()
96
- return response.json()
97
-
98
- # Drug Adverse Events
99
- @safe_json_return
100
- async def openfda_get_adverse_event_summary(self, drug_name: str) -> Dict[str, Any]:
101
- """
102
- Get high-level adverse event summary for a medication from FAERS database.
103
-
104
- Args:
105
- drug_name: Name of the medication (generic or brand name)
106
-
107
- Returns:
108
- Summary with total reports, serious reports, and top reactions
109
- """
110
- clean_name = normalize_drug_name(drug_name)
111
- logger.info(f"FDA adverse event query: '{drug_name}' -> '{clean_name}'")
112
-
113
- # Query for count and reactions
114
- search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
115
- params = self._build_params(search_query, limit=1)
116
- params["count"] = "patient.reaction.reactionmeddrapt.exact"
117
-
118
- data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
119
-
120
- total_reports = data.get("meta", {}).get("results", {}).get("total", 0)
121
- reactions = data.get("results", [])[:5]
122
-
123
- top_reactions = [
124
- {"reaction": r.get("term", "Unknown"), "count": r.get("count", 0)}
125
- for r in reactions
126
- ]
127
-
128
- # Get serious event count
129
- serious_query = f'{search_query}+AND+serious:1'
130
- serious_params = self._build_params(serious_query, limit=1)
131
- try:
132
- serious_data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, serious_params)
133
- serious_reports = serious_data.get("meta", {}).get("results", {}).get("total", 0)
134
- except Exception:
135
- serious_reports = 0
136
-
137
- return {
138
- "drug": clean_name,
139
- "total_reports": total_reports,
140
- "serious_reports": serious_reports,
141
- "top_reactions": top_reactions
142
- }
143
-
144
- @safe_json_return
145
- async def openfda_fetch_adverse_events(
146
- self,
147
- drug_name: str,
148
- limit: int = 25,
149
- max_pages: int = 1
150
- ) -> Dict[str, Any]:
151
- """
152
- Fetch raw adverse event reports from OpenFDA with pagination.
153
-
154
- Args:
155
- drug_name: Name of the medication
156
- limit: Number of events per page (max 100)
157
- max_pages: Number of pages to fetch
158
-
159
- Returns:
160
- List of adverse event reports with metadata
161
- """
162
- clean_name = normalize_drug_name(drug_name)
163
- search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
164
-
165
- all_events = []
166
- for page in range(max_pages):
167
- params = self._build_params(search_query, limit=min(limit, 100))
168
- params["skip"] = page * limit
169
-
170
- try:
171
- data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
172
- results = data.get("results", [])
173
-
174
- for result in results:
175
- reactions = result.get("patient", {}).get("reaction", [])
176
- reaction_list = [r.get("reactionmeddrapt", "Unknown") for r in reactions]
177
-
178
- all_events.append({
179
- "safety_report_id": result.get("safetyreportid", "Unknown"),
180
- "receive_date": result.get("receivedate", "Unknown"),
181
- "serious": result.get("serious", 0) == 1,
182
- "reactions": reaction_list[:5],
183
- "patient_age": result.get("patient", {}).get("patientonsetage", "Unknown"),
184
- "patient_sex": result.get("patient", {}).get("patientsex", "Unknown")
185
- })
186
-
187
- if len(results) < limit:
188
- break
189
- except Exception as e:
190
- logger.warning(f"Error fetching page {page}: {e}")
191
- break
192
-
193
- return {
194
- "drug": clean_name,
195
- "events": all_events,
196
- "total_fetched": len(all_events)
197
- }
198
-
199
- @safe_json_return
200
- async def openfda_top_reactions(self, drug_name: str) -> Dict[str, Any]:
201
- """
202
- Get top 5 most commonly reported adverse reactions for a medication.
203
-
204
- Args:
205
- drug_name: Name of the medication
206
-
207
- Returns:
208
- Top 5 reactions with counts
209
- """
210
- clean_name = normalize_drug_name(drug_name)
211
- search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
212
-
213
- params = self._build_params(search_query, limit=1)
214
- params["count"] = "patient.reaction.reactionmeddrapt.exact"
215
-
216
- data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
217
- reactions = data.get("results", [])[:5]
218
-
219
- return {
220
- "drug": clean_name,
221
- "top_reactions": [
222
- {"reaction": r.get("term", "Unknown"), "count": r.get("count", 0)}
223
- for r in reactions
224
- ]
225
- }
226
-
227
- # Drug Labels
228
- @safe_json_return
229
- async def openfda_search_drug_labels(self, query: str, limit: int = 10) -> Dict[str, Any]:
230
- """
231
- Search FDA drug labeling information (warnings, indications, dosage).
232
-
233
- Args:
234
- query: Drug name, active ingredient, or condition
235
- limit: Maximum results (max 100)
236
-
237
- Returns:
238
- Drug label information
239
- """
240
- params = self._build_params(query, limit)
241
- data = await self._fetch_fda_data(OPENFDA_DRUG_LABEL, params)
242
-
243
- results = data.get("results", [])
244
- labels = []
245
-
246
- for result in results:
247
- openfda = result.get("openfda", {})
248
- labels.append({
249
- "brand_name": openfda.get("brand_name", ["Unknown"])[0],
250
- "generic_name": openfda.get("generic_name", ["Unknown"])[0],
251
- "manufacturer": openfda.get("manufacturer_name", ["Unknown"])[0],
252
- "purpose": result.get("purpose", ["Not specified"])[0] if result.get("purpose") else "Not specified",
253
- "warnings": result.get("warnings", ["Not specified"])[0][:500] if result.get("warnings") else "Not specified",
254
- "indications_and_usage": result.get("indications_and_usage", ["Not specified"])[0][:500] if result.get("indications_and_usage") else "Not specified"
255
- })
256
-
257
- return {"results": labels, "total": len(labels)}
258
-
259
- # NDC Directory
260
- @safe_json_return
261
- async def openfda_search_ndc(self, query: str, limit: int = 10) -> Dict[str, Any]:
262
- """
263
- Search National Drug Code (NDC) directory for drug product information.
264
-
265
- Args:
266
- query: Brand name, generic name, or NDC number
267
- limit: Maximum results (max 100)
268
-
269
- Returns:
270
- NDC product information
271
- """
272
- params = self._build_params(query, limit)
273
- data = await self._fetch_fda_data(OPENFDA_DRUG_NDC, params)
274
-
275
- results = data.get("results", [])
276
- products = []
277
-
278
- for result in results:
279
- products.append({
280
- "product_ndc": result.get("product_ndc", "Unknown"),
281
- "brand_name": result.get("brand_name", "Unknown"),
282
- "generic_name": result.get("generic_name", "Unknown"),
283
- "manufacturer": result.get("labeler_name", "Unknown"),
284
- "dosage_form": result.get("dosage_form", "Unknown"),
285
- "route": result.get("route", ["Unknown"])[0] if result.get("route") else "Unknown",
286
- "marketing_status": result.get("marketing_status", "Unknown")
287
- })
288
-
289
- return {"results": products, "total": len(products)}
290
-
291
- # Drug Recalls
292
- @safe_json_return
293
- async def openfda_search_drug_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
294
- """
295
- Search FDA drug recall and enforcement reports.
296
-
297
- Args:
298
- query: Drug name, manufacturer, or reason for recall
299
- limit: Maximum results (max 100)
300
-
301
- Returns:
302
- Drug recall information
303
- """
304
- params = self._build_params(query, limit)
305
- data = await self._fetch_fda_data(OPENFDA_DRUG_ENFORCEMENT, params)
306
-
307
- results = data.get("results", [])
308
- recalls = []
309
-
310
- for result in results:
311
- recalls.append({
312
- "product_description": result.get("product_description", "Unknown"),
313
- "reason_for_recall": result.get("reason_for_recall", "Unknown"),
314
- "classification": result.get("classification", "Unknown"),
315
- "status": result.get("status", "Unknown"),
316
- "recall_date": result.get("recall_initiation_date", "Unknown"),
317
- "recalling_firm": result.get("recalling_firm", "Unknown")
318
- })
319
-
320
- return {"results": recalls, "total": len(recalls)}
321
-
322
- # Drugs@FDA
323
- @safe_json_return
324
- async def openfda_search_drugs_fda(self, query: str, limit: int = 10) -> Dict[str, Any]:
325
- """
326
- Search Drugs@FDA database for approved drug products and applications.
327
-
328
- Args:
329
- query: Drug name or active ingredient
330
- limit: Maximum results (max 100)
331
-
332
- Returns:
333
- Approved drug information
334
- """
335
- params = self._build_params(query, limit)
336
- data = await self._fetch_fda_data(OPENFDA_DRUG_DRUGSFDA, params)
337
-
338
- results = data.get("results", [])
339
- drugs = []
340
-
341
- for result in results:
342
- products = result.get("products", [])
343
- for product in products:
344
- drugs.append({
345
- "application_number": result.get("application_number", "Unknown"),
346
- "sponsor_name": result.get("sponsor_name", "Unknown"),
347
- "brand_name": product.get("brand_name", "Unknown"),
348
- "active_ingredients": product.get("active_ingredients", []),
349
- "dosage_form": product.get("dosage_form", "Unknown"),
350
- "route": product.get("route", "Unknown"),
351
- "marketing_status": product.get("marketing_status", "Unknown")
352
- })
353
-
354
- return {"results": drugs, "total": len(drugs)}
355
-
356
- # Device Events
357
- @safe_json_return
358
- async def openfda_search_device_events(self, query: str, limit: int = 10) -> Dict[str, Any]:
359
- """
360
- Search medical device adverse event reports.
361
-
362
- Args:
363
- query: Device name or brand
364
- limit: Maximum results (max 100)
365
-
366
- Returns:
367
- Device adverse event information
368
- """
369
- params = self._build_params(query, limit)
370
- data = await self._fetch_fda_data(OPENFDA_DEVICE_EVENT, params)
371
-
372
- results = data.get("results", [])
373
- events = []
374
-
375
- for result in results:
376
- device = result.get("device", [{}])[0]
377
- events.append({
378
- "report_number": result.get("report_number", "Unknown"),
379
- "date_received": result.get("date_received", "Unknown"),
380
- "device_name": device.get("brand_name", "Unknown"),
381
- "manufacturer": device.get("manufacturer_d_name", "Unknown"),
382
- "event_type": result.get("event_type", "Unknown"),
383
- "device_problem": device.get("device_problem_codes", ["Unknown"])[0] if device.get("device_problem_codes") else "Unknown"
384
- })
385
-
386
- return {"results": events, "total": len(events)}
387
-
388
- # Device Recalls
389
- @safe_json_return
390
- async def openfda_search_device_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
391
- """
392
- Search medical device recall and enforcement reports.
393
-
394
- Args:
395
- query: Device name, manufacturer, or reason
396
- limit: Maximum results (max 100)
397
-
398
- Returns:
399
- Device recall information
400
- """
401
- params = self._build_params(query, limit)
402
- data = await self._fetch_fda_data(OPENFDA_DEVICE_ENFORCEMENT, params)
403
-
404
- results = data.get("results", [])
405
- recalls = []
406
-
407
- for result in results:
408
- recalls.append({
409
- "product_description": result.get("product_description", "Unknown"),
410
- "reason_for_recall": result.get("reason_for_recall", "Unknown"),
411
- "classification": result.get("classification", "Unknown"),
412
- "status": result.get("status", "Unknown"),
413
- "recall_date": result.get("recall_initiation_date", "Unknown"),
414
- "recalling_firm": result.get("recalling_firm", "Unknown")
415
- })
416
-
417
- return {"results": recalls, "total": len(recalls)}
418
-
419
- # Device Classifications
420
- @safe_json_return
421
- async def openfda_search_device_classifications(self, query: str, limit: int = 10) -> Dict[str, Any]:
422
- """
423
- Search medical device classification database.
424
-
425
- Args:
426
- query: Device type or classification
427
- limit: Maximum results (max 100)
428
-
429
- Returns:
430
- Device classification information
431
- """
432
- params = self._build_params(query, limit)
433
- data = await self._fetch_fda_data(OPENFDA_DEVICE_CLASSIFICATION, params)
434
-
435
- results = data.get("results", [])
436
- classifications = []
437
-
438
- for result in results:
439
- classifications.append({
440
- "device_name": result.get("device_name", "Unknown"),
441
- "device_class": result.get("device_class", "Unknown"),
442
- "medical_specialty": result.get("medical_specialty_description", "Unknown"),
443
- "regulation_number": result.get("regulation_number", "Unknown"),
444
- "product_code": result.get("product_code", "Unknown")
445
- })
446
-
447
- return {"results": classifications, "total": len(classifications)}
448
-
449
- # 510(k) Clearances
450
- @safe_json_return
451
- async def openfda_search_510k(self, query: str, limit: int = 10) -> Dict[str, Any]:
452
- """
453
- Search FDA 510(k) premarket clearance database.
454
-
455
- Args:
456
- query: Device name or manufacturer
457
- limit: Maximum results (max 100)
458
-
459
- Returns:
460
- 510(k) clearance information
461
- """
462
- params = self._build_params(query, limit)
463
- data = await self._fetch_fda_data(OPENFDA_DEVICE_510K, params)
464
-
465
- results = data.get("results", [])
466
- clearances = []
467
-
468
- for result in results:
469
- clearances.append({
470
- "k_number": result.get("k_number", "Unknown"),
471
- "device_name": result.get("device_name", "Unknown"),
472
- "applicant": result.get("applicant", "Unknown"),
473
- "clearance_date": result.get("date_received", "Unknown"),
474
- "decision_description": result.get("decision_description", "Unknown"),
475
- "product_code": result.get("product_code", "Unknown")
476
- })
477
-
478
- return {"results": clearances, "total": len(clearances)}
479
-
480
- # PMA Approvals
481
- @safe_json_return
482
- async def openfda_search_pma(self, query: str, limit: int = 10) -> Dict[str, Any]:
483
- """
484
- Search FDA premarket approval (PMA) database.
485
-
486
- Args:
487
- query: Device name or manufacturer
488
- limit: Maximum results (max 100)
489
-
490
- Returns:
491
- PMA approval information
492
- """
493
- params = self._build_params(query, limit)
494
- data = await self._fetch_fda_data(OPENFDA_DEVICE_PMA, params)
495
-
496
- results = data.get("results", [])
497
- approvals = []
498
-
499
- for result in results:
500
- approvals.append({
501
- "pma_number": result.get("pma_number", "Unknown"),
502
- "device_name": result.get("device_name", "Unknown"),
503
- "applicant": result.get("applicant", "Unknown"),
504
- "approval_date": result.get("date_received", "Unknown"),
505
- "decision_description": result.get("decision_description", "Unknown"),
506
- "product_code": result.get("product_code", "Unknown")
507
- })
508
-
509
- return {"results": approvals, "total": len(approvals)}
510
-
511
- # Food Recalls
512
- @safe_json_return
513
- async def openfda_search_food_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
514
- """
515
- Search FDA food recall and enforcement reports.
516
-
517
- Args:
518
- query: Food product or reason for recall
519
- limit: Maximum results (max 100)
520
-
521
- Returns:
522
- Food recall information
523
- """
524
- params = self._build_params(query, limit)
525
- data = await self._fetch_fda_data(OPENFDA_FOOD_ENFORCEMENT, params)
526
-
527
- results = data.get("results", [])
528
- recalls = []
529
-
530
- for result in results:
531
- recalls.append({
532
- "product_description": result.get("product_description", "Unknown"),
533
- "reason_for_recall": result.get("reason_for_recall", "Unknown"),
534
- "classification": result.get("classification", "Unknown"),
535
- "status": result.get("status", "Unknown"),
536
- "recall_date": result.get("recall_initiation_date", "Unknown"),
537
- "recalling_firm": result.get("recalling_firm", "Unknown")
538
- })
539
-
540
- return {"results": recalls, "total": len(recalls)}
541
-
542
- # Food Events
543
- @safe_json_return
544
- async def openfda_search_food_events(self, query: str, limit: int = 10) -> Dict[str, Any]:
545
- """
546
- Search FDA food adverse event reports.
547
-
548
- Args:
549
- query: Food product or reaction
550
- limit: Maximum results (max 100)
551
-
552
- Returns:
553
- Food adverse event information
554
- """
555
- params = self._build_params(query, limit)
556
- data = await self._fetch_fda_data(OPENFDA_FOOD_EVENT, params)
557
-
558
- results = data.get("results", [])
559
- events = []
560
-
561
- for result in results:
562
- products = result.get("products", [{}])
563
- reactions = result.get("reactions", [])
564
-
565
- events.append({
566
- "report_number": result.get("report_number", "Unknown"),
567
- "date_started": result.get("date_started", "Unknown"),
568
- "products": [p.get("name_brand", "Unknown") for p in products],
569
- "reactions": [r.get("reaction", "Unknown") for r in reactions][:5],
570
- "outcomes": result.get("outcomes", ["Unknown"])
571
- })
572
-
573
- return {"results": events, "total": len(events)}
 
1
+ """OpenFDA API Provider for adverse events, drug labels, recalls, and more."""
2
+
3
+ import re
4
+ import logging
5
+ from typing import List, Dict, Any, Callable, Optional
6
+ import httpx
7
+ from functools import lru_cache
8
+
9
+ from core.base_provider import BaseProvider
10
+ from core.decorators import safe_json_return, with_retry
11
+ from providers import register_provider
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # OpenFDA API endpoints
16
+ OPENFDA_DRUG_EVENT = "https://api.fda.gov/drug/event.json"
17
+ OPENFDA_DRUG_LABEL = "https://api.fda.gov/drug/label.json"
18
+ OPENFDA_DRUG_NDC = "https://api.fda.gov/drug/ndc.json"
19
+ OPENFDA_DRUG_ENFORCEMENT = "https://api.fda.gov/drug/enforcement.json"
20
+ OPENFDA_DRUG_DRUGSFDA = "https://api.fda.gov/drug/drugsfda.json"
21
+ OPENFDA_DEVICE_EVENT = "https://api.fda.gov/device/event.json"
22
+ OPENFDA_DEVICE_ENFORCEMENT = "https://api.fda.gov/device/enforcement.json"
23
+ OPENFDA_DEVICE_CLASSIFICATION = "https://api.fda.gov/device/classification.json"
24
+ OPENFDA_DEVICE_510K = "https://api.fda.gov/device/510k.json"
25
+ OPENFDA_DEVICE_PMA = "https://api.fda.gov/device/pma.json"
26
+ OPENFDA_FOOD_ENFORCEMENT = "https://api.fda.gov/food/enforcement.json"
27
+ OPENFDA_FOOD_EVENT = "https://api.fda.gov/food/event.json"
28
+
29
+
30
+ def normalize_drug_name(raw_name: str) -> str:
31
+ """Extract base drug name from full medication name."""
32
+ if not raw_name:
33
+ return ""
34
+
35
+ cleaned = raw_name
36
+ patterns = [
37
+ r'\s+\d+(?:\.\d+)?\s*(MG|MCG|ML|G|%)',
38
+ r'\s+Oral\s+',
39
+ r'\s+Tablet\s*',
40
+ r'\s+Capsule\s*',
41
+ r'\s+Injectable\s*',
42
+ r'\s+Solution\s*',
43
+ r'\s+Suspension\s*'
44
+ ]
45
+
46
+ for pattern in patterns:
47
+ cleaned = re.split(pattern, cleaned, flags=re.IGNORECASE)[0]
48
+
49
+ base_name = cleaned.strip().split()[0] if cleaned.strip() else cleaned.strip()
50
+ return base_name
51
+
52
+
53
+ @register_provider("openfda")
54
+ class OpenFDAProvider(BaseProvider):
55
+ """Provider for OpenFDA APIs."""
56
+
57
+ def __init__(self, client: httpx.AsyncClient, api_key: Optional[str] = None):
58
+ super().__init__("openfda", client)
59
+ self.api_key = api_key
60
+
61
+ async def initialize(self) -> None:
62
+ """Initialize OpenFDA provider."""
63
+ logger.info("OpenFDA provider initialized")
64
+
65
+ def get_tools(self) -> List[Callable]:
66
+ """Return all OpenFDA tools."""
67
+ return [
68
+ self.openfda_get_adverse_event_summary,
69
+ self.openfda_fetch_adverse_events,
70
+ self.openfda_top_reactions,
71
+ self.openfda_search_drug_labels,
72
+ self.openfda_search_ndc,
73
+ self.openfda_search_drug_recalls,
74
+ self.openfda_search_drugs_fda,
75
+ self.openfda_search_device_events,
76
+ self.openfda_search_device_recalls,
77
+ self.openfda_search_device_classifications,
78
+ self.openfda_search_510k,
79
+ self.openfda_search_pma,
80
+ self.openfda_search_food_recalls,
81
+ self.openfda_search_food_events,
82
+ ]
83
+
84
+ def _build_params(self, search: str, limit: int = 10) -> Dict[str, Any]:
85
+ """Build query parameters with optional API key."""
86
+ params = {"search": search, "limit": min(limit, 100)}
87
+ if self.api_key:
88
+ params["api_key"] = self.api_key
89
+ return params
90
+
91
+ @with_retry
92
+ async def _fetch_fda_data(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
93
+ """Fetch data from OpenFDA API with retry logic."""
94
+ response = await self.client.get(url, params=params)
95
+ response.raise_for_status()
96
+ return response.json()
97
+
98
+ # Drug Adverse Events
99
+ @safe_json_return
100
+ async def openfda_get_adverse_event_summary(self, drug_name: str) -> Dict[str, Any]:
101
+ """
102
+ Get high-level adverse event summary for a medication from FAERS database.
103
+
104
+ Args:
105
+ drug_name: Name of the medication (generic or brand name)
106
+
107
+ Returns:
108
+ Summary with total reports, serious reports, and top reactions
109
+ """
110
+ clean_name = normalize_drug_name(drug_name)
111
+ logger.info(f"FDA adverse event query: '{drug_name}' -> '{clean_name}'")
112
+
113
+ # Query for count and reactions
114
+ search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
115
+ params = self._build_params(search_query, limit=1)
116
+ params["count"] = "patient.reaction.reactionmeddrapt.exact"
117
+
118
+ data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
119
+
120
+ total_reports = data.get("meta", {}).get("results", {}).get("total", 0)
121
+ reactions = data.get("results", [])[:5]
122
+
123
+ top_reactions = [
124
+ {"reaction": r.get("term", "Unknown"), "count": r.get("count", 0)}
125
+ for r in reactions
126
+ ]
127
+
128
+ # Get serious event count
129
+ serious_query = f'{search_query}+AND+serious:1'
130
+ serious_params = self._build_params(serious_query, limit=1)
131
+ try:
132
+ serious_data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, serious_params)
133
+ serious_reports = serious_data.get("meta", {}).get("results", {}).get("total", 0)
134
+ except Exception:
135
+ serious_reports = 0
136
+
137
+ return {
138
+ "drug": clean_name,
139
+ "total_reports": total_reports,
140
+ "serious_reports": serious_reports,
141
+ "top_reactions": top_reactions
142
+ }
143
+
144
+ @safe_json_return
145
+ async def openfda_fetch_adverse_events(
146
+ self,
147
+ drug_name: str,
148
+ limit: int = 25,
149
+ max_pages: int = 1
150
+ ) -> Dict[str, Any]:
151
+ """
152
+ Fetch raw adverse event reports from OpenFDA with pagination.
153
+
154
+ Args:
155
+ drug_name: Name of the medication
156
+ limit: Number of events per page (max 100)
157
+ max_pages: Number of pages to fetch
158
+
159
+ Returns:
160
+ List of adverse event reports with metadata
161
+ """
162
+ clean_name = normalize_drug_name(drug_name)
163
+ search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
164
+
165
+ all_events = []
166
+ for page in range(max_pages):
167
+ params = self._build_params(search_query, limit=min(limit, 100))
168
+ params["skip"] = page * limit
169
+
170
+ try:
171
+ data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
172
+ results = data.get("results", [])
173
+
174
+ for result in results:
175
+ reactions = result.get("patient", {}).get("reaction", [])
176
+ reaction_list = [r.get("reactionmeddrapt", "Unknown") for r in reactions]
177
+
178
+ all_events.append({
179
+ "safety_report_id": result.get("safetyreportid", "Unknown"),
180
+ "receive_date": result.get("receivedate", "Unknown"),
181
+ "serious": result.get("serious", 0) == 1,
182
+ "reactions": reaction_list[:5],
183
+ "patient_age": result.get("patient", {}).get("patientonsetage", "Unknown"),
184
+ "patient_sex": result.get("patient", {}).get("patientsex", "Unknown")
185
+ })
186
+
187
+ if len(results) < limit:
188
+ break
189
+ except Exception as e:
190
+ logger.warning(f"Error fetching page {page}: {e}")
191
+ break
192
+
193
+ return {
194
+ "drug": clean_name,
195
+ "events": all_events,
196
+ "total_fetched": len(all_events)
197
+ }
198
+
199
+ @safe_json_return
200
+ async def openfda_top_reactions(self, drug_name: str) -> Dict[str, Any]:
201
+ """
202
+ Get top 5 most commonly reported adverse reactions for a medication.
203
+
204
+ Args:
205
+ drug_name: Name of the medication
206
+
207
+ Returns:
208
+ Top 5 reactions with counts
209
+ """
210
+ clean_name = normalize_drug_name(drug_name)
211
+ search_query = f'patient.drug.medicinalproduct:"{clean_name}"'
212
+
213
+ params = self._build_params(search_query, limit=1)
214
+ params["count"] = "patient.reaction.reactionmeddrapt.exact"
215
+
216
+ data = await self._fetch_fda_data(OPENFDA_DRUG_EVENT, params)
217
+ reactions = data.get("results", [])[:5]
218
+
219
+ return {
220
+ "drug": clean_name,
221
+ "top_reactions": [
222
+ {"reaction": r.get("term", "Unknown"), "count": r.get("count", 0)}
223
+ for r in reactions
224
+ ]
225
+ }
226
+
227
+ # Drug Labels
228
+ @safe_json_return
229
+ async def openfda_search_drug_labels(self, query: str, limit: int = 10) -> Dict[str, Any]:
230
+ """
231
+ Search FDA drug labeling information (warnings, indications, dosage).
232
+
233
+ Args:
234
+ query: Drug name, active ingredient, or condition
235
+ limit: Maximum results (max 100)
236
+
237
+ Returns:
238
+ Drug label information
239
+ """
240
+ params = self._build_params(query, limit)
241
+ data = await self._fetch_fda_data(OPENFDA_DRUG_LABEL, params)
242
+
243
+ results = data.get("results", [])
244
+ labels = []
245
+
246
+ for result in results:
247
+ openfda = result.get("openfda", {})
248
+ labels.append({
249
+ "brand_name": openfda.get("brand_name", ["Unknown"])[0],
250
+ "generic_name": openfda.get("generic_name", ["Unknown"])[0],
251
+ "manufacturer": openfda.get("manufacturer_name", ["Unknown"])[0],
252
+ "purpose": result.get("purpose", ["Not specified"])[0] if result.get("purpose") else "Not specified",
253
+ "warnings": result.get("warnings", ["Not specified"])[0][:500] if result.get("warnings") else "Not specified",
254
+ "indications_and_usage": result.get("indications_and_usage", ["Not specified"])[0][:500] if result.get("indications_and_usage") else "Not specified"
255
+ })
256
+
257
+ return {"results": labels, "total": len(labels)}
258
+
259
+ # NDC Directory
260
+ @safe_json_return
261
+ async def openfda_search_ndc(self, query: str, limit: int = 10) -> Dict[str, Any]:
262
+ """
263
+ Search National Drug Code (NDC) directory for drug product information.
264
+
265
+ Args:
266
+ query: Brand name, generic name, or NDC number
267
+ limit: Maximum results (max 100)
268
+
269
+ Returns:
270
+ NDC product information
271
+ """
272
+ params = self._build_params(query, limit)
273
+ data = await self._fetch_fda_data(OPENFDA_DRUG_NDC, params)
274
+
275
+ results = data.get("results", [])
276
+ products = []
277
+
278
+ for result in results:
279
+ products.append({
280
+ "product_ndc": result.get("product_ndc", "Unknown"),
281
+ "brand_name": result.get("brand_name", "Unknown"),
282
+ "generic_name": result.get("generic_name", "Unknown"),
283
+ "manufacturer": result.get("labeler_name", "Unknown"),
284
+ "dosage_form": result.get("dosage_form", "Unknown"),
285
+ "route": result.get("route", ["Unknown"])[0] if result.get("route") else "Unknown",
286
+ "marketing_status": result.get("marketing_status", "Unknown")
287
+ })
288
+
289
+ return {"results": products, "total": len(products)}
290
+
291
+ # Drug Recalls
292
+ @safe_json_return
293
+ async def openfda_search_drug_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
294
+ """
295
+ Search FDA drug recall and enforcement reports.
296
+
297
+ Args:
298
+ query: Drug name, manufacturer, or reason for recall
299
+ limit: Maximum results (max 100)
300
+
301
+ Returns:
302
+ Drug recall information
303
+ """
304
+ params = self._build_params(query, limit)
305
+ data = await self._fetch_fda_data(OPENFDA_DRUG_ENFORCEMENT, params)
306
+
307
+ results = data.get("results", [])
308
+ recalls = []
309
+
310
+ for result in results:
311
+ recalls.append({
312
+ "product_description": result.get("product_description", "Unknown"),
313
+ "reason_for_recall": result.get("reason_for_recall", "Unknown"),
314
+ "classification": result.get("classification", "Unknown"),
315
+ "status": result.get("status", "Unknown"),
316
+ "recall_date": result.get("recall_initiation_date", "Unknown"),
317
+ "recalling_firm": result.get("recalling_firm", "Unknown")
318
+ })
319
+
320
+ return {"results": recalls, "total": len(recalls)}
321
+
322
+ # Drugs@FDA
323
+ @safe_json_return
324
+ async def openfda_search_drugs_fda(self, query: str, limit: int = 10) -> Dict[str, Any]:
325
+ """
326
+ Search Drugs@FDA database for approved drug products and applications.
327
+
328
+ Args:
329
+ query: Drug name or active ingredient
330
+ limit: Maximum results (max 100)
331
+
332
+ Returns:
333
+ Approved drug information
334
+ """
335
+ params = self._build_params(query, limit)
336
+ data = await self._fetch_fda_data(OPENFDA_DRUG_DRUGSFDA, params)
337
+
338
+ results = data.get("results", [])
339
+ drugs = []
340
+
341
+ for result in results:
342
+ products = result.get("products", [])
343
+ for product in products:
344
+ drugs.append({
345
+ "application_number": result.get("application_number", "Unknown"),
346
+ "sponsor_name": result.get("sponsor_name", "Unknown"),
347
+ "brand_name": product.get("brand_name", "Unknown"),
348
+ "active_ingredients": product.get("active_ingredients", []),
349
+ "dosage_form": product.get("dosage_form", "Unknown"),
350
+ "route": product.get("route", "Unknown"),
351
+ "marketing_status": product.get("marketing_status", "Unknown")
352
+ })
353
+
354
+ return {"results": drugs, "total": len(drugs)}
355
+
356
+ # Device Events
357
+ @safe_json_return
358
+ async def openfda_search_device_events(self, query: str, limit: int = 10) -> Dict[str, Any]:
359
+ """
360
+ Search medical device adverse event reports.
361
+
362
+ Args:
363
+ query: Device name or brand
364
+ limit: Maximum results (max 100)
365
+
366
+ Returns:
367
+ Device adverse event information
368
+ """
369
+ params = self._build_params(query, limit)
370
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_EVENT, params)
371
+
372
+ results = data.get("results", [])
373
+ events = []
374
+
375
+ for result in results:
376
+ device = result.get("device", [{}])[0]
377
+ events.append({
378
+ "report_number": result.get("report_number", "Unknown"),
379
+ "date_received": result.get("date_received", "Unknown"),
380
+ "device_name": device.get("brand_name", "Unknown"),
381
+ "manufacturer": device.get("manufacturer_d_name", "Unknown"),
382
+ "event_type": result.get("event_type", "Unknown"),
383
+ "device_problem": device.get("device_problem_codes", ["Unknown"])[0] if device.get("device_problem_codes") else "Unknown"
384
+ })
385
+
386
+ return {"results": events, "total": len(events)}
387
+
388
+ # Device Recalls
389
+ @safe_json_return
390
+ async def openfda_search_device_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
391
+ """
392
+ Search medical device recall and enforcement reports.
393
+
394
+ Args:
395
+ query: Device name, manufacturer, or reason
396
+ limit: Maximum results (max 100)
397
+
398
+ Returns:
399
+ Device recall information
400
+ """
401
+ params = self._build_params(query, limit)
402
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_ENFORCEMENT, params)
403
+
404
+ results = data.get("results", [])
405
+ recalls = []
406
+
407
+ for result in results:
408
+ recalls.append({
409
+ "product_description": result.get("product_description", "Unknown"),
410
+ "reason_for_recall": result.get("reason_for_recall", "Unknown"),
411
+ "classification": result.get("classification", "Unknown"),
412
+ "status": result.get("status", "Unknown"),
413
+ "recall_date": result.get("recall_initiation_date", "Unknown"),
414
+ "recalling_firm": result.get("recalling_firm", "Unknown")
415
+ })
416
+
417
+ return {"results": recalls, "total": len(recalls)}
418
+
419
+ # Device Classifications
420
+ @safe_json_return
421
+ async def openfda_search_device_classifications(self, query: str, limit: int = 10) -> Dict[str, Any]:
422
+ """
423
+ Search medical device classification database.
424
+
425
+ Args:
426
+ query: Device type or classification
427
+ limit: Maximum results (max 100)
428
+
429
+ Returns:
430
+ Device classification information
431
+ """
432
+ params = self._build_params(query, limit)
433
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_CLASSIFICATION, params)
434
+
435
+ results = data.get("results", [])
436
+ classifications = []
437
+
438
+ for result in results:
439
+ classifications.append({
440
+ "device_name": result.get("device_name", "Unknown"),
441
+ "device_class": result.get("device_class", "Unknown"),
442
+ "medical_specialty": result.get("medical_specialty_description", "Unknown"),
443
+ "regulation_number": result.get("regulation_number", "Unknown"),
444
+ "product_code": result.get("product_code", "Unknown")
445
+ })
446
+
447
+ return {"results": classifications, "total": len(classifications)}
448
+
449
+ # 510(k) Clearances
450
+ @safe_json_return
451
+ async def openfda_search_510k(self, query: str, limit: int = 10) -> Dict[str, Any]:
452
+ """
453
+ Search FDA 510(k) premarket clearance database.
454
+
455
+ Args:
456
+ query: Device name or manufacturer
457
+ limit: Maximum results (max 100)
458
+
459
+ Returns:
460
+ 510(k) clearance information
461
+ """
462
+ params = self._build_params(query, limit)
463
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_510K, params)
464
+
465
+ results = data.get("results", [])
466
+ clearances = []
467
+
468
+ for result in results:
469
+ clearances.append({
470
+ "k_number": result.get("k_number", "Unknown"),
471
+ "device_name": result.get("device_name", "Unknown"),
472
+ "applicant": result.get("applicant", "Unknown"),
473
+ "clearance_date": result.get("date_received", "Unknown"),
474
+ "decision_description": result.get("decision_description", "Unknown"),
475
+ "product_code": result.get("product_code", "Unknown")
476
+ })
477
+
478
+ return {"results": clearances, "total": len(clearances)}
479
+
480
+ # PMA Approvals
481
+ @safe_json_return
482
+ async def openfda_search_pma(self, query: str, limit: int = 10) -> Dict[str, Any]:
483
+ """
484
+ Search FDA premarket approval (PMA) database.
485
+
486
+ Args:
487
+ query: Device name or manufacturer
488
+ limit: Maximum results (max 100)
489
+
490
+ Returns:
491
+ PMA approval information
492
+ """
493
+ params = self._build_params(query, limit)
494
+ data = await self._fetch_fda_data(OPENFDA_DEVICE_PMA, params)
495
+
496
+ results = data.get("results", [])
497
+ approvals = []
498
+
499
+ for result in results:
500
+ approvals.append({
501
+ "pma_number": result.get("pma_number", "Unknown"),
502
+ "device_name": result.get("device_name", "Unknown"),
503
+ "applicant": result.get("applicant", "Unknown"),
504
+ "approval_date": result.get("date_received", "Unknown"),
505
+ "decision_description": result.get("decision_description", "Unknown"),
506
+ "product_code": result.get("product_code", "Unknown")
507
+ })
508
+
509
+ return {"results": approvals, "total": len(approvals)}
510
+
511
+ # Food Recalls
512
+ @safe_json_return
513
+ async def openfda_search_food_recalls(self, query: str, limit: int = 10) -> Dict[str, Any]:
514
+ """
515
+ Search FDA food recall and enforcement reports.
516
+
517
+ Args:
518
+ query: Food product or reason for recall
519
+ limit: Maximum results (max 100)
520
+
521
+ Returns:
522
+ Food recall information
523
+ """
524
+ params = self._build_params(query, limit)
525
+ data = await self._fetch_fda_data(OPENFDA_FOOD_ENFORCEMENT, params)
526
+
527
+ results = data.get("results", [])
528
+ recalls = []
529
+
530
+ for result in results:
531
+ recalls.append({
532
+ "product_description": result.get("product_description", "Unknown"),
533
+ "reason_for_recall": result.get("reason_for_recall", "Unknown"),
534
+ "classification": result.get("classification", "Unknown"),
535
+ "status": result.get("status", "Unknown"),
536
+ "recall_date": result.get("recall_initiation_date", "Unknown"),
537
+ "recalling_firm": result.get("recalling_firm", "Unknown")
538
+ })
539
+
540
+ return {"results": recalls, "total": len(recalls)}
541
+
542
+ # Food Events
543
+ @safe_json_return
544
+ async def openfda_search_food_events(self, query: str, limit: int = 10) -> Dict[str, Any]:
545
+ """
546
+ Search FDA food adverse event reports.
547
+
548
+ Args:
549
+ query: Food product or reaction
550
+ limit: Maximum results (max 100)
551
+
552
+ Returns:
553
+ Food adverse event information
554
+ """
555
+ params = self._build_params(query, limit)
556
+ data = await self._fetch_fda_data(OPENFDA_FOOD_EVENT, params)
557
+
558
+ results = data.get("results", [])
559
+ events = []
560
+
561
+ for result in results:
562
+ products = result.get("products", [{}])
563
+ reactions = result.get("reactions", [])
564
+
565
+ events.append({
566
+ "report_number": result.get("report_number", "Unknown"),
567
+ "date_started": result.get("date_started", "Unknown"),
568
+ "products": [p.get("name_brand", "Unknown") for p in products],
569
+ "reactions": [r.get("reaction", "Unknown") for r in reactions][:5],
570
+ "outcomes": result.get("outcomes", ["Unknown"])
571
+ })
572
+
573
+ return {"results": events, "total": len(events)}