KevinIsInCoding Claude Sonnet 4.6 commited on
Commit
57735b3
Β·
1 Parent(s): d2a29fe

Add Expanded Access Program (EAP) as a search option alongside clinical trials

Browse files

Doctors noted patients may be interested in compassionate use / EAP when
they don't qualify for a trial. The intake agent now asks whether the
patient wants clinical trials, EAP, or both. The research agent can call
search_clinical_trials with study_type=EXPANDED_ACCESS, which queries
ClinicalTrials.gov with the correct AVAILABLE status and EXPANDED_ACCESS
study type filter. Results appear in a dedicated EAP section of the report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +2 -0
  2. clinical_trials_guru.py +42 -11
app.py CHANGED
@@ -62,6 +62,7 @@ def _intake_turn(
62
  lon=lon,
63
  radius_miles=data.get("radius_miles", 100),
64
  phases=data.get("phases") or [],
 
65
  )
66
  return text or "Got it β€” searching for trials now…", messages, profile
67
 
@@ -105,6 +106,7 @@ def _run_research(profile: PatientProfile) -> str:
105
  lon=args["lon"],
106
  radius_miles=args.get("radius_miles", profile.radius_miles),
107
  phases=args.get("phases") or None,
 
108
  max_results=args.get("max_results", 20),
109
  )
110
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
 
62
  lon=lon,
63
  radius_miles=data.get("radius_miles", 100),
64
  phases=data.get("phases") or [],
65
+ include_eap=data.get("include_eap", False),
66
  )
67
  return text or "Got it β€” searching for trials now…", messages, profile
68
 
 
106
  lon=args["lon"],
107
  radius_miles=args.get("radius_miles", profile.radius_miles),
108
  phases=args.get("phases") or None,
109
+ study_type=args.get("study_type", "INTERVENTIONAL"),
110
  max_results=args.get("max_results", 20),
111
  )
112
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
clinical_trials_guru.py CHANGED
@@ -64,6 +64,10 @@ SUBMIT_PROFILE_TOOL: anthropic.types.ToolParam = {
64
  "items": {"type": "string", "enum": ["0", "1", "2", "3", "4"]},
65
  "description": "Desired trial phases. Empty = all phases.",
66
  },
 
 
 
 
67
  },
68
  "required": ["disease", "age", "onset_months", "diagnosis_months", "zip_code"],
69
  },
