File size: 7,125 Bytes
e1624f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """
Specialist Node — Tier-adaptive clinical reasoning with Chain-of-Thought.
Design patterns:
- Model Tiering: routes to Qwen3.5-9B (fast) or Qwen3.6-27B (deep)
- Reflexion: accepts critic feedback for iterative refinement
- Anti-Hallucination: system prompt strictly forbids inventing treatments
The specialist produces a structured recommendation with explicit
reasoning sections (Findings → Staging → Treatment → Recommendation).
"""
import logging
from typing import Dict, Any
from .state import AgentState
from .tools import call_tier_model, get_tier_spec
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Prompt Engineering
# ---------------------------------------------------------------------------
_SYSTEM_PROMPT_TEMPLATE = """\
You are an expert clinical oncologist operating as part of the OncoAgent system.
Your task is to analyze the patient case and provide the most appropriate clinical
next steps based STRICTLY on the provided guidelines.
MODEL TIER: {tier_name} ({tier_description})
DIAGNOSTIC RIGOR POLICY:
1. You MUST verify if a definitive diagnosis (e.g., pathology report, biopsy) exists.
2. If diagnostic evidence is missing or inconclusive, your PRIMARY recommendation
MUST be the specific diagnostic procedure needed (e.g., "Esperar informe de biopsia",
"Realizar legrado diagnóstico").
3. You are STRICTLY FORBIDDEN from assuming cancer exists or jumping to treatment
protocols (surgery, chemo, radiation) if the pathology is not confirmed in the input.
ANTI-HALLUCINATION POLICY:
1. If the information is NOT explicitly in the guidelines, reply ONLY with:
"Información no concluyente en las guías provistas."
2. Do NOT invent dosages or protocols.
OUTPUT FORMAT (use this exact structure):
## Hallazgos Clínicos
[Summary of current patient presentation]
## Validación Diagnóstica
[State if pathology/biopsy is present and confirmed. If missing, specify what is needed.]
## Análisis de Estadificación
[Map findings to staging ONLY if diagnosis is confirmed. Otherwise, state why it's not possible.]
## Opciones de Manejo
[List clinical next steps or treatment options ONLY if appropriate for the diagnostic stage.]
## Recomendación Final
[The absolute next step for the clinician with confidence level]
Provide your recommendation in Spanish, clearly citing the guidelines.
IMPORTANT: Output your recommendation DIRECTLY. Do NOT wrap it in <think> tags."""
_USER_PROMPT_TEMPLATE = """\
Patient Information:
- Original Text: {clinical_text}
- Cancer Type: {cancer_type}
- Stage: {stage}
- Mutations: {mutations}
Clinical Guidelines Context:
{context}
{api_evidence}
{critic_feedback_section}
Based ONLY on the guidelines above, what are the recommended clinical next steps?"""
def _build_specialist_prompt(
state: AgentState,
) -> tuple[str, str]:
"""Build the system and user prompts for the specialist.
Incorporates critic feedback if this is a retry iteration.
Args:
state: Current LangGraph state.
Returns:
Tuple of (system_prompt, user_prompt).
"""
tier = state.get("selected_tier", 1)
spec = get_tier_spec(tier)
system_prompt = _SYSTEM_PROMPT_TEMPLATE.format(
tier_name=spec.name,
tier_description=spec.description,
)
entities = state.get("extracted_entities", {})
context = "\n---\n".join(state.get("rag_context", []))
api_evidence = state.get("api_evidence_context", [])
# Format API evidence if available
api_section = ""
if api_evidence:
api_section = "Additional Evidence (Genomic/Trials):\n" + "\n".join(api_evidence)
# Inject critic feedback for retry iterations
critic_feedback = state.get("critic_feedback", "")
critic_attempts = state.get("critic_attempts", 0)
feedback_section = ""
if critic_attempts > 0 and critic_feedback:
feedback_section = (
f"\n⚠️ PREVIOUS ATTEMPT FEEDBACK (attempt {critic_attempts}):\n"
f"The following issues were identified in your previous recommendation. "
f"Please address them in this revision:\n{critic_feedback}\n"
)
user_prompt = _USER_PROMPT_TEMPLATE.format(
clinical_text=state.get("clinical_text", ""),
cancer_type=entities.get("cancer_type", "Unknown"),
stage=entities.get("stage", "Unknown"),
mutations=", ".join(entities.get("mutations", [])),
context=context,
api_evidence=api_section,
critic_feedback_section=feedback_section,
)
return system_prompt, user_prompt
# ---------------------------------------------------------------------------
# Specialist Node
# ---------------------------------------------------------------------------
def specialist_node(state: AgentState) -> Dict[str, Any]:
"""Generate a clinical recommendation using the tier-adaptive model.
If critic feedback exists in the state (retry iteration), the feedback
is injected into the prompt so the model can self-correct.
Args:
state: Current LangGraph state.
Returns:
State update with clinical_recommendation and reasoning_trace.
"""
context = state.get("rag_context", [])
tier = state.get("selected_tier", 1)
attempt = state.get("critic_attempts", 0)
# Guard: no context available
if not context:
return {
"clinical_recommendation": (
"Información no concluyente en las guías provistas. "
"No se encontró evidencia relevante en la base de datos clínica."
),
"reasoning_trace": "No RAG context available — safe fallback triggered.",
}
system_prompt, user_prompt = _build_specialist_prompt(state)
spec = get_tier_spec(tier)
logger.info(
"Specialist invoking %s (attempt %d, context chunks: %d)",
spec, attempt + 1, len(context),
)
try:
recommendation = call_tier_model(
tier=tier,
system_prompt=system_prompt,
user_prompt=user_prompt,
)
# Build reasoning trace for the critic
reasoning_trace = (
f"Tier: {spec.name} ({spec.model_id})\n"
f"Attempt: {attempt + 1}\n"
f"Context chunks: {len(context)}\n"
f"API evidence items: {len(state.get('api_evidence_context', []))}\n"
f"Recommendation length: {len(recommendation)} chars"
)
except RuntimeError as exc:
logger.error("Specialist inference failed: %s", exc)
recommendation = (
"Error en el sistema de inferencia. "
"No se pudo generar la recomendación clínica en este momento."
)
reasoning_trace = f"INFERENCE ERROR: {exc}"
# Detect if model returned the safe phrase
if "información no concluyente" in recommendation.lower():
recommendation = "Información no concluyente en las guías provistas."
return {
"clinical_recommendation": recommendation,
"reasoning_trace": reasoning_trace,
}
|