#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
02 — Buscar por significado, no por palabra
============================================

Episodio «¿Cómo se le da memoria a una IA?» — Punto 3 del guion,
«Lo que dijiste, lo que pasó y cuándo» (acto II — Los hechos y el retrato).

La idea del episodio
--------------------
Cuando le preguntas algo, el asistente no busca el libro que se titula
literalmente como tu pregunta. Busca **ideas**. Si tú preguntas por «coche» y
la ficha dice «automóvil», un buscador normal no lo encuentra; este sí. En el
mundo real, el sistema traduce el texto a números (lo que llaman *búsqueda
semántica* o RAG) y compara esos números. Aquí no vamos a meter un modelo: lo
imitamos con un diccionario de conceptos hecho a mano, para que se vea la
diferencia sin instalar nada.

Qué hace este ejemplo
---------------------
  1. Compara un **buscador por palabra** (cuenta letras en común) con un
     **buscador por significado** (compara conceptos). Misma pregunta, mismos
     recuerdos, resultados distintos.
  2. Busca «quiero algo sobre el miedo a hablar en público» y enseña cómo el
     buscador de significado trae la ficha que habla de «ansiedad antes de una
     presentación», aunque no compartan ni una palabra.
  3. De propina, reproduce el caso «coche / automóvil».

No necesita nada instalado (solo Python 3).

Uso:
    python3 02-buscar-por-significado.py