@@ -72,10 +76,11 @@ SUBMIT_PROFILE_TOOL: anthropic.types.ToolParam = {
72
  SEARCH_TRIALS_TOOL: anthropic.types.ToolParam = {
73
  "name": "search_clinical_trials",
74
  "description": (
75
- "Search ClinicalTrials.gov for recruiting trials within a geographic radius. "
76
  "Results are pre-ranked by distance from the patient's location. "
77
  "Call multiple times with different parameters (synonyms, broader radius, "
78
- "different phases) if initial results are sparse."
 
79
  ),
80
  "input_schema": {
81
  "type": "object",
@@ -90,7 +95,12 @@ SEARCH_TRIALS_TOOL: anthropic.types.ToolParam = {
90
  "phases": {
91
  "type": "array",
92
  "items": {"type": "string"},
93
- "description": "Phase numbers to filter ['1','2','3']. Empty = all.",
 
 
 
 
 
94
  },
95
  "max_results": {"type": "integer", "description": "Max trials to return (default 20)"},
96
  },
@@ -117,7 +127,11 @@ OPTIONAL (ask based on disease):
117
  Huntington's β†’ TFC (0-13) + CAG repeats; SMA β†’ HFMS + SMA type;
118
  Duchenne/Pompe β†’ 6-Minute Walk Test; Friedreich's β†’ SARA score
119
  β€’ Preferred search radius in miles (default 100)
120
- β€’ Trial phases of interest (1 / 2 / 3 / 4 / early)
 
 
 
 
121
 
122
  Ask naturally. Infer what you can. Once you have the required fields, call submit_profile.\
123
  """
@@ -129,15 +143,19 @@ Results are already ranked by geographic distance from the patient.
129
 
130
  Workflow:
131
  1. Search for the patient's disease. Use both the full medical name and common abbreviation.
 
 
 
132
  2. If fewer than 3 results are found, retry with: a wider radius, a disease synonym,
133
  or fewer phase filters.
134
- 3. Produce a final report listing the top 5 trials ranked by site proximity.
135
- For EACH trial use exactly this format (repeat the block per trial):
 
136
 
137
  πŸ“ **[Closest hospital name]** β€” [City, State] ([X] mi)
138
- **Trial:** [Full trial title] ([Phase])
139
  **Sponsor:** [Lead sponsor]
140
- **Summary:** [2–3 sentence plain-language description of what the trial is testing
141
  and why it may matter for this patient]
142
  **Eligibility notes:** [Key inclusion/exclusion criteria relevant to this patient,
143
  including any red flags]
@@ -145,7 +163,8 @@ Workflow:
145
 
146
  ---
147
 
148
- 4. After the trial list add a short "Next steps" section (bullet points).
 
149
 
150
  Be accurate. Do not fabricate details. If data is missing, say so.\
151
  """
@@ -165,6 +184,7 @@ class PatientProfile:
165
  lon: float = 0.0
166
  radius_miles: int = 100
167
  phases: list[str] = field(default_factory=list)
 
168
 
169
  def summary(self) -> str:
170
  lines = [
@@ -183,6 +203,10 @@ class PatientProfile:
183
  if self.phases:
184
  labels = ["Early Phase 1" if p == "0" else f"Phase {p}" for p in self.phases]
185
  lines.append(f"Phases: {', '.join(labels)}")
 
 
 
 
186
  return "\n".join(lines)
187
 
188
 
@@ -220,16 +244,19 @@ def search_trials_api(
220
  lon: float,
221
  radius_miles: int = 100,
222
  phases: list[str] | None = None,
 
223
  max_results: int = 20,
224
  ) -> list[dict]:
 
225
  params: dict[str, str | int] = {
226
  "query.cond": condition,
227
- "filter.overallStatus": "RECRUITING",
 
228
  "filter.geo": f"distance({lat},{lon},{radius_miles}mi)",
229
  "pageSize": max_results,
230
  "format": "json",
231
  }
232
- if phases:
233
  params["aggFilters"] = "phase:" + " ".join(phases)
234
  for attempt in range(3):
235
  try:
@@ -341,6 +368,7 @@ def run_intake_agent(client: anthropic.Anthropic) -> PatientProfile:
341
  lon=lon,
342
  radius_miles=data.get("radius_miles", 100),
343
  phases=data.get("phases") or [],
 
344
  )
345
 
346
  messages.append({"role": "assistant", "content": response.content})
@@ -385,9 +413,11 @@ def run_research_agent(client: anthropic.Anthropic, profile: PatientProfile) ->
385
  args = block.input
386
  radius = args.get("radius_miles", profile.radius_miles)
387
  phases = args.get("phases") or None
 
388
  status_msg = (
389
  f"[cyan]Searching:[/cyan] '[bold]{args['condition']}[/bold]' | "
390
  f"radius=[bold]{radius}[/bold] mi | "
 
391
  f"phases=[bold]{phases or 'all'}[/bold]"
392
  )
393
  try:
@@ -398,6 +428,7 @@ def run_research_agent(client: anthropic.Anthropic, profile: PatientProfile) ->
398
  lon=args["lon"],
399
  radius_miles=radius,
400
  phases=phases,
 
401
  max_results=args.get("max_results", 20),
402
  )
403
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
 
64
  "items": {"type": "string", "enum": ["0", "1", "2", "3", "4"]},
65
  "description": "Desired trial phases. Empty = all phases.",
66
  },
67
+ "include_eap": {
68
+ "type": "boolean",
69
+ "description": "Whether patient is interested in Expanded Access Programs (compassionate use)",
70
+ },
71
  },
72
  "required": ["disease", "age", "onset_months", "diagnosis_months", "zip_code"],
73
  },
 
