Spaces:
Sleeping
Sleeping
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 filesPreviously 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 +7 -2
- tests/agents/test_research.py +35 -0
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=
|
| 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
|