morphos / backend /app /rag /traduccion_consulta.py
Jose Salazar
Ajustes varios al engine y al prompt basado en resultados de los evals
bf9f7d1
Raw
History Blame Contribute Delete
7.67 kB
"""Traducción ES→EN de la consulta de recuperación (vocabulario clínico controlado).
La consulta RAG no es texto libre: se arma con nombres de patrones/hallazgos que provienen
de un vocabulario ACOTADO (las 78 alteraciones de `alteraciones.json` + los analitos). Casi
todos son cognados grecolatinos; sólo un puñado de palabras conectivas/modificadoras difiere.
Por eso la traducción es un léxico determinista palabra-a-palabra (sin coste de LLM, auditable),
con paso directo (sin acentos) para los cognados no listados.
Motivación: encoders biomédicos de alto rendimiento (p. ej. MedCPT) son sólo-inglés. Traducir
la consulta a inglés permite evaluarlos, y además empareja mejor consulta↔corpus (inglés) incluso
con bge-m3. El troceo por embeddings/BM25 es en gran medida insensible al orden de palabras, así
que la traducción token-a-token (sin reordenar) es suficiente para recuperar.
"""
from __future__ import annotations
import re
import unicodedata
# Modificadores/conectores y raíces clínicas que NO son cognados limpios. Las claves están
# sin acentos y en minúsculas (así se normaliza el token antes de buscar). Ampliable.
_LEXICO: dict[str, str] = {
# conectores / modificadores
"de": "of", "del": "of", "en": "in", "y": "and", "o": "or", "con": "with", "sin": "without",
"elevada": "elevated", "elevado": "elevated", "elevados": "elevated", "elevadas": "elevated",
"elevacion": "elevation", "aumentada": "increased", "aumentado": "increased",
"disminuida": "decreased", "disminuido": "decreased", "reducida": "reduced", "reducido": "reduced",
"aislada": "isolated", "aislado": "isolated", "nivel": "level", "niveles": "levels",
"bajo": "low", "baja": "low", "alto": "high", "alta": "high", "normal": "normal",
"posible": "possible", "sospecha": "suspicion", "prolongado": "prolonged", "prolongada": "prolonged",
"orina": "urine", "deficit": "deficiency", "hierro": "iron", "serico": "serum", "muy": "very",
"concentracion": "concentration", "ratio": "ratio", "no": "non",
"danio": "damage", "dano": "damage", "patron": "pattern", "toxico": "toxic",
"subterapeutico": "subtherapeutic", "via": "route",
# hematología
"anemia": "anemia", "regenerativa": "regenerative", "regenerativo": "regenerative",
"microcitica": "microcytic", "macrocitica": "macrocytic", "normocitica": "normocytic",
"hipocromica": "hypochromic", "hemorragia": "hemorrhage", "sangrado": "bleeding",
"eritrocitosis": "erythrocytosis", "eritrocitos": "erythrocytes",
"leucocitosis": "leukocytosis", "leucopenia": "leukopenia", "leucocitos": "leukocytes",
"neutrofilica": "neutrophilic", "neutrofilia": "neutrophilia", "neutropenia": "neutropenia",
"linfocitica": "lymphocytic", "linfocitosis": "lymphocytosis", "linfopenia": "lymphopenia",
"linfocitos": "lymphocytes", "eosinofilia": "eosinophilia", "monocitosis": "monocytosis",
"trombocitopenia": "thrombocytopenia", "trombocitosis": "thrombocytosis",
"reticulocitos": "reticulocytes", "reticulocitosis": "reticulocytosis",
# hepático / pancreático
"hepatocelular": "hepatocellular", "colestasico": "cholestatic", "colestasis": "cholestasis",
"hiperbilirrubinemia": "hyperbilirubinemia", "hiperamylasemia": "hyperamylasemia",
"pancreatitis": "pancreatitis", "hepatopatia": "hepatopathy",
# renal / electrolitos
"azotemia": "azotemia", "hiperuremia": "hyperuremia", "creatinina": "creatinine",
"hiperglucemia": "hyperglycemia", "hipoglucemia": "hypoglycemia",
"hiperproteinemia": "hyperproteinemia", "hipoproteinemia": "hypoproteinemia",
"hipoalbuminemia": "hypoalbuminemia", "hiperalbuminemia": "hyperalbuminemia",
"hipercalcemia": "hypercalcemia", "hipocalcemia": "hypocalcemia",
"hipernatremia": "hypernatremia", "hiponatremia": "hyponatremia",
"hiperpotasemia": "hyperkalemia", "hipopotasemia": "hypokalemia",
"hiperfosforemia": "hyperphosphatemia", "hipofosforemia": "hypophosphatemia",
"hipomagnesemia": "hypomagnesemia", "hipermagnesemia": "hypermagnesemia",
"hiperuricemia": "hyperuricemia", "hiposthenuria": "hyposthenuria", "isosthenuria": "isosthenuria",
# endocrino / otros
"hipoadrenocorticismo": "hypoadrenocorticism", "hiperadrenocorticismo": "hyperadrenocorticism",
"hipotiroidismo": "hypothyroidism", "hipertiroidismo": "hyperthyroidism",
"coagulopatia": "coagulopathy", "acidosis": "acidosis", "alcalosis": "alkalosis",
"respiratoria": "respiratory", "metabolica": "metabolic", "ionizada": "ionized",
"fenobarbital": "phenobarbital", "ciclosporina": "cyclosporine", "insulina": "insulin",
"cortisol": "cortisol",
# descriptores clínicos no-cognados presentes en alteraciones.json (evita que pasen sin
# traducir). Cubierto por test_traduccion_consulta::test_cobertura_alteraciones.
"enfermedad": "disease", "aguda": "acute", "agudo": "acute", "agudas": "acute", "agudos": "acute",
"libre": "free", "respuesta": "response", "estado": "state", "capacidad": "capacity",
"diseminada": "disseminated", "diseminado": "disseminated", "intravascular": "intravascular",
"suprimido": "suppressed", "suprimida": "suppressed", "deteriorada": "impaired", "deteriorado": "impaired",
"multiples": "multiple", "multiple": "multiple", "primario": "primary", "primaria": "primary",
"extrinseca": "extrinsic", "extrinseco": "extrinsic", "intrinseca": "intrinsic", "intrinseco": "intrinsic",
"prolongados": "prolonged", "prolongadas": "prolonged", "cardiopatia": "cardiomyopathy",
"miocardico": "myocardial", "potencialmente": "potentially", "protrombotico": "prothrombotic",
"basal": "basal", "hematuria": "hematuria", "piuria": "pyuria", "proteinuria": "proteinuria",
"progesterona": "progesterone", "troponina": "troponin", "antitrombina": "antithrombin",
"hipoxemia": "hypoxemia", "hiperlactatemia": "hyperlactatemia",
"hiperfibrinogenemia": "hyperfibrinogenemia", "hipofibrinogenemia": "hypofibrinogenemia",
"coagulacion": "coagulation",
# Estadiaje IRIS (entidad erc_iris de alteraciones.json)
"escala": "scale", "graduada": "graded", "graduado": "graded", "estadio": "stage",
"estadiaje": "staging", "proteinurico": "proteinuric",
}
# Cognados/proper-nouns/acrónimos que pasan directos sin necesidad de entrada en el léxico
# (los usa el test de cobertura para no exigir traducción explícita de estos).
COGNADOS_PERMITIDOS: frozenset[str] = frozenset({
"anion", "willebrand", "probnp", "addison", "cushing",
})
def _sin_acentos(texto: str) -> str:
return "".join(c for c in unicodedata.normalize("NFD", texto) if unicodedata.category(c) != "Mn")
def _traducir_token(token: str) -> str:
"""Traduce un token conservando puntuación adyacente; cae a paso directo sin acentos."""
m = re.match(r"^(\W*)(.*?)(\W*)$", token, re.DOTALL)
pre, nucleo, post = m.group(1), m.group(2), m.group(3)
if not nucleo:
return token
clave = _sin_acentos(nucleo).lower()
traducido = _LEXICO.get(clave)
if traducido is None:
# Cognato no listado (anemia, azotemia…): paso directo sin acentos.
traducido = _sin_acentos(nucleo)
if nucleo.isupper():
traducido = traducido.upper()
return f"{pre}{traducido}{post}"
def traducir_consulta(consulta: str, idioma_destino: str = "en") -> str:
"""Traduce la consulta al idioma destino. Sólo 'en' está soportado; cualquier otro
valor (incl. 'es') devuelve la consulta intacta."""
if idioma_destino != "en" or not consulta.strip():
return consulta
return " ".join(_traducir_token(t) for t in consulta.split(" "))