76
  SEARCH_TRIALS_TOOL: anthropic.types.ToolParam = {
77
  "name": "search_clinical_trials",
78
  "description": (
79
+ "Search ClinicalTrials.gov for studies within a geographic radius. "
80
  "Results are pre-ranked by distance from the patient's location. "
81
  "Call multiple times with different parameters (synonyms, broader radius, "
82
+ "different phases) if initial results are sparse. "
83
+ "Use study_type='EXPANDED_ACCESS' to search for Expanded Access Programs (EAP / compassionate use)."
84
  ),
85
  "input_schema": {
86
  "type": "object",
 
95
  "phases": {
96
  "type": "array",
97
  "items": {"type": "string"},
98
+ "description": "Phase numbers to filter ['1','2','3']. Empty = all. Ignored for EAP.",
99
+ },
100
+ "study_type": {
101
+ "type": "string",
102
+ "enum": ["INTERVENTIONAL", "EXPANDED_ACCESS"],
103
+ "description": "INTERVENTIONAL (default) for clinical trials; EXPANDED_ACCESS for EAP/compassionate use.",
104
  },
105
  "max_results": {"type": "integer", "description": "Max trials to return (default 20)"},
106
  },
 
127
  Huntington's β†’ TFC (0-13) + CAG repeats; SMA β†’ HFMS + SMA type;
128
  Duchenne/Pompe β†’ 6-Minute Walk Test; Friedreich's β†’ SARA score
129
  β€’ Preferred search radius in miles (default 100)
130
+ β€’ Study types of interest β€” ask whether the patient wants:
131
+ - Clinical trials (phases 1 / 2 / 3 / 4 / early phase 1)
132
+ - Expanded Access Programs (EAP / compassionate use) β€” for patients who may not
133
+ qualify for a trial but want access to an investigational treatment
134
+ - Or both
135
 
136
  Ask naturally. Infer what you can. Once you have the required fields, call submit_profile.\
137
  """
 
143
 
144
  Workflow:
145
  1. Search for the patient's disease. Use both the full medical name and common abbreviation.
146
+ - If the patient wants clinical trials, search with study_type="INTERVENTIONAL".
147
+ - If the patient wants Expanded Access Programs (EAP), also search with study_type="EXPANDED_ACCESS".
148
+ - If the patient wants both, run separate searches for each study_type.
149
  2. If fewer than 3 results are found, retry with: a wider radius, a disease synonym,
150
  or fewer phase filters.
151
+ 3. Produce a final report. Use separate sections for Clinical Trials and Expanded Access if both apply.
152
+ List the top 5 results per section ranked by site proximity.
153
+ For EACH entry use exactly this format (repeat the block per entry):
154
 
155
  πŸ“ **[Closest hospital name]** β€” [City, State] ([X] mi)
156
+ **Trial:** [Full title] ([Phase] β€” or "Expanded Access" for EAP)
157
  **Sponsor:** [Lead sponsor]
158
+ **Summary:** [2–3 sentence plain-language description of what the trial/program is testing
159
  and why it may matter for this patient]
160
  **Eligibility notes:** [Key inclusion/exclusion criteria relevant to this patient,
161
  including any red flags]
 
163
 
164
  ---
165
 
166
+ 4. After the results add a short "Next steps" section (bullet points).
167
+ For EAP results, note that patients typically need a physician to submit the EAP request.
168
 
169
  Be accurate. Do not fabricate details. If data is missing, say so.\
170
  """
 
184
  lon: float = 0.0
185
  radius_miles: int = 100
186
  phases: list[str] = field(default_factory=list)
187
+ include_eap: bool = False
188
 
189
  def summary(self) -> str:
190
  lines = [
 
203
  if self.phases:
204
  labels = ["Early Phase 1" if p == "0" else f"Phase {p}" for p in self.phases]
205
  lines.append(f"Phases: {', '.join(labels)}")
206
+ interests = ["Clinical trials"]
207
+ if self.include_eap:
208
+ interests.append("Expanded Access Programs (EAP)")
209
+ lines.append(f"Study type interest: {', '.join(interests)}")
210
  return "\n".join(lines)
211
 
212
 
 
244
  lon: float,
245
  radius_miles: int = 100,
246
  phases: list[str] | None = None,
247
+ study_type: str = "INTERVENTIONAL",
248
  max_results: int = 20,
249
  ) -> list[dict]:
250
+ is_eap = study_type == "EXPANDED_ACCESS"
251
  params: dict[str, str | int] = {
252
  "query.cond": condition,
253
+ "filter.overallStatus": "AVAILABLE" if is_eap else "RECRUITING",
254
+ "filter.studyType": study_type,
255
  "filter.geo": f"distance({lat},{lon},{radius_miles}mi)",
256
  "pageSize": max_results,
257
  "format": "json",
258
  }
259
+ if phases and not is_eap:
260
  params["aggFilters"] = "phase:" + " ".join(phases)
261
  for attempt in range(3):
262
  try:
 
368
  lon=lon,
369
  radius_miles=data.get("radius_miles", 100),
370
  phases=data.get("phases") or [],
371
+ include_eap=data.get("include_eap", False),
372
  )
373
 
374
  messages.append({"role": "assistant", "content": response.content})
 
413
  args = block.input
414
  radius = args.get("radius_miles", profile.radius_miles)
415
  phases = args.get("phases") or None
416
+ study_type = args.get("study_type", "INTERVENTIONAL")
417
  status_msg = (
418
  f"[cyan]Searching:[/cyan] '[bold]{args['condition']}[/bold]' | "
419
  f"radius=[bold]{radius}[/bold] mi | "
420
+ f"type=[bold]{study_type}[/bold] | "
421
  f"phases=[bold]{phases or 'all'}[/bold]"
422
  )
423
  try:
 
428
  lon=args["lon"],
429
  radius_miles=radius,
430
  phases=phases,
431
+ study_type=study_type,
432
  max_results=args.get("max_results", 20),
433
  )
434
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)