OncoAgent / agents /formatter.py
MaximoLopezChenlo's picture
Upload folder using huggingface_hub
e1624f5 verified
"""
Formatter & Fallback Nodes — Structured output and safe degradation.
Formatter: transforms the validated recommendation into a structured
format optimised for the Gradio UI, including confidence reports and
source citations.
Fallback: safe degradation when RAG or reasoning fails, following the
Anti-Hallucination Policy (Rule #39).
"""
import logging
from datetime import datetime, timezone
from typing import Dict, Any, List
from .state import AgentState
from .tools import get_tier_spec
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Response Formatter Node
# ---------------------------------------------------------------------------
def formatter_node(state: AgentState) -> Dict[str, Any]:
"""Transform the validated recommendation into structured UI output.
Produces:
- formatted_recommendation: Markdown with metadata header
- confidence_report: Dict of all quality metrics
- source_citations: Formatted bibliography
Args:
state: Current LangGraph state.
Returns:
State update with formatted output, confidence report, and citations.
"""
recommendation = state.get("clinical_recommendation", "")
tier = state.get("selected_tier", 1)
spec = get_tier_spec(tier)
rag_confidence = state.get("rag_confidence", 0.0)
critic_attempts = state.get("critic_attempts", 0)
complexity_score = state.get("complexity_score", 0.0)
rag_sources = state.get("rag_sources", [])
rag_count = state.get("rag_retrieval_count", 0)
rag_graded = state.get("rag_grading_pass_count", 0)
rag_rewrites = state.get("rag_query_rewrites", 0)
api_evidence = state.get("api_evidence_context", [])
entities = state.get("extracted_entities", {})
# --- Confidence report ---
confidence_report: Dict[str, Any] = {
"tier_used": tier,
"tier_name": spec.name,
"model_id": spec.model_id,
"complexity_score": complexity_score,
"rag_confidence": rag_confidence,
"rag_retrieval_count": rag_count,
"rag_graded_relevant": rag_graded,
"rag_query_rewrites": rag_rewrites,
"critic_iterations": critic_attempts,
"api_evidence_count": len(api_evidence),
"timestamp": datetime.now(timezone.utc).isoformat(),
}
# --- Confidence level label ---
if rag_confidence >= 0.7:
confidence_label = "🟢 Alta"
elif rag_confidence >= 0.4:
confidence_label = "🟡 Media"
else:
confidence_label = "🔴 Baja"
# --- Formatted recommendation with metadata header ---
header = (
f"---\n"
f"**OncoAgent — Recomendación Clínica**\n"
f"📊 Modelo: {spec.name} (Tier {tier}) | "
f"Confianza RAG: {confidence_label} ({rag_confidence:.2f}) | "
f"Iteraciones Críticas: {critic_attempts}\n"
f"🧬 Tipo: {entities.get('cancer_type', 'N/A')} | "
f"Estadío: {entities.get('stage', 'N/A')} | "
f"Mutaciones: {', '.join(entities.get('mutations', [])) or 'N/A'}\n"
f"---\n\n"
)
formatted = header + recommendation
# --- Source citations ---
citations = []
if rag_sources:
citations.append("### Fuentes Clínicas (RAG)")
citations.extend(rag_sources)
if api_evidence:
citations.append("\n### Evidencia Adicional (APIs)")
citations.extend([f"- {e}" for e in api_evidence])
# --- Safety status ---
safety_status = "Validated against clinical oncology guidelines"
return {
"formatted_recommendation": formatted,
"confidence_report": confidence_report,
"source_citations": citations,
"safety_status": safety_status,
"is_safe": True,
}
# ---------------------------------------------------------------------------
# Fallback Node (Safe Degradation)
# ---------------------------------------------------------------------------
_SAFE_MESSAGE = (
"---\n"
"**OncoAgent — Resultado No Concluyente**\n"
"---\n\n"
"## ⚠️ Información no concluyente en las guías provistas.\n\n"
"El sistema no pudo generar una recomendación clínica confiable "
"para este caso por una de las siguientes razones:\n\n"
"1. No se encontró evidencia suficiente en las guías clínicas cargadas.\n"
"2. La recomendación generada no pasó la validación de seguridad.\n"
"3. El caso requiere revisión clínica especializada fuera del alcance "
"de las guías disponibles.\n\n"
"**Acción recomendada:** Consulte con un oncólogo especialista para "
"una evaluación personalizada.\n"
)
def fallback_node(state: AgentState) -> Dict[str, Any]:
"""Generate a safe fallback response when the pipeline cannot produce
a reliable recommendation.
This node is triggered when:
- RAG retrieval yields insufficient relevant documents
- The critic fails after max iterations
- The input is too short or unintelligible
Args:
state: Current LangGraph state.
Returns:
State update with safe fallback response and diagnostic info.
"""
# Determine why we fell back
routing = state.get("routing_decision", "")
rag_count = state.get("rag_retrieval_count", 0)
critic_verdict = state.get("critic_verdict", "")
critic_attempts = state.get("critic_attempts", 0)
reasons = []
if routing == "insufficient":
reasons.append("Input too short or unintelligible for clinical triage.")
if rag_count == 0:
reasons.append("No relevant documents found in clinical guidelines database.")
if critic_verdict == "FAIL" and critic_attempts >= 2:
reasons.append(
f"Recommendation failed safety validation after {critic_attempts} attempts."
)
if not reasons:
reasons.append("Unknown system error — safe fallback triggered.")
fallback_reason = " | ".join(reasons)
logger.warning("Fallback triggered: %s", fallback_reason)
return {
"formatted_recommendation": _SAFE_MESSAGE,
"clinical_recommendation": "Información no concluyente en las guías provistas.",
"confidence_report": {
"tier_used": state.get("selected_tier", 0),
"fallback": True,
"reason": fallback_reason,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
"source_citations": [],
"fallback_reason": fallback_reason,
"safety_status": f"Fallback: {fallback_reason}",
"is_safe": False,
}