KevinIsInCoding commited on
Commit
b3c8b62
Β·
unverified Β·
2 Parent(s): 95c1c84ee146e1

Merge pull request #1 from KevinIsInCoding/feat/na-phase-and-full-pagination

Browse files
Files changed (2) hide show
  1. app.py +0 -1
  2. clinical_trials_guru.py +59 -25
app.py CHANGED
@@ -107,7 +107,6 @@ def _run_research(profile: PatientProfile) -> str:
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)
113
  content = json.dumps(ranked)
 
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
  )
111
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
112
  content = json.dumps(ranked)
clinical_trials_guru.py CHANGED
@@ -61,8 +61,8 @@ SUBMIT_PROFILE_TOOL: anthropic.types.ToolParam = {
61
  },
62
  "phases": {
63
  "type": "array",
64
- "items": {"type": "string", "enum": ["0", "1", "2", "3", "4"]},
65
- "description": "Desired trial phases (0=Early Phase 1, 1=Phase 1, 2=Phase 2, 3=Phase 3, 4=Phase 4). Empty = all phases.",
66
  },
67
  "include_eap": {
68
  "type": "boolean",
@@ -95,14 +95,19 @@ SEARCH_TRIALS_TOOL: anthropic.types.ToolParam = {
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
  },
107
  "required": ["condition", "lat", "lon", "radius_miles"],
108
  },
@@ -151,6 +156,9 @@ OPTIONAL (ask based on disease):
151
  (1,000–3,000 people); required for regulatory approval; best efficacy evidence.
152
  Phase 4 β€” Post-approval surveillance; treatment is already FDA-approved;
153
  studies long-term safety, rare side effects, and new uses.
 
 
 
154
  - Expanded Access Programs (EAP / compassionate use) β€” a pathway for patients who
155
  do not qualify for or cannot access a clinical trial to receive an investigational
156
  drug, biologic, or device outside of a trial. Also called "compassionate use."
@@ -172,10 +180,14 @@ Workflow:
172
  - If the patient wants clinical trials, search with study_type="INTERVENTIONAL".
173
  - If the patient wants Expanded Access Programs (EAP), also search with study_type="EXPANDED_ACCESS".
174
  - If the patient wants both, run separate searches for each study_type.
175
- 2. If fewer than 3 results are found, retry with: a wider radius, a disease synonym,
176
- or fewer phase filters.
177
- 3. Produce a final report. Use separate sections for Clinical Trials and Expanded Access if both apply.
178
- List the top 5 results per section ranked by site proximity.
 
 
 
 
179
  For EACH entry use exactly this format (repeat the block per entry):
180
 
181
  πŸ“ **[Closest hospital/facility name]** β€” [City, State] ([X] mi)
@@ -192,7 +204,7 @@ Workflow:
192
 
193
  ---
194
 
195
- 4. After the results add a short "Next steps" section (bullet points).
196
  For EAP results, note that patients typically need a physician to submit the EAP request.
197
 
198
  IMPORTANT: Only report trials returned by the search_clinical_trials tool. Do NOT suggest,
@@ -235,7 +247,13 @@ class PatientProfile:
235
  )
236
  lines.append(f"Search radius: {self.radius_miles} miles")
237
  if self.phases:
238
- labels = ["Early Phase 1" if p == "0" else f"Phase {p}" for p in self.phases]
 
 
 
 
 
 
239
  lines.append(f"Phases: {', '.join(labels)}")
240
  interests = ["Clinical trials"]
241
  if self.include_eap:
@@ -279,36 +297,52 @@ def search_trials_api(
279
  radius_miles: int = 100,
280
  phases: list[str] | None = None,
281
  study_type: str = "INTERVENTIONAL",
282
- max_results: int = 20,
283
  ) -> list[dict]:
284
  is_eap = study_type == "EXPANDED_ACCESS"
