feat: configure Parlant guidelines
Browse filesAdd 11 guidelines across 5 journey phases plus 3 global guidelines.
Each has condition/action text and tool associations. Includes INGEST
extraction, PRESCREEN search/refine/relax, VALIDATE_TRIALS evaluation,
GAP_FOLLOWUP analysis, SUMMARY generation, and medical disclaimer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- trialpath/agent/guidelines.py +138 -0
- trialpath/tests/test_guidelines.py +92 -0
trialpath/agent/guidelines.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parlant guideline configuration for the TrialPath agent."""
|
| 2 |
+
from parlant.sdk import Agent, Guideline
|
| 3 |
+
|
| 4 |
+
from trialpath.agent.tools import (
|
| 5 |
+
analyze_gaps,
|
| 6 |
+
evaluate_trial_eligibility,
|
| 7 |
+
extract_patient_profile,
|
| 8 |
+
generate_search_anchors,
|
| 9 |
+
refine_search_query,
|
| 10 |
+
relax_search_query,
|
| 11 |
+
search_clinical_trials,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
GUIDELINE_SPECS: list[dict] = [
|
| 15 |
+
# INGEST guidelines
|
| 16 |
+
{
|
| 17 |
+
"condition": "the patient uploads medical documents",
|
| 18 |
+
"action": (
|
| 19 |
+
"Extract a structured patient profile from the uploaded documents "
|
| 20 |
+
"using the extract_patient_profile tool"
|
| 21 |
+
),
|
| 22 |
+
"tools": [extract_patient_profile],
|
| 23 |
+
"phase": "INGEST",
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"condition": "the extracted patient profile is missing critical data like stage or ECOG",
|
| 27 |
+
"action": (
|
| 28 |
+
"Ask the patient to provide the missing critical information "
|
| 29 |
+
"or upload additional documents"
|
| 30 |
+
),
|
| 31 |
+
"tools": [],
|
| 32 |
+
"phase": "INGEST",
|
| 33 |
+
},
|
| 34 |
+
# PRESCREEN guidelines
|
| 35 |
+
{
|
| 36 |
+
"condition": "the patient profile is confirmed and complete",
|
| 37 |
+
"action": (
|
| 38 |
+
"Generate search anchors from the patient profile and search "
|
| 39 |
+
"for matching clinical trials using generate_search_anchors "
|
| 40 |
+
"then search_clinical_trials"
|
| 41 |
+
),
|
| 42 |
+
"tools": [generate_search_anchors, search_clinical_trials],
|
| 43 |
+
"phase": "PRESCREEN",
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"condition": "the trial search returns more than 50 results",
|
| 47 |
+
"action": (
|
| 48 |
+
"Refine the search query to reduce the result set "
|
| 49 |
+
"using the refine_search_query tool"
|
| 50 |
+
),
|
| 51 |
+
"tools": [refine_search_query],
|
| 52 |
+
"phase": "PRESCREEN",
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"condition": "the trial search returns 0 results",
|
| 56 |
+
"action": (
|
| 57 |
+
"Relax the search query to broaden the result set "
|
| 58 |
+
"using the relax_search_query tool"
|
| 59 |
+
),
|
| 60 |
+
"tools": [relax_search_query],
|
| 61 |
+
"phase": "PRESCREEN",
|
| 62 |
+
},
|
| 63 |
+
# VALIDATE_TRIALS guideline
|
| 64 |
+
{
|
| 65 |
+
"condition": "there are trial candidates to evaluate",
|
| 66 |
+
"action": (
|
| 67 |
+
"Evaluate each trial candidate's eligibility using the "
|
| 68 |
+
"evaluate_trial_eligibility tool with dual-model approach"
|
| 69 |
+
),
|
| 70 |
+
"tools": [evaluate_trial_eligibility],
|
| 71 |
+
"phase": "VALIDATE_TRIALS",
|
| 72 |
+
},
|
| 73 |
+
# GAP_FOLLOWUP guideline
|
| 74 |
+
{
|
| 75 |
+
"condition": "eligibility evaluation reveals unknown criteria or gaps",
|
| 76 |
+
"action": (
|
| 77 |
+
"Analyze gaps across all evaluated trials and present actionable "
|
| 78 |
+
"next steps using the analyze_gaps tool"
|
| 79 |
+
),
|
| 80 |
+
"tools": [analyze_gaps],
|
| 81 |
+
"phase": "GAP_FOLLOWUP",
|
| 82 |
+
},
|
| 83 |
+
# SUMMARY guideline
|
| 84 |
+
{
|
| 85 |
+
"condition": "all trials have been evaluated and gaps analyzed",
|
| 86 |
+
"action": (
|
| 87 |
+
"Generate a comprehensive summary report with eligible, uncertain, "
|
| 88 |
+
"and ineligible trial counts plus a doctor packet for export"
|
| 89 |
+
),
|
| 90 |
+
"tools": [],
|
| 91 |
+
"phase": "SUMMARY",
|
| 92 |
+
},
|
| 93 |
+
# Global guidelines
|
| 94 |
+
{
|
| 95 |
+
"condition": "the patient asks about a specific NCT trial by ID",
|
| 96 |
+
"action": (
|
| 97 |
+
"Look up the specific trial using the search_clinical_trials tool "
|
| 98 |
+
"with the provided NCT ID"
|
| 99 |
+
),
|
| 100 |
+
"tools": [search_clinical_trials],
|
| 101 |
+
"phase": "GLOBAL",
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"condition": "the patient seems confused or asks for help",
|
| 105 |
+
"action": (
|
| 106 |
+
"Explain the current step in the journey, what data is needed, "
|
| 107 |
+
"and what will happen next in simple, empathetic language"
|
| 108 |
+
),
|
| 109 |
+
"tools": [],
|
| 110 |
+
"phase": "GLOBAL",
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"condition": "the conversation involves medical information or clinical decisions",
|
| 114 |
+
"action": (
|
| 115 |
+
"Include a disclaimer that this tool is for informational purposes only "
|
| 116 |
+
"and does not constitute medical advice. Recommend consulting with "
|
| 117 |
+
"their healthcare provider"
|
| 118 |
+
),
|
| 119 |
+
"tools": [],
|
| 120 |
+
"phase": "GLOBAL",
|
| 121 |
+
},
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def configure_guidelines(agent: Agent) -> list[Guideline]:
|
| 126 |
+
"""Configure all guidelines on the given agent.
|
| 127 |
+
|
| 128 |
+
Returns the list of created Guideline objects.
|
| 129 |
+
"""
|
| 130 |
+
guidelines = []
|
| 131 |
+
for spec in GUIDELINE_SPECS:
|
| 132 |
+
guideline = agent.create_guideline(
|
| 133 |
+
condition=spec["condition"],
|
| 134 |
+
action=spec["action"],
|
| 135 |
+
tools=spec["tools"],
|
| 136 |
+
)
|
| 137 |
+
guidelines.append(guideline)
|
| 138 |
+
return guidelines
|
trialpath/tests/test_guidelines.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TDD tests for Parlant guideline configuration."""
|
| 2 |
+
from unittest.mock import MagicMock, call
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
from trialpath.agent.guidelines import GUIDELINE_SPECS, configure_guidelines
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class TestGuidelineConfiguration:
|
| 10 |
+
"""Test guideline setup."""
|
| 11 |
+
|
| 12 |
+
@pytest.fixture
|
| 13 |
+
def mock_agent(self):
|
| 14 |
+
agent = MagicMock()
|
| 15 |
+
agent.create_guideline.return_value = MagicMock()
|
| 16 |
+
return agent
|
| 17 |
+
|
| 18 |
+
def test_correct_number_of_guidelines(self, mock_agent):
|
| 19 |
+
"""Should create exactly 11 guidelines."""
|
| 20 |
+
guidelines = configure_guidelines(mock_agent)
|
| 21 |
+
assert len(guidelines) == 11
|
| 22 |
+
assert mock_agent.create_guideline.call_count == 11
|
| 23 |
+
|
| 24 |
+
def test_each_guideline_has_condition_and_action(self):
|
| 25 |
+
"""Each spec should have condition and action strings."""
|
| 26 |
+
for spec in GUIDELINE_SPECS:
|
| 27 |
+
assert "condition" in spec
|
| 28 |
+
assert "action" in spec
|
| 29 |
+
assert isinstance(spec["condition"], str)
|
| 30 |
+
assert isinstance(spec["action"], str)
|
| 31 |
+
assert len(spec["condition"]) > 10
|
| 32 |
+
assert len(spec["action"]) > 10
|
| 33 |
+
|
| 34 |
+
def test_ingest_guidelines_count(self):
|
| 35 |
+
"""Should have 2 INGEST phase guidelines."""
|
| 36 |
+
ingest = [s for s in GUIDELINE_SPECS if s["phase"] == "INGEST"]
|
| 37 |
+
assert len(ingest) == 2
|
| 38 |
+
|
| 39 |
+
def test_prescreen_guidelines_count(self):
|
| 40 |
+
"""Should have 3 PRESCREEN phase guidelines."""
|
| 41 |
+
prescreen = [s for s in GUIDELINE_SPECS if s["phase"] == "PRESCREEN"]
|
| 42 |
+
assert len(prescreen) == 3
|
| 43 |
+
|
| 44 |
+
def test_validate_trials_guidelines_count(self):
|
| 45 |
+
"""Should have 1 VALIDATE_TRIALS guideline."""
|
| 46 |
+
validate = [s for s in GUIDELINE_SPECS if s["phase"] == "VALIDATE_TRIALS"]
|
| 47 |
+
assert len(validate) == 1
|
| 48 |
+
|
| 49 |
+
def test_gap_followup_guidelines_count(self):
|
| 50 |
+
"""Should have 1 GAP_FOLLOWUP guideline."""
|
| 51 |
+
gap = [s for s in GUIDELINE_SPECS if s["phase"] == "GAP_FOLLOWUP"]
|
| 52 |
+
assert len(gap) == 1
|
| 53 |
+
|
| 54 |
+
def test_summary_guidelines_count(self):
|
| 55 |
+
"""Should have 1 SUMMARY guideline."""
|
| 56 |
+
summary = [s for s in GUIDELINE_SPECS if s["phase"] == "SUMMARY"]
|
| 57 |
+
assert len(summary) == 1
|
| 58 |
+
|
| 59 |
+
def test_global_guidelines_count(self):
|
| 60 |
+
"""Should have 3 global guidelines."""
|
| 61 |
+
global_g = [s for s in GUIDELINE_SPECS if s["phase"] == "GLOBAL"]
|
| 62 |
+
assert len(global_g) == 3
|
| 63 |
+
|
| 64 |
+
def test_tool_associations(self):
|
| 65 |
+
"""Guidelines with tools should reference correct tool entries."""
|
| 66 |
+
from parlant.sdk import ToolEntry
|
| 67 |
+
|
| 68 |
+
for spec in GUIDELINE_SPECS:
|
| 69 |
+
for tool in spec["tools"]:
|
| 70 |
+
assert isinstance(tool, ToolEntry), (
|
| 71 |
+
f"Tool {tool} in guideline '{spec['condition'][:30]}...' "
|
| 72 |
+
f"is not a ToolEntry"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
def test_medical_disclaimer_guideline_exists(self):
|
| 76 |
+
"""Should have a medical disclaimer guideline."""
|
| 77 |
+
disclaimer = [
|
| 78 |
+
s for s in GUIDELINE_SPECS
|
| 79 |
+
if "disclaimer" in s["action"].lower() or "medical advice" in s["action"].lower()
|
| 80 |
+
]
|
| 81 |
+
assert len(disclaimer) >= 1
|
| 82 |
+
|
| 83 |
+
def test_configure_passes_tools_to_agent(self, mock_agent):
|
| 84 |
+
"""configure_guidelines should pass tools list to create_guideline."""
|
| 85 |
+
configure_guidelines(mock_agent)
|
| 86 |
+
|
| 87 |
+
# Check that at least one call included tools
|
| 88 |
+
calls_with_tools = [
|
| 89 |
+
c for c in mock_agent.create_guideline.call_args_list
|
| 90 |
+
if c.kwargs.get("tools")
|
| 91 |
+
]
|
| 92 |
+
assert len(calls_with_tools) > 0
|