medgraph-engine/api/routers/comprehensive.py

278 lines
10 KiB
Python

"""Endpoint comprehensive: todo sobre un tema en un solo call."""
from fastapi import APIRouter, Query
from services import graph, vector
router = APIRouter(tags=["comprehensive"])
@router.get("/topic/{tema}/comprehensive")
async def topic_comprehensive(
tema: str,
top_k: int = Query(default=20, description="Chunks de bibliografia a devolver"),
):
"""Devuelve TODO sobre un tema: grafo + actividades + material + bibliografia.
Orquesta multiples consultas internas y devuelve un paquete completo
para que el LLM no necesite hacer multiples calls.
"""
result = {
"tema": tema,
"grafo": {},
"actividades": [],
"material": [],
"bibliografia": [],
}
# 1. Buscar en el grafo semantico (nodos + relaciones)
# Buscar como Tema
topic_detail = graph.get_topic_detail(tema)
if topic_detail:
result["grafo"]["tema"] = topic_detail
# Buscar como Patologia
patologias = graph.get_pathology(tema)
if patologias:
result["grafo"]["patologias"] = patologias
# Buscar como Procedimiento
procedimientos = graph.get_procedure(tema)
if procedimientos:
result["grafo"]["procedimientos"] = procedimientos
# Buscar entidades Dev (extraidas por LLM)
entidades_dev = _search_dev_entities(tema)
if entidades_dev:
result["grafo"]["entidades_extraidas"] = entidades_dev
# 2. Buscar actividades relacionadas (TPs, seminarios, talleres)
# Buscar con el tema original + sinónimos comunes
search_terms = [tema]
# Agregar sinónimos de entidades encontradas
for ent in entidades_dev:
if ent.get("sinonimos"):
search_terms.extend(ent["sinonimos"][:3])
actividades = []
seen_ids = set()
for term in search_terms:
results_act = graph.get_activity(term)
for a in results_act:
if a.get("id") and a["id"] not in seen_ids:
seen_ids.add(a["id"])
actividades.append(a)
if actividades:
result["actividades"] = actividades
# Para cada actividad, traer su material
for act in actividades:
if act.get("id"):
material = graph.get_activity_material(act["id"])
if material:
for doc in material:
doc["actividad"] = act["nombre"]
result["material"].extend(material)
# 3. Buscar chunks de bibliografia (hybrid search)
try:
search_result = vector.search_hybrid(tema, top_k=top_k)
if search_result.get("results"):
result["bibliografia"] = [
{
"libro": r.get("libro"),
"paginas": f"{r.get('pag_inicio', '?')}-{r.get('pag_fin', '?')}",
"capitulo": r.get("capitulo", ""),
"seccion": r.get("seccion", ""),
"tipo": r.get("tipo", ""),
"texto": r.get("texto", ""),
"score": r.get("rrf_score", 0),
}
for r in search_result["results"]
]
result["busqueda"] = {
"intencion": search_result.get("intencion", ""),
"query_expandida": search_result.get("query_expandida", ""),
}
except Exception:
pass
# 4. Ontología (ATC + SNOMED)
ontologia = {"farmacos_atc": [], "patologias_snomed": [], "query_cruzada": []}
try:
# Fármacos → ATC
farmacos_atc = graph.query("""
MATCH (f:Farmaco)-[:ES_UN*1..5]->(cat:CategoriaATC)
WHERE toLower(f.nombre) CONTAINS toLower($tema)
WITH f.nombre AS farmaco, collect(DISTINCT {codigo: cat.codigo, nombre: cat.nombre, nivel: cat.nivel}) AS jerarquia
RETURN farmaco, jerarquia
ORDER BY farmaco LIMIT 10
""", {"tema": tema})
for r in farmacos_atc:
ontologia["farmacos_atc"].append({
"nombre": r["farmaco"],
"jerarquia": sorted(r["jerarquia"], key=lambda x: x.get("nivel", 0))
})
# Patologías → SNOMED
pato_snomed = graph.query("""
MATCH (p:Patologia)-[:ES_UN*1..3]->(cat:CategoriaSNOMED)
WHERE toLower(p.nombre) CONTAINS toLower($tema)
WITH p.nombre AS patologia, collect(DISTINCT {nombre: cat.nombre, sistema: cat.sistema, nivel: cat.nivel}) AS jerarquia
RETURN patologia, jerarquia
ORDER BY patologia LIMIT 10
""", {"tema": tema})
for r in pato_snomed:
ontologia["patologias_snomed"].append({
"nombre": r["patologia"],
"jerarquia": sorted(r["jerarquia"], key=lambda x: x.get("nivel", 0))
})
# Query cruzada
if ontologia["patologias_snomed"]:
sistemas = set()
for p in ontologia["patologias_snomed"]:
for j in p["jerarquia"]:
if j.get("sistema"):
sistemas.add(j["sistema"])
for sistema in list(sistemas)[:2]:
cruzada = graph.query("""
MATCH (p:Patologia)-[:ES_UN*1..3]->(sno:CategoriaSNOMED {sistema: $sistema})
WHERE toLower(p.nombre) CONTAINS toLower($tema)
MATCH (p)-[:SE_TRATA_CON]->(f:Farmaco)
OPTIONAL MATCH (f)-[:ES_UN*1..5]->(atc:CategoriaATC)
RETURN DISTINCT f.nombre AS farmaco, p.nombre AS patologia,
collect(DISTINCT atc.nombre)[0] AS clase_atc, $sistema AS sistema
LIMIT 10
""", {"tema": tema, "sistema": sistema})
for r in cruzada:
ontologia["query_cruzada"].append({
"farmaco": r["farmaco"],
"patologia": r["patologia"],
"clase_atc": r.get("clase_atc"),
"sistema_snomed": r["sistema"]
})
except Exception:
pass
result["ontologia"] = ontologia
# 5. Resumen de fuentes encontradas
fuentes = set()
for bib in result["bibliografia"]:
if bib.get("libro"):
fuentes.add(bib["libro"])
result["fuentes"] = list(fuentes)
# 6. Flag de completitud
result["tiene_grafo"] = bool(result["grafo"])
result["tiene_actividades"] = bool(result["actividades"])
result["tiene_material"] = bool(result["material"])
result["tiene_bibliografia"] = bool(result["bibliografia"])
result["tiene_ontologia"] = bool(ontologia["farmacos_atc"] or ontologia["patologias_snomed"])
return result
@router.get("/topic/{tema}/pathways")
async def topic_pathways(tema: str):
"""Cadena causal/fisiopatologica: agente -> mecanismo -> efecto -> signo -> complicacion."""
pathways = graph.query("""
MATCH (a)-[r:PATHWAY]->(b)
WHERE toLower(a.nombre) CONTAINS toLower($tema)
OR toLower(b.nombre) CONTAINS toLower($tema)
OR r.nombre_dag CONTAINS toLower($tema)
RETURN DISTINCT r.nombre_dag AS dag, a.nombre AS desde, labels(a)[0] AS tipo_desde,
b.nombre AS hasta, labels(b)[0] AS tipo_hasta,
r.orden AS orden, r.tipo AS tipo_paso, r.nota AS nota
ORDER BY r.nombre_dag, r.orden
""", {"tema": tema})
dags = {}
for p in pathways:
dag_name = p["dag"]
if dag_name not in dags:
dags[dag_name] = {"nombre": dag_name, "pasos": []}
dags[dag_name]["pasos"].append({
"orden": p["orden"],
"desde": p["desde"],
"tipo_desde": p["tipo_desde"],
"hasta": p["hasta"],
"tipo_hasta": p["tipo_hasta"],
"tipo_paso": p.get("tipo_paso"),
"nota": p.get("nota"),
})
return {"tema": tema, "pathways": list(dags.values()), "count": len(dags)}
@router.get("/topic/{tema}/clinical")
async def topic_clinical(tema: str):
"""Arbol de decision clinica con bifurcaciones y condiciones."""
steps = graph.query("""
MATCH (a)-[r:CLINICAL]->(b)
WHERE toLower(a.nombre) CONTAINS toLower($tema)
OR toLower(b.nombre) CONTAINS toLower($tema)
OR r.nombre_dag CONTAINS toLower($tema)
RETURN DISTINCT r.nombre_dag AS dag, a.nombre AS desde, labels(a)[0] AS tipo_desde,
b.nombre AS hasta, labels(b)[0] AS tipo_hasta,
r.orden AS orden, r.tipo_paso AS tipo_paso,
r.condicion AS condicion, r.nota AS nota
ORDER BY r.nombre_dag, r.orden, r.condicion
""", {"tema": tema})
dags = {}
for s in steps:
dag_name = s["dag"]
if dag_name not in dags:
dags[dag_name] = {"nombre": dag_name, "pasos": []}
dags[dag_name]["pasos"].append({
"orden": s["orden"],
"desde": s["desde"],
"tipo_desde": s["tipo_desde"],
"hasta": s["hasta"],
"tipo_hasta": s["tipo_hasta"],
"tipo_paso": s.get("tipo_paso"),
"condicion": s.get("condicion"),
"nota": s.get("nota"),
})
return {"tema": tema, "clinical": list(dags.values()), "count": len(dags)}
def _search_dev_entities(tema: str) -> list:
"""Busca entidades extraidas (Dev) relacionadas con el tema."""
# Buscar en todos los labels Dev
dev_labels = [
"Patologia", "EstructuraAnatomica", "Procedimiento",
"Farmaco", "Agente", "Signo", "Sintoma",
"MetodoDx", "Hallazgo", "GrupoFarmacologico",
"PatologiaDev", "EstructuraAnatomicaDev", "ProcedimientoDev",
"FarmacoDev", "AgenteDev", "SignoDev", "SintomaDev",
"MetodoDxDev", "HallazgoDev", "GrupoFarmacologicoDev",
]
results = []
for label in dev_labels:
try:
matches = graph.query(f"""
MATCH (e:{label})
WHERE toLower(e.nombre) CONTAINS toLower($tema)
OPTIONAL MATCH (e)-[r]-(related)
WHERE NOT related:Chunk
RETURN e.nombre AS nombre, labels(e)[0] AS tipo,
e.sinonimos AS sinonimos, e.freq AS freq,
collect(DISTINCT {{
nombre: related.nombre,
tipo: labels(related)[0],
relacion: type(r)
}})[0..10] AS relaciones
LIMIT 5
""", {"tema": tema})
results.extend(matches)
except Exception:
pass
return results