285
  params: dict[str, str | int] = {
286
  "query.cond": condition,
287
  "filter.overallStatus": "AVAILABLE" if is_eap else "RECRUITING",
288
  "filter.geo": f"distance({lat},{lon},{radius_miles}mi)",
289
- "pageSize": max_results,
290
  "format": "json",
291
  }
292
  # aggFilters accepts only one value; studyType and phase can't be combined.
293
  # RECRUITING status already excludes EAPs, so studyType:int is only needed
294
- # when no phase filter is applied.
295
  if is_eap:
296
  params["aggFilters"] = "studyType:exp"
297
  elif phases:
298
- params["aggFilters"] = "phase:" + " ".join(phases)
 
 
 
 
 
 
299
  else:
300
  params["aggFilters"] = "studyType:int"
301
- for attempt in range(3):
302
- try:
303
- resp = httpx.get(CTGOV_BASE, params=params, timeout=30)
304
- resp.raise_for_status()
305
- return resp.json().get("studies", [])
306
- except httpx.HTTPError as exc:
307
- if attempt == 2:
308
- raise
309
- wait = 2 ** attempt
310
- console.print(f"[yellow]API warning:[/yellow] {exc} β€” retrying in {wait}s (attempt {attempt + 1}/3)…")
311
- time.sleep(wait)
 
 
 
 
 
 
 
 
 
 
 
312
 
313
 
314
  def _flatten_and_rank(studies: list[dict], patient_lat: float, patient_lon: float) -> list[dict]:
 
61
  },
62
  "phases": {
63
  "type": "array",
64
+ "items": {"type": "string", "enum": ["0", "1", "2", "3", "4", "na"]},
65
+ "description": "Desired trial phases (0=Early Phase 1, 1=Phase 1, 2=Phase 2, 3=Phase 3, 4=Phase 4, na=Not Applicable). Empty = all phases.",
66
  },
67
  "include_eap": {
68
  "type": "boolean",
 
95
  "phases": {
96
  "type": "array",
97
  "items": {"type": "string"},
98
+ "description": (
99
+ "Phase numbers to filter e.g. ['1','2','3']. "
100
+ "IMPORTANT: Never enumerate all phases to mean 'all phases' β€” "
101
+ "pass an empty array [] instead. NA-phase trials (device feasibility, "
102
+ "unphased studies) only appear when phases=[] (no filter). "
103
+ "Ignored for EAP."
104
+ ),
105
  },
106
  "study_type": {
107
  "type": "string",
108
  "enum": ["INTERVENTIONAL", "EXPANDED_ACCESS"],
109
  "description": "INTERVENTIONAL (default) for clinical trials; EXPANDED_ACCESS for EAP/compassionate use.",
110
  },
 
111
  },
112
  "required": ["condition", "lat", "lon", "radius_miles"],
113
  },
 
156
  (1,000–3,000 people); required for regulatory approval; best efficacy evidence.
157
  Phase 4 β€” Post-approval surveillance; treatment is already FDA-approved;
158
  studies long-term safety, rare side effects, and new uses.
159
+ Not Applicable β€” Studies that do not fall into the standard phase framework
160
+ (e.g., device feasibility studies, behavioral/observational trials,
161
+ or studies where phase designation is not required by FDA).
162
  - Expanded Access Programs (EAP / compassionate use) β€” a pathway for patients who
163
  do not qualify for or cannot access a clinical trial to receive an investigational
164
  drug, biologic, or device outside of a trial. Also called "compassionate use."
 
180
  - If the patient wants clinical trials, search with study_type="INTERVENTIONAL".
181
  - If the patient wants Expanded Access Programs (EAP), also search with study_type="EXPANDED_ACCESS".
182
  - If the patient wants both, run separate searches for each study_type.
