KevinIsInCoding Claude Sonnet 4.6 commited on
Commit
276f805
·
1 Parent(s): 092de1d

fix: enforce patient's phase preference in API call, not LLM guidance

Browse files

Previously the research LLM was expected to forward profile.phases to
the search tool — an unreliable pattern. Now the code sets phases
directly from profile.phases for INTERVENTIONAL searches, ignoring
whatever the LLM passed. EAP and OBSERVATIONAL searches are unaffected
(phase filters don't apply there).

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

agents/research.py CHANGED
@@ -108,8 +108,10 @@ def run_research_agent(client: anthropic.Anthropic, profile: PatientProfile) ->
108
  continue
109
  args = block.input
110
  radius = args.get("radius_miles", profile.radius_miles)
111
- phases = args.get("phases") or None
112
  study_type = args.get("study_type", "INTERVENTIONAL")
 
 
 
113
  status_msg = (
114
  f"[cyan]Searching:[/cyan] '[bold]{args['condition']}[/bold]' | "
115
  f"radius=[bold]{radius}[/bold] mi | "
@@ -191,6 +193,9 @@ def stream_research_agent(
191
  args = block.input
192
  radius = args.get("radius_miles", profile.radius_miles)
193
  study_type = args.get("study_type", "INTERVENTIONAL")
 
 
 
194
  type_label = {"INTERVENTIONAL": "clinical trials", "EXPANDED_ACCESS": "expanded access programs", "OBSERVATIONAL": "observational studies"}.get(study_type, study_type.lower())
195
  yield ("status", f"Searching ClinicalTrials.gov for **{args['condition']}** ({type_label}, {radius} mi radius)…")
196
  try:
@@ -199,7 +204,7 @@ def stream_research_agent(
199
  lat=args["lat"],
200
  lon=args["lon"],
201
  radius_miles=radius,
202
- phases=args.get("phases") or None,
203
  study_type=study_type,
204
  )
205
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
 
108
  continue
109
  args = block.input
110
  radius = args.get("radius_miles", profile.radius_miles)
 
111
  study_type = args.get("study_type", "INTERVENTIONAL")
112
+ # Enforce patient's phase preference; don't rely on LLM to repeat it.
113
+ # Phase filters only apply to INTERVENTIONAL searches.
114
+ phases = (profile.phases or None) if study_type == "INTERVENTIONAL" else None
115
  status_msg = (
116
  f"[cyan]Searching:[/cyan] '[bold]{args['condition']}[/bold]' | "
117
  f"radius=[bold]{radius}[/bold] mi | "
 
193
  args = block.input
194
  radius = args.get("radius_miles", profile.radius_miles)
195
  study_type = args.get("study_type", "INTERVENTIONAL")
196
+ # Enforce patient's phase preference; don't rely on LLM to repeat it.
197
+ # Phase filters only apply to INTERVENTIONAL searches.
198
+ phases = (profile.phases or None) if study_type == "INTERVENTIONAL" else None
199
  type_label = {"INTERVENTIONAL": "clinical trials", "EXPANDED_ACCESS": "expanded access programs", "OBSERVATIONAL": "observational studies"}.get(study_type, study_type.lower())
200
  yield ("status", f"Searching ClinicalTrials.gov for **{args['condition']}** ({type_label}, {radius} mi radius)…")
201
  try:
 
204
  lat=args["lat"],
205
  lon=args["lon"],
206
  radius_miles=radius,
207
+ phases=phases,
208
  study_type=study_type,
209
  )
210
  ranked = _flatten_and_rank(studies, profile.lat, profile.lon)
tests/agents/test_research.py CHANGED
@@ -150,6 +150,41 @@ class TestRunResearchAgent:
150
 
151
  assert result == "Done."
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  def test_search_results_serialized_into_tool_result(self, mock_client, als_patient):
154
  """Verify flatten output flows through bulk_parse and into the tool-result message."""
155
  import json as _json
 
150
 
151
  assert result == "Done."
152
 
153
+ def test_profile_phases_enforced_in_interventional_search(self, mock_client, als_patient):
154
+ """Patient's phase preference is passed to search_trials_api, not whatever LLM chose."""
155
+ patient_with_phases = als_patient.__class__(
156
+ **{**als_patient.__dict__, "phases": ["3", "4"]}
157
+ )
158
+ # LLM passes no phases in its tool call args
159
+ search_msg = make_message(content=[_search_block()], stop_reason="tool_use")
160
+ final_msg = make_message(content=[make_text_block("Done.")], stop_reason="end_turn")
161
+ mock_client.messages.create.side_effect = [search_msg, final_msg]
162
+
163
+ with patch(PATCH_SEARCH, return_value=[]) as ms, \
164
+ patch(PATCH_FLATTEN, return_value=[]), \
165
+ patch(PATCH_BULK, return_value=[]):
166
+ run_research_agent(mock_client, patient_with_phases)
167
+
168
+ _, kwargs = ms.call_args
169
+ assert kwargs["phases"] == ["3", "4"]
170
+
171
+ def test_no_phase_filter_when_profile_phases_empty(self, mock_client, als_patient):
172
+ """No phase filter applied when patient has no phase preference."""
173
+ patient_no_phases = als_patient.__class__(
174
+ **{**als_patient.__dict__, "phases": []}
175
+ )
176
+ search_msg = make_message(content=[_search_block()], stop_reason="tool_use")
177
+ final_msg = make_message(content=[make_text_block("Done.")], stop_reason="end_turn")
178
+ mock_client.messages.create.side_effect = [search_msg, final_msg]
179
+
180
+ with patch(PATCH_SEARCH, return_value=[]) as ms, \
181
+ patch(PATCH_FLATTEN, return_value=[]), \
182
+ patch(PATCH_BULK, return_value=[]):
183
+ run_research_agent(mock_client, patient_no_phases)
184
+
185
+ _, kwargs = ms.call_args
186
+ assert kwargs["phases"] is None
187
+
188
  def test_search_results_serialized_into_tool_result(self, mock_client, als_patient):
189
  """Verify flatten output flows through bulk_parse and into the tool-result message."""
190
  import json as _json