Spaces:
Running
Running
| """ | |
| 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, | |
| } | |