183
+ 2. IMPORTANT β€” phase filtering: Never pass phases=["1","2","3","4"] to mean "all phases."
184
+ Always pass phases=[] (omit the field) when the patient has no phase preference.
185
+ NA-phase trials (device feasibility studies, unphased interventions) only appear
186
+ when no phase filter is applied. Passing explicit phase numbers silently excludes them.
187
+ 3. If fewer than 3 results are found, retry with: a wider radius, a disease synonym,
188
+ or drop phase filters entirely (phases=[]).
189
+ 4. Produce a final report. Use separate sections for Clinical Trials and Expanded Access if both apply.
190
+ List the top 10 results per section ranked by site proximity.
191
  For EACH entry use exactly this format (repeat the block per entry):
192
 
193
  πŸ“ **[Closest hospital/facility name]** β€” [City, State] ([X] mi)
 
204
 
205
  ---
206
 
207
+ 5. After the results add a short "Next steps" section (bullet points).
208
  For EAP results, note that patients typically need a physician to submit the EAP request.
209
 
210
  IMPORTANT: Only report trials returned by the search_clinical_trials tool. Do NOT suggest,
 
247
  )
248
  lines.append(f"Search radius: {self.radius_miles} miles")
249
  if self.phases:
250
+ def _phase_label(p: str) -> str:
251
+ if p == "0":
252
+ return "Early Phase 1"
253
+ if p == "na":
254
+ return "Not Applicable"
255
+ return f"Phase {p}"
256
+ labels = [_phase_label(p) for p in self.phases]
257
  lines.append(f"Phases: {', '.join(labels)}")
258
  interests = ["Clinical trials"]
259
  if self.include_eap:
 
297
  radius_miles: int = 100,
298
  phases: list[str] | None = None,
299
  study_type: str = "INTERVENTIONAL",
 
300
  ) -> list[dict]:
301
  is_eap = study_type == "EXPANDED_ACCESS"
302
  params: dict[str, str | int] = {
303
  "query.cond": condition,
304
  "filter.overallStatus": "AVAILABLE" if is_eap else "RECRUITING",
305
  "filter.geo": f"distance({lat},{lon},{radius_miles}mi)",
306
+ "pageSize": 200, # max page size; we paginate until exhausted
307
  "format": "json",
308
  }
309
  # aggFilters accepts only one value; studyType and phase can't be combined.
310
  # RECRUITING status already excludes EAPs, so studyType:int is only needed
311
+ # when no phase filter is applied. studyType:int returns all phases including N/A.
312
  if is_eap:
313
  params["aggFilters"] = "studyType:exp"
314
  elif phases:
315
+ # Exclude "na" from the phase filter β€” N/A trials have no phase value to match on;
316
+ # they appear naturally when no phase filter is applied (studyType:int branch).
317
+ numbered = [p for p in phases if p != "na"]
318
+ if numbered:
319
+ params["aggFilters"] = "phase:" + " ".join(numbered)
320
+ else:
321
+ params["aggFilters"] = "studyType:int"
322
  else:
323
  params["aggFilters"] = "studyType:int"
324
+
325
+ all_studies: list[dict] = []
326
+ while True:
327
+ for attempt in range(3):
328
+ try:
329
+ resp = httpx.get(CTGOV_BASE, params=params, timeout=30)
330
+ resp.raise_for_status()
331
+ body = resp.json()
332
+ break
333
+ except httpx.HTTPError as exc:
334
+ if attempt == 2:
335
+ raise
336
+ wait = 2 ** attempt
337
+ console.print(f"[yellow]API warning:[/yellow] {exc} β€” retrying in {wait}s (attempt {attempt + 1}/3)…")
338
+ time.sleep(wait)
339
+ all_studies.extend(body.get("studies", []))
340
+ next_token = body.get("nextPageToken")
341
+ if not next_token:
342
+ break
343
+ params["pageToken"] = next_token
344
+
345
+ return all_studies
346
 
347
 
348
  def _flatten_and_rank(studies: list[dict], patient_lat: float, patient_lon: float) -> list[dict]: