Drug Interaction Arbitrator
Build an adversarial debate agent where one pharmacist argues an interaction is clinically significant while another argues it's manageable, helping reduce alert fatigue
Drug Interaction Arbitrator
Build an adversarial multi-agent system that debates the clinical significance of drug interactions, helping pharmacists make nuanced decisions instead of being overwhelmed by alerts.
| Difficulty | Advanced |
| Time | 2-3 days |
| Code | ~700 lines |
| Pattern | Adversarial Debate (Pharmacy Domain) |
TL;DR
Apply the adversarial debate pattern to drug interaction assessment using two pharmacist agents (one arguing significant risk, one arguing manageable), patient-specific context (age, renal function, other meds), evidence from drug databases, and clinical decision synthesis. Reduces alert fatigue by providing nuanced guidance instead of binary warnings.
Clinical Disclaimer
This system is for educational purposes only. It is designed to assist licensed pharmacists in evaluating drug interactions. It does not replace professional judgment, and all recommendations must be verified against authoritative drug information resources before any clinical decision.
The Problem: Alert Fatigue
┌─────────────────────────────────────────────────────────────────────┐
│ ALERT FATIGUE IN PHARMACY │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ The problem: │
│ • 90% of drug interaction alerts are overridden │
│ • Most alerts are not clinically significant │
│ • Pharmacists become desensitized to warnings │
│ • Real dangers get lost in the noise │
│ │
│ Current system: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Drug A + Drug B detected │ │
│ │ ⚠️ WARNING: Potential interaction │ │
│ │ │ │
│ │ [ Override ] [ Cancel ] │ │
│ │ │ │
│ │ Pharmacist: *clicks override for the 50th time today* │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Adversarial debate: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Drug A + Drug B detected │ │
│ │ │ │
│ │ Significance Advocate: "This is high-risk because..." │ │
│ │ Manageability Advocate: "This is manageable because..." │ │
│ │ │ │
│ │ Clinical Decision: │ │
│ │ "Manageable with weekly INR monitoring. Patient's stable │ │
│ │ renal function and low warfarin dose reduce risk." │ │
│ │ │ │
│ │ [ Proceed with monitoring plan ] [ Consult prescriber ] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Result: Nuanced guidance that supports clinical decision-making │
│ │
└─────────────────────────────────────────────────────────────────────┘What You'll Build
A drug interaction arbitrator that:
- Parses interaction alerts - Extracts drug pair, interaction type, and severity
- Gathers patient context - Age, renal/hepatic function, other medications, diagnoses
- Significance Advocate argues - Why this interaction poses real clinical risk
- Manageability Advocate argues - Why this can be safely managed with monitoring
- Critiques and rebuttals - Each side challenges the other's reasoning
- Clinical synthesizer - Provides nuanced recommendation with specific monitoring plan
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ DRUG INTERACTION ARBITRATOR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Input: Warfarin + Fluconazole interaction in 72yo with CKD │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ CONTEXT GATHERER │ Patient factors, other meds, diagnoses │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ╔══════════════════════════════════════════════════════════════╗ │
│ ║ ROUND 1: Position Statements ║ │
│ ║ ┌─────────────────┐ ┌─────────────────┐ ║ │
│ ║ │ SIGNIFICANCE │ (parallel) │ MANAGEABILITY │ ║ │
│ ║ │ ADVOCATE │ │ ADVOCATE │ ║ │
│ ║ │ "High risk: │ │ "Manageable: │ ║ │
│ ║ │ CYP2C9 inhib, │ │ dose adjust, │ ║ │
│ ║ │ bleeding risk"│ │ monitor INR" │ ║ │
│ ║ └─────────────────┘ └─────────────────┘ ║ │
│ ╚══════════════════════════════════════════════════════════════╝ │
│ │ │
│ ▼ │
│ ╔══════════════════════════════════════════════════════════════╗ │
│ ║ ROUND 2: Challenges ║ │
│ ║ ┌─────────────────┐ ┌─────────────────┐ ║ │
│ ║ │ SIGNIFICANCE │ (parallel) │ MANAGEABILITY │ ║ │
│ ║ │ "Monitoring │ │ "Patient has │ ║ │
│ ║ │ won't prevent │ │ tolerated │ ║ │
│ ║ │ rapid INR ↑" │ │ before..." │ ║ │
│ ║ └─────────────────┘ └─────────────────┘ ║ │
│ ╚══════════════════════════════════════════════════════════════╝ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ CLINICAL SYNTH │ Weighs both sides, patient factors │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ DECISION: Proceed with caution │ │
│ │ • Reduce warfarin dose by 25% │ │
│ │ • Check INR in 3-5 days │ │
│ │ • Patient education on bleeding signs │ │
│ │ • Document interaction management │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘Project Structure
debate-drug-interaction/
├── src/
│ ├── __init__.py
│ ├── config.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── interaction.py # Drug interaction models
│ │ ├── patient.py # Patient context models
│ │ ├── arguments.py # Debate argument models
│ │ └── state.py # LangGraph state
│ ├── agents/
│ │ ├── __init__.py
│ │ ├── context_gatherer.py # Gathers patient context
│ │ ├── significance.py # Argues for clinical significance
│ │ ├── manageability.py # Argues for manageability
│ │ └── synthesizer.py # Clinical decision synthesis
│ ├── workflow/
│ │ ├── __init__.py
│ │ └── arbitrator.py # LangGraph workflow
│ └── api/
│ ├── __init__.py
│ └── main.py # FastAPI endpoints
├── tests/
├── docker-compose.yml
└── requirements.txtTech Stack
| Technology | Purpose |
|---|---|
| LangGraph | Adversarial debate workflow |
| OpenAI GPT-4o | Pharmacist personas with clinical reasoning |
| Pydantic | Drug interaction and patient context models |
| FastAPI | API for interaction arbitration |
Implementation
Configuration
# src/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
openai_api_key: str
openai_model: str = "gpt-4o"
temperature_advocates: float = 0.4
temperature_synthesizer: float = 0.2
# Debate settings
num_risk_factors: int = 4
num_mitigation_strategies: int = 3
class Config:
env_file = ".env"
settings = Settings()Drug Interaction Models
# src/models/interaction.py
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
class InteractionSeverity(str, Enum):
CONTRAINDICATED = "contraindicated"
MAJOR = "major"
MODERATE = "moderate"
MINOR = "minor"
class InteractionMechanism(str, Enum):
PHARMACOKINETIC = "pharmacokinetic" # Absorption, distribution, metabolism, excretion
PHARMACODYNAMIC = "pharmacodynamic" # Additive, synergistic, antagonistic effects
BOTH = "both"
class DrugInteraction(BaseModel):
"""A drug-drug interaction alert."""
drug_a: str
drug_a_class: Optional[str] = None
drug_b: str
drug_b_class: Optional[str] = None
severity: InteractionSeverity
mechanism: InteractionMechanism
description: str = Field(description="Standard interaction description")
clinical_effects: List[str] = Field(
description="Potential adverse outcomes"
)
management_options: List[str] = Field(
default_factory=list,
description="Standard management recommendations"
)
# Mechanistic details
enzyme_involved: Optional[str] = None # e.g., CYP2C9, CYP3A4
protein_binding: Optional[str] = None
onset: Optional[str] = None # rapid, delayed
class InteractionContext(BaseModel):
"""Full context for an interaction assessment."""
interaction: DrugInteraction
indication_drug_a: Optional[str] = None
indication_drug_b: Optional[str] = None
duration_drug_b: Optional[str] = None # How long will interacting drug be usedPatient Context Models
# src/models/patient.py
from pydantic import BaseModel, Field
from typing import List, Optional
class RenalFunction(BaseModel):
"""Patient renal function."""
creatinine: Optional[float] = None # mg/dL
egfr: Optional[float] = None # mL/min/1.73m2
stage: Optional[str] = None # Normal, Mild, Moderate, Severe, ESRD
class HepaticFunction(BaseModel):
"""Patient hepatic function."""
alt: Optional[float] = None
ast: Optional[float] = None
bilirubin: Optional[float] = None
child_pugh: Optional[str] = None # A, B, C
class PatientContext(BaseModel):
"""Patient-specific factors for interaction assessment."""
age: int
weight_kg: Optional[float] = None
sex: str
# Organ function
renal: Optional[RenalFunction] = None
hepatic: Optional[HepaticFunction] = None
# Current medications (besides the interacting pair)
other_medications: List[str] = Field(default_factory=list)
# Relevant conditions
diagnoses: List[str] = Field(default_factory=list)
# History
previous_adverse_reactions: List[str] = Field(default_factory=list)
bleeding_history: bool = False
fall_risk: bool = False
# Relevant labs
inr: Optional[float] = None
platelets: Optional[int] = None
# Monitoring capability
can_monitor_closely: bool = True
adherence_concerns: bool = FalseArgument Models
# src/models/arguments.py
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
class AdvocateRole(str, Enum):
SIGNIFICANCE = "significance_advocate"
MANAGEABILITY = "manageability_advocate"
class RiskArgument(BaseModel):
"""An argument for clinical significance."""
risk_factor: str = Field(description="Specific risk factor")
patient_relevance: str = Field(
description="How this applies to THIS patient"
)
potential_outcome: str = Field(
description="What could happen if ignored"
)
evidence_strength: str = Field(
description="strong, moderate, weak"
)
class SignificancePosition(BaseModel):
"""Complete position from Significance Advocate."""
overall_risk: str = Field(description="high, moderate, low")
risk_arguments: List[RiskArgument]
contraindication_strength: str = Field(
description="absolute, relative, conditional"
)
recommended_action: str = Field(
description="avoid, use alternative, requires specialist"
)
confidence: float = Field(ge=0.0, le=1.0)
class MitigationStrategy(BaseModel):
"""A strategy to manage the interaction."""
strategy: str = Field(description="The mitigation approach")
implementation: str = Field(description="How to implement")
effectiveness: str = Field(description="How well this mitigates risk")
monitoring_required: str = Field(description="What to monitor")
class ManageabilityPosition(BaseModel):
"""Complete position from Manageability Advocate."""
overall_manageability: str = Field(description="highly, moderately, marginally")
mitigation_strategies: List[MitigationStrategy]
dose_adjustments: Optional[str] = None
monitoring_plan: str
patient_education: List[str]
confidence: float = Field(ge=0.0, le=1.0)
class Challenge(BaseModel):
"""A challenge to the opponent's position."""
target_point: str
weakness: str
counter_evidence: str
class ChallengeSet(BaseModel):
"""Collection of challenges."""
role: AdvocateRole
challenges: List[Challenge]Synthesis Models
# src/models/synthesis.py
from pydantic import BaseModel, Field
from typing import List, Optional
from enum import Enum
class ClinicalDecision(str, Enum):
AVOID = "avoid"
USE_ALTERNATIVE = "use_alternative"
PROCEED_WITH_CAUTION = "proceed_with_caution"
PROCEED_STANDARD = "proceed_standard"
SPECIALIST_CONSULT = "specialist_consult"
class MonitoringItem(BaseModel):
"""A specific monitoring requirement."""
parameter: str # INR, creatinine, symptoms
frequency: str # daily, weekly, PRN
action_threshold: Optional[str] = None
escalation_plan: Optional[str] = None
class InteractionDecision(BaseModel):
"""Final synthesized decision on the interaction."""
decision: ClinicalDecision
confidence: float = Field(ge=0.0, le=1.0)
rationale: str = Field(
description="Brief explanation of decision"
)
# If proceeding
dose_adjustment: Optional[str] = None
monitoring_plan: List[MonitoringItem] = Field(default_factory=list)
duration_limited: Optional[str] = None # e.g., "5 days max"
# Patient communication
patient_counseling: List[str] = Field(default_factory=list)
warning_signs: List[str] = Field(default_factory=list)
# Documentation
documentation_note: str = Field(
description="Note for the medical record"
)
# If not proceeding
alternative_suggestion: Optional[str] = None
prescriber_communication: Optional[str] = None
# Key factors
factors_favoring_significance: List[str]
factors_favoring_manageability: List[str]Debate State
# src/models/state.py
from typing import TypedDict, List, Optional, Annotated
from enum import Enum
import operator
from .interaction import InteractionContext
from .patient import PatientContext
from .arguments import SignificancePosition, ManageabilityPosition, ChallengeSet
from .synthesis import InteractionDecision
class ArbitrationPhase(str, Enum):
CONTEXT = "context"
POSITIONS = "positions"
CHALLENGES = "challenges"
SYNTHESIS = "synthesis"
COMPLETE = "complete"
class ArbitrationState(TypedDict):
"""State for the drug interaction arbitration."""
# Input
interaction: InteractionContext
patient: PatientContext
# Workflow
phase: ArbitrationPhase
# Positions
significance_position: Optional[SignificancePosition]
manageability_position: Optional[ManageabilityPosition]
# Challenges
significance_challenges: Optional[ChallengeSet]
manageability_challenges: Optional[ChallengeSet]
# Output
decision: Optional[InteractionDecision]
# Audit
reasoning_trace: Annotated[List[str], operator.add]Significance Advocate Agent
# src/agents/significance.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from ..models.interaction import InteractionContext
from ..models.patient import PatientContext
from ..models.arguments import (
SignificancePosition, RiskArgument, AdvocateRole,
Challenge, ChallengeSet, ManageabilityPosition
)
from ..config import settings
class SignificanceAdvocate:
"""Argues for clinical significance of the interaction."""
def __init__(self):
self.llm = ChatOpenAI(
model=settings.openai_model,
api_key=settings.openai_api_key,
temperature=settings.temperature_advocates
)
self.role = AdvocateRole.SIGNIFICANCE
async def generate_position(
self,
interaction: InteractionContext,
patient: PatientContext
) -> SignificancePosition:
"""Generate position arguing for clinical significance."""
llm = self.llm.with_structured_output(SignificancePosition)
prompt = ChatPromptTemplate.from_messages([
("system", """You are a clinical pharmacist advocating for the
SIGNIFICANCE of a drug interaction. Your role is to ensure risks
are not dismissed too easily.
Argue why this interaction poses REAL clinical risk for THIS patient.
Consider:
1. Mechanism and severity of the interaction
2. Patient-specific risk factors (age, organ function, other meds)
3. Potential adverse outcomes if the interaction occurs
4. Cases where "manageable" interactions caused harm
5. Whether monitoring can truly prevent adverse outcomes
Generate {num_risks} risk arguments specific to this patient.
Be rigorous - pharmacists override too many alerts. Make the case
for why THIS one matters."""),
("human", """INTERACTION:
{drug_a} + {drug_b}
Severity: {severity}
Mechanism: {mechanism}
Clinical effects: {effects}
PATIENT:
Age: {age}, Sex: {sex}
Renal function: {renal}
Hepatic function: {hepatic}
Other medications: {other_meds}
Diagnoses: {diagnoses}
Relevant labs: {labs}
Bleeding history: {bleeding}
Fall risk: {fall}
Argue for the clinical significance of this interaction.""")
])
chain = prompt | llm
result = await chain.ainvoke({
"num_risks": settings.num_risk_factors,
"drug_a": interaction.interaction.drug_a,
"drug_b": interaction.interaction.drug_b,
"severity": interaction.interaction.severity.value,
"mechanism": interaction.interaction.mechanism.value,
"effects": ", ".join(interaction.interaction.clinical_effects),
"age": patient.age,
"sex": patient.sex,
"renal": f"eGFR: {patient.renal.egfr}" if patient.renal else "Unknown",
"hepatic": patient.hepatic.child_pugh if patient.hepatic else "Unknown",
"other_meds": ", ".join(patient.other_medications) or "None",
"diagnoses": ", ".join(patient.diagnoses) or "None",
"labs": f"INR: {patient.inr}, Plt: {patient.platelets}" if patient.inr else "Not available",
"bleeding": "Yes" if patient.bleeding_history else "No",
"fall": "Yes" if patient.fall_risk else "No"
})
return result
async def generate_challenges(
self,
interaction: InteractionContext,
patient: PatientContext,
opponent_position: ManageabilityPosition
) -> ChallengeSet:
"""Challenge the manageability advocate's position."""
llm = self.llm.with_structured_output(ChallengeSet)
prompt = ChatPromptTemplate.from_messages([
("system", """You are challenging the Manageability Advocate's position.
They argued this interaction is manageable:
{opponent_position}
Generate challenges to their position. Focus on:
1. Limitations of proposed monitoring
2. Patient factors that complicate management
3. Cases where "monitoring" failed to prevent harm
4. Unrealistic assumptions about adherence or follow-up"""),
("human", "Challenge their manageability argument.")
])
opponent_text = (
f"Overall: {opponent_position.overall_manageability}\n"
f"Strategies: {[s.strategy for s in opponent_position.mitigation_strategies]}\n"
f"Monitoring: {opponent_position.monitoring_plan}"
)
chain = prompt | llm
result = await chain.ainvoke({
"opponent_position": opponent_text
})
result.role = self.role
return resultManageability Advocate Agent
# src/agents/manageability.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from ..models.interaction import InteractionContext
from ..models.patient import PatientContext
from ..models.arguments import (
ManageabilityPosition, MitigationStrategy, AdvocateRole,
ChallengeSet, SignificancePosition
)
from ..config import settings
class ManageabilityAdvocate:
"""Argues for manageability of the interaction."""
def __init__(self):
self.llm = ChatOpenAI(
model=settings.openai_model,
api_key=settings.openai_api_key,
temperature=settings.temperature_advocates
)
self.role = AdvocateRole.MANAGEABILITY
async def generate_position(
self,
interaction: InteractionContext,
patient: PatientContext
) -> ManageabilityPosition:
"""Generate position arguing for manageability."""
llm = self.llm.with_structured_output(ManageabilityPosition)
prompt = ChatPromptTemplate.from_messages([
("system", """You are a clinical pharmacist advocating for the
MANAGEABILITY of a drug interaction. Your role is to ensure
therapeutic options aren't unnecessarily restricted.
Argue why this interaction can be SAFELY MANAGED for THIS patient.
Consider:
1. Dose adjustments that reduce risk
2. Monitoring parameters and frequency
3. Patient factors that REDUCE risk (good renal function, etc.)
4. Limited duration of interacting drug
5. Patient's ability to recognize warning signs
Generate {num_strategies} mitigation strategies.
Be practical - many interactions are routinely managed. Make the case
for how to do it safely, not whether to avoid entirely."""),
("human", """INTERACTION:
{drug_a} + {drug_b}
Severity: {severity}
Mechanism: {mechanism}
Standard management: {management}
PATIENT:
Age: {age}, Sex: {sex}
Renal function: {renal}
Hepatic function: {hepatic}
Other medications: {other_meds}
Can monitor closely: {can_monitor}
Adherence concerns: {adherence}
Argue for the manageability of this interaction.""")
])
chain = prompt | llm
result = await chain.ainvoke({
"num_strategies": settings.num_mitigation_strategies,
"drug_a": interaction.interaction.drug_a,
"drug_b": interaction.interaction.drug_b,
"severity": interaction.interaction.severity.value,
"mechanism": interaction.interaction.mechanism.value,
"management": ", ".join(interaction.interaction.management_options) or "Standard monitoring",
"age": patient.age,
"sex": patient.sex,
"renal": f"eGFR: {patient.renal.egfr}" if patient.renal else "Unknown",
"hepatic": patient.hepatic.child_pugh if patient.hepatic else "Unknown",
"other_meds": ", ".join(patient.other_medications) or "None",
"can_monitor": "Yes" if patient.can_monitor_closely else "Limited",
"adherence": "Yes" if patient.adherence_concerns else "No"
})
return result
async def generate_challenges(
self,
interaction: InteractionContext,
patient: PatientContext,
opponent_position: SignificancePosition
) -> ChallengeSet:
"""Challenge the significance advocate's position."""
llm = self.llm.with_structured_output(ChallengeSet)
prompt = ChatPromptTemplate.from_messages([
("system", """You are challenging the Significance Advocate's position.
They argued this interaction is highly significant:
{opponent_position}
Generate challenges to their position. Focus on:
1. Overstated risks not supported by evidence
2. Patient factors they ignored that reduce risk
3. Clinical experience with safe management
4. Therapeutic necessity of both drugs"""),
("human", "Challenge their significance argument.")
])
opponent_text = (
f"Overall risk: {opponent_position.overall_risk}\n"
f"Arguments: {[a.risk_factor for a in opponent_position.risk_arguments]}\n"
f"Recommendation: {opponent_position.recommended_action}"
)
chain = prompt | llm
result = await chain.ainvoke({
"opponent_position": opponent_text
})
result.role = self.role
return resultClinical Synthesizer
# src/agents/synthesizer.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from ..models.interaction import InteractionContext
from ..models.patient import PatientContext
from ..models.arguments import SignificancePosition, ManageabilityPosition, ChallengeSet
from ..models.synthesis import InteractionDecision, ClinicalDecision, MonitoringItem
from ..config import settings
class ClinicalSynthesizer:
"""Synthesizes debate into clinical decision."""
def __init__(self):
self.llm = ChatOpenAI(
model=settings.openai_model,
api_key=settings.openai_api_key,
temperature=settings.temperature_synthesizer
).with_structured_output(InteractionDecision)
self.prompt = ChatPromptTemplate.from_messages([
("system", """You are a senior clinical pharmacist making the final
decision on a drug interaction after reviewing both advocates' positions.
Your options:
- AVOID: Interaction too risky, do not dispense
- USE_ALTERNATIVE: Suggest a safer alternative drug
- PROCEED_WITH_CAUTION: Can use with specific monitoring/dose changes
- PROCEED_STANDARD: Interaction is clinically insignificant
- SPECIALIST_CONSULT: Need specialist input
Weigh:
1. Strength of each side's arguments
2. Patient-specific risk/benefit
3. Therapeutic necessity of both drugs
4. Feasibility of monitoring
5. What a reasonable pharmacist would do
Provide a specific, actionable decision with:
- Exact monitoring parameters and frequency
- Dose adjustments if applicable
- Patient counseling points
- Documentation for the record"""),
("human", """INTERACTION: {drug_a} + {drug_b} ({severity})
SIGNIFICANCE ADVOCATE:
Risk level: {sig_risk}
Key arguments: {sig_args}
Recommendation: {sig_rec}
MANAGEABILITY ADVOCATE:
Manageability: {man_level}
Strategies: {man_strats}
Monitoring: {man_monitor}
CHALLENGES EXCHANGED:
Significance challenged: {sig_challenges}
Manageability challenged: {man_challenges}
PATIENT FACTORS:
{patient_summary}
Make your clinical decision.""")
])
async def synthesize(
self,
interaction: InteractionContext,
patient: PatientContext,
significance: SignificancePosition,
manageability: ManageabilityPosition,
sig_challenges: ChallengeSet,
man_challenges: ChallengeSet
) -> InteractionDecision:
"""Produce final clinical decision."""
patient_summary = (
f"Age: {patient.age}, {patient.sex}\n"
f"Renal: {patient.renal.egfr if patient.renal else 'Unknown'} mL/min\n"
f"Other meds: {', '.join(patient.other_medications[:5]) or 'None'}\n"
f"Bleeding history: {patient.bleeding_history}\n"
f"Can monitor: {patient.can_monitor_closely}"
)
chain = self.prompt | self.llm
result = await chain.ainvoke({
"drug_a": interaction.interaction.drug_a,
"drug_b": interaction.interaction.drug_b,
"severity": interaction.interaction.severity.value,
"sig_risk": significance.overall_risk,
"sig_args": ", ".join([a.risk_factor for a in significance.risk_arguments]),
"sig_rec": significance.recommended_action,
"man_level": manageability.overall_manageability,
"man_strats": ", ".join([s.strategy for s in manageability.mitigation_strategies]),
"man_monitor": manageability.monitoring_plan,
"sig_challenges": ", ".join([c.weakness for c in sig_challenges.challenges]),
"man_challenges": ", ".join([c.weakness for c in man_challenges.challenges]),
"patient_summary": patient_summary
})
return resultLangGraph Workflow
# src/workflow/arbitrator.py
from langgraph.graph import StateGraph, END
from ..models.state import ArbitrationState, ArbitrationPhase
from ..agents.significance import SignificanceAdvocate
from ..agents.manageability import ManageabilityAdvocate
from ..agents.synthesizer import ClinicalSynthesizer
# Initialize agents
significance = SignificanceAdvocate()
manageability = ManageabilityAdvocate()
synthesizer = ClinicalSynthesizer()
async def significance_position_node(state: ArbitrationState) -> ArbitrationState:
"""Significance advocate generates position."""
position = await significance.generate_position(
state["interaction"],
state["patient"]
)
return {
"significance_position": position,
"reasoning_trace": [f"Significance: {position.overall_risk} risk"]
}
async def manageability_position_node(state: ArbitrationState) -> ArbitrationState:
"""Manageability advocate generates position."""
position = await manageability.generate_position(
state["interaction"],
state["patient"]
)
return {
"manageability_position": position,
"reasoning_trace": [f"Manageability: {position.overall_manageability}"]
}
async def advance_to_challenges_node(state: ArbitrationState) -> ArbitrationState:
"""Advance to challenge phase."""
return {
**state,
"phase": ArbitrationPhase.CHALLENGES,
"reasoning_trace": ["Advancing to challenges"]
}
async def significance_challenges_node(state: ArbitrationState) -> ArbitrationState:
"""Significance challenges manageability."""
challenges = await significance.generate_challenges(
state["interaction"],
state["patient"],
state["manageability_position"]
)
return {
"significance_challenges": challenges,
"reasoning_trace": [f"Significance raised {len(challenges.challenges)} challenges"]
}
async def manageability_challenges_node(state: ArbitrationState) -> ArbitrationState:
"""Manageability challenges significance."""
challenges = await manageability.generate_challenges(
state["interaction"],
state["patient"],
state["significance_position"]
)
return {
"manageability_challenges": challenges,
"reasoning_trace": [f"Manageability raised {len(challenges.challenges)} challenges"]
}
async def synthesize_node(state: ArbitrationState) -> ArbitrationState:
"""Synthesize final decision."""
decision = await synthesizer.synthesize(
state["interaction"],
state["patient"],
state["significance_position"],
state["manageability_position"],
state["significance_challenges"],
state["manageability_challenges"]
)
return {
**state,
"decision": decision,
"phase": ArbitrationPhase.COMPLETE,
"reasoning_trace": [f"Decision: {decision.decision.value}"]
}
def create_arbitration_workflow() -> StateGraph:
"""Create the drug interaction arbitration workflow."""
workflow = StateGraph(ArbitrationState)
# Add nodes
workflow.add_node("significance_position", significance_position_node)
workflow.add_node("manageability_position", manageability_position_node)
workflow.add_node("advance_challenges", advance_to_challenges_node)
workflow.add_node("significance_challenges", significance_challenges_node)
workflow.add_node("manageability_challenges", manageability_challenges_node)
workflow.add_node("synthesize", synthesize_node)
# Entry point - both positions in parallel
workflow.set_entry_point("significance_position")
workflow.add_edge("significance_position", "advance_challenges")
# Also start manageability in parallel (fan-out from entry)
workflow.set_entry_point("manageability_position")
workflow.add_edge("manageability_position", "advance_challenges")
# Challenges in parallel
workflow.add_edge("advance_challenges", "significance_challenges")
workflow.add_edge("advance_challenges", "manageability_challenges")
workflow.add_edge("significance_challenges", "synthesize")
workflow.add_edge("manageability_challenges", "synthesize")
workflow.add_edge("synthesize", END)
return workflow.compile()
arbitration_agent = create_arbitration_workflow()FastAPI Application
# src/api/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from ..workflow.arbitrator import arbitration_agent, ArbitrationState
from ..models.state import ArbitrationPhase
from ..models.interaction import DrugInteraction, InteractionContext, InteractionSeverity, InteractionMechanism
from ..models.patient import PatientContext, RenalFunction
from ..models.synthesis import ClinicalDecision
app = FastAPI(
title="Drug Interaction Arbitrator",
description="Adversarial debate for nuanced drug interaction decisions",
version="1.0.0"
)
class InteractionRequest(BaseModel):
drug_a: str
drug_b: str
severity: str # contraindicated, major, moderate, minor
mechanism: str # pharmacokinetic, pharmacodynamic, both
clinical_effects: List[str]
# Patient
patient_age: int
patient_sex: str
patient_egfr: Optional[float] = None
other_medications: List[str] = []
diagnoses: List[str] = []
bleeding_history: bool = False
inr: Optional[float] = None
class DecisionResponse(BaseModel):
decision: str
confidence: float
rationale: str
dose_adjustment: Optional[str]
monitoring: List[dict]
patient_counseling: List[str]
warning_signs: List[str]
documentation_note: str
factors_for_significance: List[str]
factors_for_manageability: List[str]
@app.post("/arbitrate", response_model=DecisionResponse)
async def arbitrate_interaction(request: InteractionRequest):
"""Run adversarial arbitration on a drug interaction."""
interaction = InteractionContext(
interaction=DrugInteraction(
drug_a=request.drug_a,
drug_b=request.drug_b,
severity=InteractionSeverity(request.severity),
mechanism=InteractionMechanism(request.mechanism),
description=f"Interaction between {request.drug_a} and {request.drug_b}",
clinical_effects=request.clinical_effects
)
)
patient = PatientContext(
age=request.patient_age,
sex=request.patient_sex,
renal=RenalFunction(egfr=request.patient_egfr) if request.patient_egfr else None,
other_medications=request.other_medications,
diagnoses=request.diagnoses,
bleeding_history=request.bleeding_history,
inr=request.inr
)
initial_state: ArbitrationState = {
"interaction": interaction,
"patient": patient,
"phase": ArbitrationPhase.POSITIONS,
"significance_position": None,
"manageability_position": None,
"significance_challenges": None,
"manageability_challenges": None,
"decision": None,
"reasoning_trace": []
}
try:
result = await arbitration_agent.ainvoke(initial_state)
decision = result["decision"]
return DecisionResponse(
decision=decision.decision.value,
confidence=decision.confidence,
rationale=decision.rationale,
dose_adjustment=decision.dose_adjustment,
monitoring=[
{"parameter": m.parameter, "frequency": m.frequency}
for m in decision.monitoring_plan
],
patient_counseling=decision.patient_counseling,
warning_signs=decision.warning_signs,
documentation_note=decision.documentation_note,
factors_for_significance=decision.factors_favoring_significance,
factors_for_manageability=decision.factors_favoring_manageability
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health():
return {"status": "healthy", "service": "drug-interaction-arbitrator"}Example Usage
curl -X POST http://localhost:8000/arbitrate \
-H "Content-Type: application/json" \
-d '{
"drug_a": "Warfarin",
"drug_b": "Fluconazole",
"severity": "major",
"mechanism": "pharmacokinetic",
"clinical_effects": ["Increased INR", "Bleeding risk"],
"patient_age": 72,
"patient_sex": "F",
"patient_egfr": 45,
"other_medications": ["Lisinopril", "Metformin"],
"diagnoses": ["Atrial fibrillation", "Type 2 diabetes", "CKD stage 3"],
"bleeding_history": false,
"inr": 2.3
}'Key Learnings
-
Nuanced decisions reduce alert fatigue - Instead of binary warn/override, the debate produces actionable guidance that respects pharmacist expertise.
-
Patient context changes everything - The same interaction can be high-risk in one patient and manageable in another. The debate forces consideration of individual factors.
-
Monitoring plans must be specific - "Monitor closely" is useless. The synthesizer produces exact parameters, frequencies, and thresholds.
-
Documentation enables learning - The structured output includes a documentation note that captures the reasoning for future reference.
Key Concepts Recap
| Concept | What It Is | Why It Matters |
|---|---|---|
| Alert Fatigue | Desensitization from too many warnings | 90% override rate means real dangers get missed |
| Significance Advocate | Argues for clinical risk | Ensures risks aren't dismissed |
| Manageability Advocate | Argues for safe management | Ensures options aren't unnecessarily restricted |
| Patient-Specific Context | Age, renal function, other meds | Same interaction, different risk in different patients |
| Actionable Monitoring | Specific parameters and frequencies | "Check INR in 3-5 days" not "monitor closely" |
Next Steps
Continue with:
- Tumor Board Simulator - Multi-expert debate with 4+ specialists
Differential Diagnosis Debate
Build an adversarial diagnostic agent where two physician personas argue for competing diagnoses, with an attending physician judging and synthesizing a final differential
Tumor Board Simulator
Build a multi-specialist debate agent simulating a tumor board conference where oncologist, surgeon, radiation oncologist, and pathologist debate optimal cancer treatment plans using LangGraph