bentosmau commited on
Commit Β·
2598bd9
1
Parent(s): f64f278
Improve response generation by tokenizing input and streaming word by word
Browse filesUpdate `logica.py` to tokenize user input and use token-based similarity for matching responses. Modify `app.py` to stream responses token by token, simulating a real AI model's output.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e3ff2484-bbd8-4aba-bea0-1940769b874a
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 38e4ae2b-4ea5-4fa5-a9cf-327acce0f548
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1739408b-93a5-479b-a658-30f2493b0467/e3ff2484-bbd8-4aba-bea0-1940769b874a/vyYjoCT
Replit-Helium-Checkpoint-Created: true
- chat-app/app.py +19 -8
- chat-app/logica.py +93 -31
chat-app/app.py
CHANGED
|
@@ -18,11 +18,22 @@ def aΓ±adir_turno(historial, user_msg, bot_msg=""):
|
|
| 18 |
{"role": "assistant", "content": bot_msg},
|
| 19 |
]
|
| 20 |
|
| 21 |
-
def
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
yield historial
|
| 27 |
|
| 28 |
def responder(mensaje, historial):
|
|
@@ -40,7 +51,7 @@ def responder(mensaje, historial):
|
|
| 40 |
"_Escribe tu operaciΓ³n o di 'salir de calculadora' para volver._"
|
| 41 |
)
|
| 42 |
historial = aΓ±adir_turno(historial, mensaje)
|
| 43 |
-
for h in
|
| 44 |
yield h, ""
|
| 45 |
return
|
| 46 |
|
|
@@ -54,7 +65,7 @@ def responder(mensaje, historial):
|
|
| 54 |
if resultado is not None:
|
| 55 |
respuesta = formatear_resultado(mensaje, resultado)
|
| 56 |
historial = aΓ±adir_turno(historial, mensaje)
|
| 57 |
-
for h in
|
| 58 |
yield h, ""
|
| 59 |
return
|
| 60 |
|
|
@@ -84,7 +95,7 @@ def responder(mensaje, historial):
|
|
| 84 |
respuesta_personalizada = buscar_respuesta_personalizada(mensaje)
|
| 85 |
if respuesta_personalizada:
|
| 86 |
historial = aΓ±adir_turno(historial, mensaje)
|
| 87 |
-
for h in
|
| 88 |
yield h, ""
|
| 89 |
return
|
| 90 |
|
|
|
|
| 18 |
{"role": "assistant", "content": bot_msg},
|
| 19 |
]
|
| 20 |
|
| 21 |
+
def stream_tokens(texto, historial):
|
| 22 |
+
"""
|
| 23 |
+
Emite la respuesta token por token (palabra por palabra),
|
| 24 |
+
simulando el proceso de generaciΓ³n de un modelo de lenguaje real.
|
| 25 |
+
Tokens largos = pausas ligeramente mayores (mΓ‘s "peso" semΓ‘ntico).
|
| 26 |
+
"""
|
| 27 |
+
tokens = texto.split(" ")
|
| 28 |
+
acumulado = ""
|
| 29 |
+
for i, token in enumerate(tokens):
|
| 30 |
+
acumulado += token
|
| 31 |
+
if i < len(tokens) - 1:
|
| 32 |
+
acumulado += " "
|
| 33 |
+
historial[-1]["content"] = acumulado
|
| 34 |
+
# Pausa variable: tokens largos tardan un poco mΓ‘s (como un LLM real)
|
| 35 |
+
delay = 0.055 if len(token) > 4 else 0.030
|
| 36 |
+
time.sleep(delay)
|
| 37 |
yield historial
|
| 38 |
|
| 39 |
def responder(mensaje, historial):
|
|
|
|
| 51 |
"_Escribe tu operaciΓ³n o di 'salir de calculadora' para volver._"
|
| 52 |
)
|
| 53 |
historial = aΓ±adir_turno(historial, mensaje)
|
| 54 |
+
for h in stream_tokens(saludo, historial):
|
| 55 |
yield h, ""
|
| 56 |
return
|
| 57 |
|
|
|
|
| 65 |
if resultado is not None:
|
| 66 |
respuesta = formatear_resultado(mensaje, resultado)
|
| 67 |
historial = aΓ±adir_turno(historial, mensaje)
|
| 68 |
+
for h in stream_tokens(respuesta, historial):
|
| 69 |
yield h, ""
|
| 70 |
return
|
| 71 |
|
|
|
|
| 95 |
respuesta_personalizada = buscar_respuesta_personalizada(mensaje)
|
| 96 |
if respuesta_personalizada:
|
| 97 |
historial = aΓ±adir_turno(historial, mensaje)
|
| 98 |
+
for h in stream_tokens(respuesta_personalizada, historial):
|
| 99 |
yield h, ""
|
| 100 |
return
|
| 101 |
|
chat-app/logica.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import json
|
|
|
|
| 4 |
from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
|
| 5 |
from matematicas import (
|
| 6 |
es_solicitud_calculadora, es_operacion_matematica,
|
|
@@ -40,22 +41,105 @@ def cargar_respuestas():
|
|
| 40 |
|
| 41 |
RESPUESTAS_PERSONALIZADAS = cargar_respuestas()
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
def buscar_respuesta_personalizada(mensaje):
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
for entrada in RESPUESTAS_PERSONALIZADAS:
|
| 46 |
for pregunta in entrada.get("preguntas", []):
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return None
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def generar_explicacion_juego(datos):
|
| 52 |
-
nombre
|
| 53 |
-
tema
|
| 54 |
descripcion = datos.get("descripcion", "").strip()
|
| 55 |
-
visitas
|
| 56 |
-
creador
|
| 57 |
|
| 58 |
-
tipo
|
| 59 |
partes = [f"**{nombre}** es {tipo} de Roblox creado por **{creador}**."]
|
| 60 |
|
| 61 |
if descripcion and descripcion != "Sin descripciΓ³n.":
|
|
@@ -110,29 +194,7 @@ def detectar_roblox(mensaje):
|
|
| 110 |
return "juego", m.group(1).strip()
|
| 111 |
return None, None
|
| 112 |
|
| 113 |
-
|
| 114 |
-
if not content:
|
| 115 |
-
return ""
|
| 116 |
-
if isinstance(content, str):
|
| 117 |
-
return content
|
| 118 |
-
if isinstance(content, list):
|
| 119 |
-
partes = []
|
| 120 |
-
for bloque in content:
|
| 121 |
-
if isinstance(bloque, str):
|
| 122 |
-
partes.append(bloque)
|
| 123 |
-
elif isinstance(bloque, dict):
|
| 124 |
-
partes.append(str(bloque.get("text") or bloque.get("value") or bloque.get("content") or ""))
|
| 125 |
-
return " ".join(partes)
|
| 126 |
-
return str(content)
|
| 127 |
-
|
| 128 |
-
def modo_calculadora_activo(historial):
|
| 129 |
-
if not historial:
|
| 130 |
-
return False
|
| 131 |
-
for msg in reversed(historial):
|
| 132 |
-
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 133 |
-
texto = extraer_texto_content(msg.get("content")).lower()
|
| 134 |
-
return "calculadora neo-1" in texto or "aquΓ tienes nuestra calculadora" in texto
|
| 135 |
-
return False
|
| 136 |
|
| 137 |
def respuesta_final(mensaje, historial):
|
| 138 |
texto = mensaje.strip().lower()
|
|
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import json
|
| 4 |
+
import unicodedata
|
| 5 |
from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
|
| 6 |
from matematicas import (
|
| 7 |
es_solicitud_calculadora, es_operacion_matematica,
|
|
|
|
| 41 |
|
| 42 |
RESPUESTAS_PERSONALIZADAS = cargar_respuestas()
|
| 43 |
|
| 44 |
+
# ββ TOKENIZADOR ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
+
|
| 46 |
+
def tokenizar(texto):
|
| 47 |
+
"""
|
| 48 |
+
Convierte texto a lista de tokens normalizados:
|
| 49 |
+
1. Elimina tildes/acentos
|
| 50 |
+
2. Convierte a minΓΊsculas
|
| 51 |
+
3. Elimina puntuaciΓ³n
|
| 52 |
+
4. Divide en palabras (tokens)
|
| 53 |
+
"""
|
| 54 |
+
# 1. Normalizar unicode β eliminar acentos
|
| 55 |
+
texto = unicodedata.normalize("NFD", texto)
|
| 56 |
+
texto = "".join(c for c in texto if unicodedata.category(c) != "Mn")
|
| 57 |
+
# 2. MinΓΊsculas
|
| 58 |
+
texto = texto.lower()
|
| 59 |
+
# 3. Eliminar puntuaciΓ³n (conservar letras, nΓΊmeros y espacios)
|
| 60 |
+
texto = re.sub(r"[^\w\s]", " ", texto)
|
| 61 |
+
# 4. Dividir en tokens
|
| 62 |
+
return [t for t in texto.split() if t]
|
| 63 |
+
|
| 64 |
+
def similitud_tokens(tokens_entrada, tokens_patron):
|
| 65 |
+
"""
|
| 66 |
+
Calcula similitud Jaccard entre dos listas de tokens.
|
| 67 |
+
TambiΓ©n da bonus si el patrΓ³n es subconjunto del mensaje.
|
| 68 |
+
Retorna un score entre 0.0 y 1.0
|
| 69 |
+
"""
|
| 70 |
+
set_entrada = set(tokens_entrada)
|
| 71 |
+
set_patron = set(tokens_patron)
|
| 72 |
+
if not set_patron:
|
| 73 |
+
return 0.0
|
| 74 |
+
|
| 75 |
+
interseccion = set_entrada & set_patron
|
| 76 |
+
union = set_entrada | set_patron
|
| 77 |
+
jaccard = len(interseccion) / len(union)
|
| 78 |
+
|
| 79 |
+
# Bonus: si todos los tokens del patrΓ³n estΓ‘n en la entrada
|
| 80 |
+
if set_patron.issubset(set_entrada):
|
| 81 |
+
jaccard = max(jaccard, 0.80)
|
| 82 |
+
|
| 83 |
+
return jaccard
|
| 84 |
+
|
| 85 |
def buscar_respuesta_personalizada(mensaje):
|
| 86 |
+
"""
|
| 87 |
+
Busca la mejor respuesta usando similitud de tokens.
|
| 88 |
+
Umbral mΓnimo: 0.20 (al menos 20% de overlap Jaccard)
|
| 89 |
+
"""
|
| 90 |
+
tokens_entrada = tokenizar(mensaje)
|
| 91 |
+
mejor_respuesta = None
|
| 92 |
+
mejor_score = 0.0
|
| 93 |
+
UMBRAL = 0.20
|
| 94 |
+
|
| 95 |
for entrada in RESPUESTAS_PERSONALIZADAS:
|
| 96 |
for pregunta in entrada.get("preguntas", []):
|
| 97 |
+
tokens_patron = tokenizar(pregunta)
|
| 98 |
+
score = similitud_tokens(tokens_entrada, tokens_patron)
|
| 99 |
+
if score > mejor_score:
|
| 100 |
+
mejor_score = score
|
| 101 |
+
mejor_respuesta = entrada.get("respuesta")
|
| 102 |
+
|
| 103 |
+
if mejor_score >= UMBRAL:
|
| 104 |
+
return mejor_respuesta
|
| 105 |
return None
|
| 106 |
|
| 107 |
+
# ββ UTILIDADES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
+
|
| 109 |
+
def extraer_texto_content(content):
|
| 110 |
+
if not content:
|
| 111 |
+
return ""
|
| 112 |
+
if isinstance(content, str):
|
| 113 |
+
return content
|
| 114 |
+
if isinstance(content, list):
|
| 115 |
+
partes = []
|
| 116 |
+
for bloque in content:
|
| 117 |
+
if isinstance(bloque, str):
|
| 118 |
+
partes.append(bloque)
|
| 119 |
+
elif isinstance(bloque, dict):
|
| 120 |
+
partes.append(str(bloque.get("text") or bloque.get("value") or bloque.get("content") or ""))
|
| 121 |
+
return " ".join(partes)
|
| 122 |
+
return str(content)
|
| 123 |
+
|
| 124 |
+
def modo_calculadora_activo(historial):
|
| 125 |
+
if not historial:
|
| 126 |
+
return False
|
| 127 |
+
for msg in reversed(historial):
|
| 128 |
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 129 |
+
texto = extraer_texto_content(msg.get("content")).lower()
|
| 130 |
+
return "calculadora neo-1" in texto or "aquΓ tienes nuestra calculadora" in texto
|
| 131 |
+
return False
|
| 132 |
+
|
| 133 |
+
# ββ ROBLOX βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
+
|
| 135 |
def generar_explicacion_juego(datos):
|
| 136 |
+
nombre = datos.get("nombre", "Este juego")
|
| 137 |
+
tema = datos.get("tema", "")
|
| 138 |
descripcion = datos.get("descripcion", "").strip()
|
| 139 |
+
visitas = datos.get("visitas", 0)
|
| 140 |
+
creador = datos.get("creador", "")
|
| 141 |
|
| 142 |
+
tipo = GENEROS_ROBLOX.get(tema, f"un juego de {tema.lower()}" if tema else "un juego")
|
| 143 |
partes = [f"**{nombre}** es {tipo} de Roblox creado por **{creador}**."]
|
| 144 |
|
| 145 |
if descripcion and descripcion != "Sin descripciΓ³n.":
|
|
|
|
| 194 |
return "juego", m.group(1).strip()
|
| 195 |
return None, None
|
| 196 |
|
| 197 |
+
# ββ RESPUESTA FINAL (no-streaming, usada por neo_rest.py) βββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
def respuesta_final(mensaje, historial):
|
| 200 |
texto = mensaje.strip().lower()
|