"""

import math
import re
import unicodedata


# ─────────────────────────────────────────────────────────────────────────────
# El "traductor a significado" de juguete.
#
# En un sistema real, un modelo convierte cada texto en una lista de números
# (un vector) de forma que los textos que hablan de lo mismo acaban con números
# parecidos. Aquí no hay modelo: definimos a mano unos cuantos CONCEPTOS y
# decimos qué palabras activan cada uno. Es una maqueta, pero enseña la idea.
# ─────────────────────────────────────────────────────────────────────────────
CONCEPTOS = {
    "miedo_escenario": [
        "miedo",
        "panico",
        "nervios",
        "ansiedad",
        "presentacion",
        "presentar",
        "publico",
        "escenario",
        "hablar en publico",
        "audiencia",
        "examen",
    ],
    "vehiculo": [
        "coche",
        "automovil",
        "carro",
        "turismo",
        "vehiculo",
        "conducir",
        "volante",
    ],
    "comida": [
        "comida",
        "cena",
        "receta",
        "cocina",
        "carbonara",
        "restaurante",
        "ingrediente",
    ],
    "salud": ["dentista", "medico", "salud", "diente", "muela", "consulta", "revision"],
    "tecnologia": [
        "disco",
        "duro",
        "ordenador",
        "backup",
        "copia",
        "linux",
        "programa",
    ],
}


# Palabras vacías del castellano: las que aparecen en todas partes y no
# significan nada por sí solas. Un buscador serio las ignora; si no, cualquier
# pregunta haría "coincidir" con cualquier recuerdo solo por compartir un «el».
STOPWORDS = {
    "el",
    "la",
    "los",
    "las",
    "un",
    "una",
    "unos",
    "unas",
    "de",
    "del",
    "a",
    "al",
    "en",
    "y",
    "o",
    "u",
    "que",
    "por",
    "para",
    "con",
    "sin",
    "se",
    "su",
    "sus",
    "mi",
    "mis",
    "me",
    "te",
    "tu",
    "tus",
    "lo",
    "es",
    "son",
    "fue",
    "fui",
    "soy",
    "era",
}


def normalizar(texto):
    """Minúsculas y sin acentos, para que 'público' y 'publico' se encuentren."""
    texto = unicodedata.normalize("NFKD", texto.lower())
    return "".join(c for c in texto if not unicodedata.combining(c))


def palabras_con_peso(texto):
    """Trocea un texto en palabras con significado, tirando las vacías (stopwords)."""
    todas = re.findall(r"\w+", normalizar(texto))
    return {p for p in todas if p not in STOPWORDS}


def pluralizar(n, singular, plural):
    """'1 palabra' / 'N palabras'."""
    return f"{n} {singular if n == 1 else plural}"


def vector_de_conceptos(texto):
    """Convierte un texto en una pequeña lista de números: cuánto toca cada concepto."""
    limpio = normalizar(texto)
    vector = []
    for palabras in CONCEPTOS.values():
        cuenta = sum(1 for p in palabras if p in limpio)
        vector.append(float(cuenta))
    return vector


def similitud_coseno(a, b):
    """Cuánto se parecen dos listas de números. Va de 0 (nada) a 1 (idénticas)."""
    producto = sum(x * y for x, y in zip(a, b))
    norma_a = math.sqrt(sum(x * x for x in a))
    norma_b = math.sqrt(sum(y * y for y in b))
    if norma_a == 0 or norma_b == 0:
        return 0.0
    return producto / (norma_a * norma_b)


def buscar_por_palabra(pregunta, recuerdos):
    """Buscador clásico: cuenta cuántas palabras CON SIGNIFICADO comparten."""
    palabras_pregunta = palabras_con_peso(pregunta)
    puntuaciones = []
    for r in recuerdos:
        comunes = palabras_pregunta & palabras_con_peso(r)
        puntuaciones.append(len(comunes))
    return puntuaciones


def buscar_por_significado(pregunta, recuerdos):
    """Buscador moderno: compara el 'significado' (los conceptos) de cada texto."""
    v_pregunta = vector_de_conceptos(pregunta)
    return [similitud_coseno(v_pregunta, vector_de_conceptos(r)) for r in recuerdos]


def batalla(pregunta, recuerdos):
    print("=" * 68)
    print("  BUSCAR POR SIGNIFICADO, NO POR PALABRA")
    print("=" * 68)
    print(f"\n  Pregunta: «{pregunta}»\n")
    print("  Recuerdos disponibles:")
    for i, r in enumerate(recuerdos, 1):
        print(f"    {i}. {r}")

    por_palabra = buscar_por_palabra(pregunta, recuerdos)
    por_significado = buscar_por_significado(pregunta, recuerdos)

    mejor_palabra = max(range(len(recuerdos)), key=lambda i: por_palabra[i])
    mejor_sig = max(range(len(recuerdos)), key=lambda i: por_significado[i])

    print("\n  ── Buscador por PALABRA ─────────────────────────────────────")
    if max(por_palabra) == 0:
        print("    ✗ No encuentra nada: ningún recuerdo comparte palabras con")
        print("      la pregunta. Aquí se queda sin respuesta.")
    else:
        for i, r in enumerate(recuerdos):
            marca = "  ← el primero" if i == mejor_palabra else ""
            etiqueta = pluralizar(
                por_palabra[i], "palabra en común", "palabras en común"
            )
            print(f"    {etiqueta} · {r}{marca}")

    print("\n  ── Buscador por SIGNIFICADO ─────────────────────────────────")
    for i, r in enumerate(recuerdos):
        marca = "  ← el primero" if i == mejor_sig else ""
        print(f"    {por_significado[i]:.2f} de parecido  · {r}{marca}")

    print("\n  Fíjate: el buscador de significado trae la ficha que HABLA DE LO")
    print("  MISMO aunque no comparta ni una palabra con tu pregunta. Eso es")
    print("  lo que hace que el asistente parezca que te recuerda.\n")


def main():
    # Caso 1: el ejemplo estrella del episodio.
    batalla(
        "quiero algo sobre el miedo a hablar en público",
        [
            "Ansiedad antes de una presentación y consejos para respirar.",
            "Receta de carbonara para la cena del sábado.",
            "El martes fui al dentista por la mañana.",
        ],
    )

    # Caso 2: el «coche / automóvil» que cuenta el episodio.
    batalla(
        "¿qué te conté del coche?",
        [
            "Hablamos de comprar un automóvil de segunda mano.",
            "Ayer preparé una carbonara bastante decente.",
        ],
    )

    print("  Nota: en Valet esto es de verdad, con un modelo que traduce el")
    print("  texto a números. Aquí es una maqueta para que se vea la idea.\n")


if __name__ == "__main__":
    main()
