bentosmau commited on
Commit
2598bd9
Β·
1 Parent(s): f64f278

Improve response generation by tokenizing input and streaming word by word

Browse files

Update `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

Files changed (2) hide show
  1. chat-app/app.py +19 -8
  2. 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 stream_texto(texto, historial):
22
- CHUNK = 8
23
- for i in range(0, len(texto), CHUNK):
24
- historial[-1]["content"] += texto[i:i+CHUNK]
25
- time.sleep(0.02)
 
 
 
 
 
 
 
 
 
 
 
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 stream_texto(saludo, historial):
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 stream_texto(respuesta, historial):
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 stream_texto(respuesta_personalizada, historial):
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
- texto = mensaje.lower().strip()
 
 
 
 
 
 
 
 
45
  for entrada in RESPUESTAS_PERSONALIZADAS:
46
  for pregunta in entrada.get("preguntas", []):
47
- if pregunta.lower() in texto or texto in pregunta.lower():
48
- return entrada.get("respuesta")
 
 
 
 
 
 
49
  return None
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def generar_explicacion_juego(datos):
52
- nombre = datos.get("nombre", "Este juego")
53
- tema = datos.get("tema", "")
54
  descripcion = datos.get("descripcion", "").strip()
55
- visitas = datos.get("visitas", 0)
56
- creador = datos.get("creador", "")
57
 
58
- tipo = GENEROS_ROBLOX.get(tema, f"un juego de {tema.lower()}" if tema else "un juego")
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
- def extraer_texto_content(content):
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()