teszenofficial commited on
Commit
fd07f17
·
verified ·
1 Parent(s): 686e418

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -828
app.py CHANGED
@@ -1,11 +1,8 @@
1
  import os
2
- import sys
3
  import torch
4
  import json
5
- import time
6
- import gc
7
  import re
8
- from fastapi import FastAPI, Request
9
  from fastapi.responses import HTMLResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from pydantic import BaseModel, Field
@@ -16,26 +13,13 @@ import torch.nn as nn
16
  import torch.nn.functional as F
17
  import sentencepiece as spm
18
 
19
- # ======================
20
- # CONFIGURACIÓN DE DISPOSITIVO
21
- # ======================
22
- if torch.cuda.is_available():
23
- DEVICE = "cuda"
24
- print("✅ GPU NVIDIA detectada. Usando CUDA.")
25
- else:
26
- DEVICE = "cpu"
27
- print("⚠️ GPU no detectada. Usando CPU (puede ser más lento).")
28
-
29
- if DEVICE == "cpu":
30
- torch.set_num_threads(max(1, os.cpu_count() // 2))
31
-
32
- torch.set_grad_enabled(False)
33
 
34
  MODEL_REPO = "TeszenAI/MTP-3"
35
 
36
- # ======================
37
- # ARQUITECTURA DEL MODELO MEJORADA
38
- # ======================
39
  class LayerNorm(nn.Module):
40
  def __init__(self, d_model: int, eps: float = 1e-5):
41
  super().__init__()
@@ -114,8 +98,8 @@ class PositionalEncoding(nn.Module):
114
  return x + self.pe[:, :x.size(1), :]
115
 
116
  class MTPModel(nn.Module):
117
- def __init__(self, vocab_size: int, d_model: int = 256, n_heads: int = 8,
118
- n_layers: int = 6, d_ff: int = 1024, dropout: float = 0.1, max_len: int = 512):
119
  super().__init__()
120
  self.vocab_size = vocab_size
121
  self.d_model = d_model
@@ -125,7 +109,6 @@ class MTPModel(nn.Module):
125
  self.blocks = nn.ModuleList([TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)])
126
  self.norm = LayerNorm(d_model)
127
  self.lm_head = nn.Linear(d_model, vocab_size)
128
-
129
  def forward(self, x, mask=None):
130
  if mask is None:
131
  mask = torch.tril(torch.ones(x.size(1), x.size(1))).unsqueeze(0).unsqueeze(0).to(x.device)
@@ -136,891 +119,214 @@ class MTPModel(nn.Module):
136
  x = self.norm(x)
137
  return self.lm_head(x)
138
 
139
- # ======================
140
- # NLP UTILITIES - PROCESAMIENTO DE LENGUAJE NATURAL
141
- # ======================
142
- class NLPProcessor:
143
- """Procesador de lenguaje natural para entender mejor las intenciones"""
144
-
145
- @staticmethod
146
- def detect_intent(text):
147
- """Detecta la intención del usuario"""
148
- text_lower = text.lower()
149
-
150
- intents = {
151
- 'saludo': ['hola', 'buenas', 'que tal', 'cómo estás', 'hey', 'saludos'],
152
- 'despedida': ['adiós', 'chao', 'hasta luego', 'nos vemos', 'bye'],
153
- 'agradecimiento': ['gracias', 'gracias por', 'te agradezco', 'muchas gracias'],
154
- 'pregunta': ['qué es', 'cómo funciona', 'por qué', 'cuándo', 'dónde', 'quién'],
155
- 'ayuda': ['ayuda', 'necesito ayuda', 'puedes ayudarme', 'me ayudas'],
156
- 'presentacion': ['quién eres', 'qué eres', 'presentate', 'eres'],
157
- 'capacidad': ['qué puedes hacer', 'funciones', 'capacidades', 'que sabes hacer'],
158
- 'sentimiento': ['estoy triste', 'estoy feliz', 'me siento', 'emocionado']
159
- }
160
-
161
- for intent, keywords in intents.items():
162
- for keyword in keywords:
163
- if keyword in text_lower:
164
- return intent
165
- return 'general'
166
-
167
- @staticmethod
168
- def should_stop(response, min_length=30, max_length=200):
169
- """Determina si la respuesta debe terminar"""
170
-
171
- # Palabras que indican final de respuesta
172
- stop_phrases = [
173
- '¿alguna otra pregunta?', '¿en qué más puedo ayudarte?',
174
- '¿necesitas ayuda con algo más?', '¿tienes alguna otra duda?',
175
- 'espero haberte ayudado', 'que tengas un buen día',
176
- 'hasta luego', 'adiós', 'saludos', 'gracias por consultar'
177
- ]
178
-
179
- # Si es demasiado corta, continuar
180
- if len(response) < min_length:
181
- return False
182
-
183
- # Si excede el máximo, cortar
184
- if len(response) > max_length:
185
- return True
186
-
187
- # Verificar frases de parada
188
- for phrase in stop_phrases:
189
- if phrase in response.lower():
190
- return True
191
-
192
- # Verificar si termina con puntuación adecuada
193
- if len(response) > 50:
194
- last_chars = response[-10:]
195
- # Termina con punto, signo de interrogación o exclamación
196
- if any(last_chars.rstrip().endswith(p) for p in ['.', '?', '!', '…']):
197
- # Contar oraciones completas
198
- sentences = re.split(r'[.!?]+', response)
199
- if len(sentences) >= 2: # Al menos 2 oraciones completas
200
- return True
201
-
202
- return False
203
-
204
- @staticmethod
205
- def clean_response(text):
206
- """Limpia y mejora la respuesta"""
207
- # Eliminar repeticiones excesivas
208
- text = re.sub(r'(\b\w+\b)(?:\s+\1\b)+', r'\1', text)
209
-
210
- # Corregir espaciado
211
- text = re.sub(r'\s+([.,!?;:])', r'\1', text)
212
-
213
- # Asegurar mayúscula al inicio
214
- if text and text[0].islower():
215
- text = text[0].upper() + text[1:]
216
-
217
- # Agregar punto final si no tiene
218
- if text and not text[-1] in '.!?':
219
- text += '.'
220
-
221
- return text.strip()
222
-
223
- @staticmethod
224
- def extract_key_info(text):
225
- """Extrae información clave del texto"""
226
- # Detectar números
227
- numbers = re.findall(r'\d+(?:\.\d+)?', text)
228
-
229
- # Detectar emails
230
- emails = re.findall(r'[\w\.-]+@[\w\.-]+\.\w+', text)
231
-
232
- # Detectar URLs
233
- urls = re.findall(r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+', text)
234
-
235
- return {
236
- 'has_numbers': bool(numbers),
237
- 'has_emails': bool(emails),
238
- 'has_urls': bool(urls),
239
- 'numbers': numbers,
240
- 'emails': emails,
241
- 'urls': urls
242
- }
243
-
244
- # ======================
245
- # DESCARGA Y CARGA DEL MODELO
246
- # ======================
247
- def download_with_retry(repo_id, local_dir, max_retries=3):
248
- for attempt in range(max_retries):
249
- try:
250
- print(f"📦 Intento {attempt + 1}/{max_retries} - Descargando modelo...")
251
- repo_path = snapshot_download(
252
- repo_id=repo_id,
253
- repo_type="model",
254
- local_dir=local_dir,
255
- resume_download=True,
256
- local_files_only=False
257
- )
258
- print(f"✅ Modelo descargado")
259
- return repo_path
260
- except Exception as e:
261
- print(f"⚠️ Error: {str(e)[:100]}")
262
- if attempt < max_retries - 1:
263
- time.sleep(3)
264
- else:
265
- raise
266
- return local_dir
267
-
268
- print(f"🚀 Cargando modelo...")
269
 
270
  if os.path.exists("mtp_repo") and os.path.exists("mtp_repo/mtp_model.pt"):
271
- print("📁 Modelo en caché")
272
  repo_path = "mtp_repo"
273
  else:
274
  try:
275
- repo_path = download_with_retry(MODEL_REPO, "mtp_repo", max_retries=3)
276
  except:
277
  repo_path = "mtp_repo"
278
 
279
- # Cargar configuración
280
  config_path = os.path.join(repo_path, "config.json")
281
  if os.path.exists(config_path):
282
  with open(config_path, "r") as f:
283
  config = json.load(f)
284
  else:
285
- config = {
286
- "vocab_size": 2000,
287
- "d_model": 256,
288
- "n_heads": 8,
289
- "n_layers": 6,
290
- "d_ff": 1024,
291
- "dropout": 0.1,
292
- "max_len": 512
293
- }
294
 
295
- # Cargar tokenizador
296
  tokenizer_path = os.path.join(repo_path, "mtp_tokenizer.model")
297
  if os.path.exists(tokenizer_path):
298
  sp = spm.SentencePieceProcessor()
299
  sp.load(tokenizer_path)
300
  VOCAB_SIZE = sp.get_piece_size()
301
  config["vocab_size"] = VOCAB_SIZE
302
- print(f"✅ Tokenizador: {VOCAB_SIZE} tokens")
303
  else:
304
  sp = None
305
- VOCAB_SIZE = config.get("vocab_size", 2000)
306
-
307
- print(f"🧠 Inicializando modelo...")
308
- print(f" → Vocabulario: {VOCAB_SIZE}")
309
- print(f" → Dimensión: {config['d_model']}")
310
- print(f" → Capas: {config['n_layers']}")
311
 
312
  model = MTPModel(**config)
313
  model.to(DEVICE)
314
 
315
- # Cargar pesos
316
  model_path = os.path.join(repo_path, "mtp_model.pt")
317
  if os.path.exists(model_path):
318
  try:
319
  state_dict = torch.load(model_path, map_location=DEVICE)
320
  model.load_state_dict(state_dict)
321
- print("✅ Pesos cargados")
322
- except Exception as e:
323
- print(f"⚠️ Error cargando pesos: {e}")
324
-
325
  model.eval()
326
 
327
- param_count = sum(p.numel() for p in model.parameters())
328
- print(f"✅ Modelo listo: {param_count:,} parámetros ({param_count/1e6:.1f}M)")
329
-
330
- # ======================
331
- # API CONFIG
332
- # ======================
333
- app = FastAPI(title="MTP API - Versión Mejorada", description="API con NLP integrado", version="2.0")
334
-
335
- app.add_middleware(
336
- CORSMiddleware,
337
- allow_origins=["*"],
338
- allow_methods=["*"],
339
- allow_headers=["*"],
340
- )
341
 
342
  class PromptRequest(BaseModel):
343
  text: str = Field(..., max_length=2000)
344
- max_tokens: int = Field(default=150, ge=10, le=300)
345
- temperature: float = Field(default=0.7, ge=0.1, le=2.0)
346
- top_k: int = Field(default=50, ge=1, le=100)
347
- top_p: float = Field(default=0.9, ge=0.1, le=1.0)
348
 
349
- # Inicializar NLP
350
- nlp = NLPProcessor()
 
 
 
 
 
 
 
 
 
 
 
351
 
352
- # ======================
353
- # GENERACIÓN INTELIGENTE MEJORADA
354
- # ======================
355
- def generate_response_intelligent(model, tokenizer, prompt, max_length=150, temperature=0.7, top_k=50, top_p=0.9, device='cpu'):
356
- model.eval()
357
-
358
- # Detectar intención para ajustar comportamiento
359
- intent = nlp.detect_intent(prompt)
360
-
361
- # Ajustar temperatura según intención
362
- if intent == 'despedida':
363
- temperature = 0.5 # Más determinista
364
- max_length = min(max_length, 60) # Respuestas cortas
365
- elif intent == 'pregunta':
366
- temperature = 0.6 # Más preciso
367
- elif intent == 'agradecimiento':
368
- temperature = 0.5
369
- max_length = min(max_length, 50)
370
-
371
- formatted_prompt = f"### Instrucción:\n{prompt}\n\n### Respuesta:\n"
372
- input_ids = tokenizer.encode(formatted_prompt)
 
 
373
  generated = input_ids.copy()
374
  eos_id = tokenizer.eos_id()
375
 
376
- # Contadores para control de parada
377
- consecutive_punctuation = 0
378
- last_chars = []
379
-
380
- for step in range(max_length):
381
- input_tensor = torch.tensor([generated[-model.max_len:]], dtype=torch.long).to(device)
382
  with torch.no_grad():
383
  logits = model(input_tensor)
384
  next_logits = logits[0, -1, :] / temperature
385
 
386
- # Top-k filtering
387
- if top_k > 0:
388
- indices_to_remove = next_logits < torch.topk(next_logits, top_k)[0][..., -1, None]
389
- next_logits[indices_to_remove] = float('-inf')
390
-
391
- # Top-p filtering
392
- if top_p < 1.0:
393
- sorted_logits, sorted_indices = torch.sort(next_logits, descending=True)
394
- cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
395
- sorted_indices_to_remove = cumulative_probs > top_p
396
- sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
397
- sorted_indices_to_remove[..., 0] = 0
398
- indices_to_remove = sorted_indices[sorted_indices_to_remove]
399
- next_logits[indices_to_remove] = float('-inf')
400
-
401
  probs = F.softmax(next_logits, dim=-1)
402
  next_token = torch.multinomial(probs, 1).item()
403
 
404
- # Detener en EOS
405
  if next_token == eos_id:
406
  break
407
-
408
- # Detener si hay demasiados signos de puntuación seguidos
409
- token_str = tokenizer.decode([next_token]) if hasattr(tokenizer, 'decode') else str(next_token)
410
- if token_str in '.!?':
411
- consecutive_punctuation += 1
412
- if consecutive_punctuation >= 3:
413
- break
414
- else:
415
- consecutive_punctuation = 0
416
-
417
- # Guardar últimos caracteres para análisis
418
- last_chars.append(token_str)
419
- if len(last_chars) > 20:
420
- last_chars.pop(0)
421
-
422
- # Detectar bucles de repetición
423
- if len(last_chars) >= 10:
424
- last_str = ''.join(last_chars[-5:])
425
- if last_str in ''.join(last_chars[:-5]):
426
- break
427
-
428
  generated.append(next_token)
429
-
430
- # Verificar si ya es suficiente (para respuestas cortas)
431
- current_response = tokenizer.decode(generated)
432
- if "### Respuesta:" in current_response:
433
- response_part = current_response.split("### Respuesta:")[-1].strip()
434
- if nlp.should_stop(response_part, min_length=20, max_length=max_length):
435
- break
436
 
437
- # Decodificar respuesta
438
  response = tokenizer.decode(generated)
439
-
440
- # Extraer la parte de la respuesta
441
  if "### Respuesta:" in response:
442
  response = response.split("### Respuesta:")[-1].strip()
443
- elif "Respuesta:" in response:
444
- response = response.split("Respuesta:")[-1].strip()
445
- elif "[/INST]" in response:
446
- response = response.split("[/INST]")[-1].strip()
447
 
448
- # Limpiar y mejorar respuesta
449
- garbage_words = ['foompañances', 'ciudadores', 'mejtedon', 'calportedon', 'rápidodcor', 'baon', 'domol']
450
- for word in garbage_words:
451
- response = response.replace(word, '')
452
-
453
- # Limpiar caracteres especiales
454
- response = re.sub(r'[^\w\s\u00C0-\u00FF\u0100-\u017F.,!?¿¡()\-:;"\']+', ' ', response)
455
  response = re.sub(r'\s+', ' ', response).strip()
456
 
457
- # Aplicar NLP a la respuesta
458
- response = nlp.clean_response(response)
459
-
460
- # Respuestas por defecto según intención si está vacía
461
- if len(response) < 3:
462
- default_responses = {
463
- 'saludo': "¡Hola! ¿En qué puedo ayudarte hoy?",
464
- 'despedida': "¡Hasta luego! Que tengas un excelente día.",
465
- 'agradecimiento': "¡De nada! Estoy aquí para ayudarte cuando lo necesites.",
466
- 'ayuda': "Claro, estoy aquí para ayudarte. ¿Qué necesitas saber?",
467
- 'presentacion': "Soy MTP, un asistente virtual creado para responder preguntas y ayudarte con información.",
468
- 'general': "Entendido. ¿Hay algo específico en lo que pueda ayudarte?"
469
- }
470
- response = default_responses.get(intent, default_responses['general'])
471
 
472
  return response
473
 
474
- # ======================
475
- # ENDPOINTS
476
- # ======================
477
- ACTIVE_REQUESTS = 0
478
-
479
- class TokenizerWrapper:
480
- def __init__(self, sp_model):
481
- self.sp = sp_model
482
- def encode(self, text):
483
- if self.sp is None:
484
- return [ord(c) % 1000 for c in text[:200]]
485
- return self.sp.encode(text)
486
- def decode(self, tokens):
487
- if self.sp is None:
488
- return ''.join([chr(t % 128) if 32 <= t % 128 < 127 else ' ' for t in tokens])
489
- return self.sp.decode(tokens)
490
- def eos_id(self):
491
- return self.sp.eos_id() if self.sp else 3
492
- def bos_id(self):
493
- return self.sp.bos_id() if self.sp else 2
494
- def pad_id(self):
495
- return self.sp.pad_id() if self.sp else 0
496
-
497
- tokenizer_wrapper = TokenizerWrapper(sp)
498
-
499
  @app.post("/generate")
500
  async def generate(req: PromptRequest):
501
- global ACTIVE_REQUESTS
502
- ACTIVE_REQUESTS += 1
503
-
504
  user_input = req.text.strip()
505
  if not user_input:
506
- ACTIVE_REQUESTS -= 1
507
- return {"reply": "", "tokens_generated": 0, "intent": None}
508
-
509
- # Detectar intención
510
- intent = nlp.detect_intent(user_input)
511
 
512
  try:
513
- response = generate_response_intelligent(
514
- model, tokenizer_wrapper, user_input,
515
- max_length=req.max_tokens,
516
- temperature=req.temperature,
517
- top_k=req.top_k,
518
- top_p=req.top_p,
519
- device=DEVICE
520
- )
521
-
522
- # Extraer información clave
523
- key_info = nlp.extract_key_info(response)
524
-
525
- return {
526
- "reply": response,
527
- "tokens_generated": len(response.split()),
528
- "model": "MTP-Intelligent",
529
- "intent": intent,
530
- "has_numbers": key_info['has_numbers'],
531
- "has_emails": key_info['has_emails']
532
- }
533
  except Exception as e:
534
- print(f"Error: {e}")
535
- return {"reply": "Lo siento, ocurrió un error.", "error": str(e), "intent": intent}
536
- finally:
537
- ACTIVE_REQUESTS -= 1
538
- if DEVICE == "cuda":
539
- torch.cuda.empty_cache()
540
- gc.collect()
541
 
542
- @app.get("/health")
543
- def health_check():
544
- return {
545
- "status": "healthy",
546
- "model": "MTP-Intelligent",
547
- "device": DEVICE,
548
- "active_requests": ACTIVE_REQUESTS,
549
- "vocab_size": VOCAB_SIZE
550
- }
551
-
552
- @app.get("/info")
553
- def model_info():
554
- return {
555
- "model_name": "MTP-Intelligent",
556
- "version": "2.0",
557
- "architecture": config,
558
- "parameters": sum(p.numel() for p in model.parameters()),
559
- "device": DEVICE,
560
- "nlp_enabled": True
561
- }
562
-
563
- @app.post("/analyze")
564
- async def analyze_intent(req: PromptRequest):
565
- """Endpoint para analizar intención sin generar respuesta"""
566
- intent = nlp.detect_intent(req.text)
567
- return {
568
- "text": req.text,
569
- "intent": intent,
570
- "confidence": 0.85 # Por ahora fijo, se puede mejorar
571
- }
572
-
573
- # ======================
574
- # INTERFAZ WEB MEJORADA
575
- # ======================
576
- @app.get("/", response_class=HTMLResponse)
577
- def chat_ui():
578
- return """
579
  <!DOCTYPE html>
580
- <html lang="es">
581
  <head>
582
- <meta charset="UTF-8">
583
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
584
- <title>MTP - Asistente Inteligente</title>
585
- <link rel="preconnect" href="https://fonts.googleapis.com">
586
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
587
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
588
- <style>
589
- :root {
590
- --bg-color: #131314;
591
- --surface-color: #1E1F20;
592
- --accent-color: #4a9eff;
593
- --text-primary: #e3e3e3;
594
- --text-secondary: #9aa0a6;
595
- --user-bubble: #282a2c;
596
- --success-color: #00c853;
597
- }
598
- * { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
599
- body {
600
- margin: 0;
601
- background-color: var(--bg-color);
602
- font-family: 'Inter', sans-serif;
603
- color: var(--text-primary);
604
- height: 100dvh;
605
- display: flex;
606
- flex-direction: column;
607
- overflow: hidden;
608
- }
609
- header {
610
- padding: 12px 20px;
611
- display: flex;
612
- align-items: center;
613
- justify-content: space-between;
614
- background: rgba(19, 19, 20, 0.85);
615
- backdrop-filter: blur(12px);
616
- position: fixed;
617
- top: 0;
618
- width: 100%;
619
- z-index: 50;
620
- border-bottom: 1px solid rgba(255,255,255,0.05);
621
- }
622
- .brand-wrapper {
623
- display: flex;
624
- align-items: center;
625
- gap: 12px;
626
- cursor: pointer;
627
- }
628
- .brand-logo {
629
- width: 32px;
630
- height: 32px;
631
- border-radius: 50%;
632
- background: linear-gradient(135deg, #4a9eff, #00c853);
633
- }
634
- .brand-text {
635
- font-weight: 500;
636
- font-size: 1.05rem;
637
- display: flex;
638
- align-items: center;
639
- gap: 8px;
640
- }
641
- .version-badge {
642
- font-size: 0.75rem;
643
- background: rgba(74, 158, 255, 0.15);
644
- color: #8ab4f8;
645
- padding: 2px 8px;
646
- border-radius: 12px;
647
- font-weight: 600;
648
- }
649
- .chat-scroll {
650
- flex: 1;
651
- overflow-y: auto;
652
- padding: 80px 20px 40px 20px;
653
- display: flex;
654
- flex-direction: column;
655
- gap: 30px;
656
- max-width: 850px;
657
- margin: 0 auto;
658
- width: 100%;
659
- scroll-behavior: smooth;
660
- }
661
- .msg-row {
662
- display: flex;
663
- gap: 16px;
664
- width: 100%;
665
- opacity: 0;
666
- transform: translateY(10px);
667
- animation: slideUpFade 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
668
- }
669
- .msg-row.user { justify-content: flex-end; }
670
- .msg-row.bot { justify-content: flex-start; align-items: flex-start; }
671
- .msg-content {
672
- line-height: 1.6;
673
- font-size: 1rem;
674
- word-wrap: break-word;
675
- max-width: 85%;
676
- }
677
- .user .msg-content {
678
- background-color: var(--user-bubble);
679
- padding: 10px 18px;
680
- border-radius: 18px;
681
- border-top-right-radius: 4px;
682
- color: #fff;
683
- }
684
- .bot .msg-content-wrapper {
685
- display: flex;
686
- flex-direction: column;
687
- gap: 8px;
688
- width: 100%;
689
- }
690
- .bot .msg-text {
691
- padding-top: 6px;
692
- color: var(--text-primary);
693
- }
694
- .bot-avatar {
695
- width: 34px;
696
- height: 34px;
697
- min-width: 34px;
698
- border-radius: 50%;
699
- background: linear-gradient(135deg, #4a9eff, #00c853);
700
- box-shadow: 0 2px 6px rgba(0,0,0,0.2);
701
- }
702
- .bot-actions {
703
- display: flex;
704
- gap: 10px;
705
- opacity: 0;
706
- transition: opacity 0.3s;
707
- margin-top: 5px;
708
- }
709
- .action-btn {
710
- background: transparent;
711
- border: none;
712
- color: var(--text-secondary);
713
- cursor: pointer;
714
- padding: 4px;
715
- border-radius: 4px;
716
- display: flex;
717
- align-items: center;
718
- transition: color 0.2s, background 0.2s;
719
- }
720
- .action-btn:hover {
721
- color: var(--text-primary);
722
- background: rgba(255,255,255,0.08);
723
- }
724
- .action-btn svg { width: 16px; height: 16px; fill: currentColor; }
725
- .typing-cursor::after {
726
- content: '';
727
- display: inline-block;
728
- width: 10px;
729
- height: 10px;
730
- background: var(--accent-color);
731
- border-radius: 50%;
732
- margin-left: 5px;
733
- vertical-align: middle;
734
- animation: blink 1s infinite;
735
- }
736
- .footer-container {
737
- padding: 0 20px 20px 20px;
738
- background: linear-gradient(to top, var(--bg-color) 85%, transparent);
739
- position: relative;
740
- z-index: 60;
741
- }
742
- .input-box {
743
- max-width: 850px;
744
- margin: 0 auto;
745
- background: var(--surface-color);
746
- border-radius: 28px;
747
- padding: 8px 10px 8px 20px;
748
- display: flex;
749
- align-items: center;
750
- border: 1px solid rgba(255,255,255,0.1);
751
- transition: border-color 0.2s, box-shadow 0.2s;
752
- }
753
- .input-box:focus-within {
754
- border-color: rgba(74, 158, 255, 0.5);
755
- box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.1);
756
- }
757
- #userInput {
758
- flex: 1;
759
- background: transparent;
760
- border: none;
761
- color: white;
762
- font-size: 1rem;
763
- font-family: inherit;
764
- padding: 10px 0;
765
- }
766
- #mainBtn {
767
- background: var(--accent-color);
768
- color: white;
769
- border: none;
770
- width: 36px;
771
- height: 36px;
772
- border-radius: 50%;
773
- display: flex;
774
- align-items: center;
775
- justify-content: center;
776
- cursor: pointer;
777
- margin-left: 8px;
778
- transition: transform 0.2s;
779
- }
780
- #mainBtn:hover { transform: scale(1.05); background: #3a7ed4; }
781
- .disclaimer {
782
- text-align: center;
783
- font-size: 0.75rem;
784
- color: #666;
785
- margin-top: 12px;
786
- }
787
- @keyframes slideUpFade {
788
- from { opacity: 0; transform: translateY(15px); }
789
- to { opacity: 1; transform: translateY(0); }
790
- }
791
- @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
792
- @keyframes pulseAvatar {
793
- 0% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.4); }
794
- 70% { box-shadow: 0 0 0 8px rgba(74, 158, 255, 0); }
795
- 100% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0); }
796
- }
797
- .pulsing { animation: pulseAvatar 1.5s infinite; }
798
- .intent-badge {
799
- font-size: 0.7rem;
800
- background: rgba(0, 200, 83, 0.15);
801
- color: #00c853;
802
- padding: 2px 8px;
803
- border-radius: 12px;
804
- display: inline-block;
805
- margin-top: 5px;
806
- }
807
- ::-webkit-scrollbar { width: 8px; }
808
- ::-webkit-scrollbar-track { background: transparent; }
809
- ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
810
- </style>
811
  </head>
812
  <body>
813
- <header>
814
- <div class="brand-wrapper" onclick="location.reload()">
815
- <div class="brand-logo"></div>
816
- <div class="brand-text">
817
- MTP <span class="version-badge">Inteligente</span>
818
- </div>
819
- </div>
820
- </header>
821
- <div id="chatScroll" class="chat-scroll">
822
- <div class="msg-row bot" style="animation-delay: 0.1s;">
823
- <div class="bot-avatar"></div>
824
- <div class="msg-content-wrapper">
825
- <div class="msg-text">
826
- ¡Hola! Soy MTP, tu asistente inteligente. ¿En qué puedo ayudarte hoy?
827
- </div>
828
- </div>
829
- </div>
830
- </div>
831
- <div class="footer-container">
832
- <div class="input-box">
833
- <input type="text" id="userInput" placeholder="Escribe tu mensaje..." autocomplete="off">
834
- <button id="mainBtn" onclick="handleBtnClick()">➤</button>
835
  </div>
836
- <div class="disclaimer">
837
- MTP usa NLP para entender mejor tu consulta • Respuestas inteligentes
 
838
  </div>
839
- </div>
840
- <script>
841
- const chatScroll = document.getElementById('chatScroll');
842
- const userInput = document.getElementById('userInput');
843
- const mainBtn = document.getElementById('mainBtn');
844
- let isGenerating = false;
845
- let abortController = null;
846
- let typingTimeout = null;
847
- let lastUserPrompt = "";
848
-
849
- function scrollToBottom() {
850
- chatScroll.scrollTop = chatScroll.scrollHeight;
851
- }
852
-
853
- function setBtnState(state) {
854
- if (state === 'sending') {
855
- mainBtn.innerHTML = "⏹";
856
- isGenerating = true;
857
- } else {
858
- mainBtn.innerHTML = "➤";
859
- isGenerating = false;
860
- abortController = null;
861
- }
862
- }
863
-
864
- function handleBtnClick() {
865
- if (isGenerating) {
866
- stopGeneration();
867
- } else {
868
- sendMessage();
869
- }
870
- }
871
-
872
- function stopGeneration() {
873
- if (abortController) abortController.abort();
874
- if (typingTimeout) clearTimeout(typingTimeout);
875
- const activeCursor = document.querySelector('.typing-cursor');
876
- if (activeCursor) activeCursor.classList.remove('typing-cursor');
877
- const activeAvatar = document.querySelector('.pulsing');
878
- if (activeAvatar) activeAvatar.classList.remove('pulsing');
879
- setBtnState('idle');
880
- userInput.focus();
881
- }
882
-
883
- async function sendMessage(textOverride = null) {
884
- const text = textOverride || userInput.value.trim();
885
- if (!text || isGenerating) return;
886
-
887
- lastUserPrompt = text;
888
- if (!textOverride) {
889
- userInput.value = '';
890
- addMessage(text, 'user');
891
- }
892
-
893
- setBtnState('sending');
894
- abortController = new AbortController();
895
-
896
- const botRow = document.createElement('div');
897
- botRow.className = 'msg-row bot';
898
- const avatar = document.createElement('div');
899
- avatar.className = 'bot-avatar pulsing';
900
- const wrapper = document.createElement('div');
901
- wrapper.className = 'msg-content-wrapper';
902
- const msgText = document.createElement('div');
903
- msgText.className = 'msg-text';
904
- wrapper.appendChild(msgText);
905
- botRow.appendChild(avatar);
906
- botRow.appendChild(wrapper);
907
- chatScroll.appendChild(botRow);
908
- scrollToBottom();
909
-
910
- try {
911
- const response = await fetch('/generate', {
912
- method: 'POST',
913
- headers: { 'Content-Type': 'application/json' },
914
- body: JSON.stringify({
915
- text: text,
916
- max_tokens: 200,
917
- temperature: 0.7,
918
- top_k: 50,
919
- top_p: 0.9
920
- }),
921
- signal: abortController.signal
922
- });
923
-
924
- const data = await response.json();
925
- if (!isGenerating) return;
926
 
927
- avatar.classList.remove('pulsing');
928
- const reply = data.reply || "No entendí eso.";
929
-
930
- // Mostrar intención detectada si está disponible
931
- if (data.intent && data.intent !== 'general') {
932
- const intentSpan = document.createElement('div');
933
- intentSpan.className = 'intent-badge';
934
- intentSpan.textContent = `🎯 Intención: ${data.intent}`;
935
- wrapper.appendChild(intentSpan);
936
  }
937
 
938
- await typeWriter(msgText, reply);
939
- if (isGenerating) {
940
- addActions(wrapper, reply);
941
- setBtnState('idle');
942
- }
943
- } catch (error) {
944
- if (error.name === 'AbortError') {
945
- msgText.textContent += " [Detenido]";
946
- } else {
947
- avatar.classList.remove('pulsing');
948
- msgText.textContent = "Error de conexión. Intenta de nuevo.";
949
- msgText.style.color = "#ff8b8b";
950
- setBtnState('idle');
951
- }
952
- }
953
- }
954
-
955
- function addMessage(text, sender) {
956
- const row = document.createElement('div');
957
- row.className = `msg-row ${sender}`;
958
- const content = document.createElement('div');
959
- content.className = 'msg-content';
960
- content.textContent = text;
961
- row.appendChild(content);
962
- chatScroll.appendChild(row);
963
- scrollToBottom();
964
- }
965
-
966
- function typeWriter(element, text, speed = 10) {
967
- return new Promise(resolve => {
968
- let i = 0;
969
- element.classList.add('typing-cursor');
970
- function type() {
971
- if (!isGenerating) {
972
- element.classList.remove('typing-cursor');
973
- resolve();
974
- return;
975
- }
976
- if (i < text.length) {
977
- element.textContent += text.charAt(i);
978
- i++;
979
- scrollToBottom();
980
- typingTimeout = setTimeout(type, speed + Math.random() * 5);
981
- } else {
982
- element.classList.remove('typing-cursor');
983
- resolve();
984
  }
985
  }
986
- type();
987
- });
988
- }
989
-
990
- function addActions(wrapperElement, textToCopy) {
991
- const actionsDiv = document.createElement('div');
992
- actionsDiv.className = 'bot-actions';
993
-
994
- const copyBtn = document.createElement('button');
995
- copyBtn.className = 'action-btn';
996
- copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
997
- copyBtn.onclick = () => { navigator.clipboard.writeText(textToCopy); };
998
-
999
- const regenBtn = document.createElement('button');
1000
- regenBtn.className = 'action-btn';
1001
- regenBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`;
1002
- regenBtn.onclick = () => { sendMessage(lastUserPrompt); };
1003
-
1004
- actionsDiv.appendChild(copyBtn);
1005
- actionsDiv.appendChild(regenBtn);
1006
- wrapperElement.appendChild(actionsDiv);
1007
- requestAnimationFrame(() => actionsDiv.style.opacity = "1");
1008
- scrollToBottom();
1009
- }
1010
-
1011
- userInput.addEventListener('keydown', (e) => {
1012
- if (e.key === 'Enter') handleBtnClick();
1013
- });
1014
-
1015
- window.onload = () => userInput.focus();
1016
- </script>
1017
  </body>
1018
  </html>
1019
- """
1020
 
1021
  if __name__ == "__main__":
1022
- port = int(os.environ.get("PORT", 7860))
1023
- print(f"\n🚀 MTP Inteligente iniciado en puerto {port}")
1024
- print(f"🌐 http://0.0.0.0:{port}")
1025
-
1026
- uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
 
1
  import os
 
2
  import torch
3
  import json
 
 
4
  import re
5
+ from fastapi import FastAPI
6
  from fastapi.responses import HTMLResponse
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel, Field
 
13
  import torch.nn.functional as F
14
  import sentencepiece as spm
15
 
16
+ # ====================== CONFIGURACIÓN ======================
17
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
18
+ print(f"📱 Dispositivo: {DEVICE}")
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  MODEL_REPO = "TeszenAI/MTP-3"
21
 
22
+ # ====================== ARQUITECTURA DEL MODELO ======================
 
 
23
  class LayerNorm(nn.Module):
24
  def __init__(self, d_model: int, eps: float = 1e-5):
25
  super().__init__()
 
98
  return x + self.pe[:, :x.size(1), :]
99
 
100
  class MTPModel(nn.Module):
101
+ def __init__(self, vocab_size: int, d_model: int = 512, n_heads: int = 8,
102
+ n_layers: int = 8, d_ff: int = 2048, dropout: float = 0.1, max_len: int = 512):
103
  super().__init__()
104
  self.vocab_size = vocab_size
105
  self.d_model = d_model
 
109
  self.blocks = nn.ModuleList([TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)])
110
  self.norm = LayerNorm(d_model)
111
  self.lm_head = nn.Linear(d_model, vocab_size)
 
112
  def forward(self, x, mask=None):
113
  if mask is None:
114
  mask = torch.tril(torch.ones(x.size(1), x.size(1))).unsqueeze(0).unsqueeze(0).to(x.device)
 
119
  x = self.norm(x)
120
  return self.lm_head(x)
121
 
122
+ # ====================== DESCARGA DEL MODELO ======================
123
+ print(f"📦 Cargando modelo...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  if os.path.exists("mtp_repo") and os.path.exists("mtp_repo/mtp_model.pt"):
 
126
  repo_path = "mtp_repo"
127
  else:
128
  try:
129
+ repo_path = snapshot_download(repo_id=MODEL_REPO, repo_type="model", local_dir="mtp_repo", resume_download=True)
130
  except:
131
  repo_path = "mtp_repo"
132
 
133
+ # Configuración
134
  config_path = os.path.join(repo_path, "config.json")
135
  if os.path.exists(config_path):
136
  with open(config_path, "r") as f:
137
  config = json.load(f)
138
  else:
139
+ config = {"vocab_size": 10000, "d_model": 512, "n_heads": 8, "n_layers": 8, "d_ff": 2048, "dropout": 0.1, "max_len": 512}
 
 
 
 
 
 
 
 
140
 
141
+ # Tokenizador
142
  tokenizer_path = os.path.join(repo_path, "mtp_tokenizer.model")
143
  if os.path.exists(tokenizer_path):
144
  sp = spm.SentencePieceProcessor()
145
  sp.load(tokenizer_path)
146
  VOCAB_SIZE = sp.get_piece_size()
147
  config["vocab_size"] = VOCAB_SIZE
 
148
  else:
149
  sp = None
150
+ VOCAB_SIZE = 10000
 
 
 
 
 
151
 
152
  model = MTPModel(**config)
153
  model.to(DEVICE)
154
 
 
155
  model_path = os.path.join(repo_path, "mtp_model.pt")
156
  if os.path.exists(model_path):
157
  try:
158
  state_dict = torch.load(model_path, map_location=DEVICE)
159
  model.load_state_dict(state_dict)
160
+ print("✅ Modelo cargado")
161
+ except:
162
+ print("⚠️ Error cargando pesos")
 
163
  model.eval()
164
 
165
+ # ====================== API ======================
166
+ app = FastAPI()
167
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  class PromptRequest(BaseModel):
170
  text: str = Field(..., max_length=2000)
 
 
 
 
171
 
172
+ class TokenizerWrapper:
173
+ def __init__(self, sp_model):
174
+ self.sp = sp_model
175
+ def encode(self, text):
176
+ if self.sp is None:
177
+ return [ord(c) % 1000 for c in text[:200]]
178
+ return self.sp.encode(text)
179
+ def decode(self, tokens):
180
+ if self.sp is None:
181
+ return ''.join([chr(t % 128) if 32 <= t % 128 < 127 else ' ' for t in tokens])
182
+ return self.sp.decode(tokens)
183
+ def eos_id(self):
184
+ return self.sp.eos_id() if self.sp else 3
185
 
186
+ tokenizer = TokenizerWrapper(sp)
187
+
188
+ # Diccionario de respuestas por categoría (fallback cuando el modelo alucina)
189
+ RESPUESTAS_FALLBACK = {
190
+ "marketing": "El marketing digital incluye estrategias como SEO, marketing en redes sociales, email marketing, publicidad pagada y marketing de contenidos. ¿Te gustaría que profundice en alguna de estas áreas?",
191
+ "blackpink": "BLACKPINK es un grupo femenino de K-pop formado por Jisoo, Jennie, Rosé y Lisa. Tienen éxitos como 'Ddu-Du Ddu-Du', 'Kill This Love' y 'How You Like That'.",
192
+ "bts": "BTS es un grupo masculino de K-pop formado por RM, Jin, Suga, J-Hope, Jimin, V y Jungkook. Son conocidos por éxitos como 'Dynamite', 'Butter' y 'Boy With Luv'.",
193
+ "default": "Lo siento, no entendí bien tu pregunta. ¿Podrías reformularla? Estoy aquí para ayudarte con marketing, K-pop (BLACKPINK, BTS), tecnología, y más."
194
+ }
195
+
196
+ def detectar_tema(texto):
197
+ texto = texto.lower()
198
+ if "marketing" in texto or "seo" in texto or "publicidad" in texto or "redes sociales" in texto:
199
+ return "marketing"
200
+ if "blackpink" in texto or "jisoo" in texto or "jennie" in texto or "rosé" in texto or "lisa" in texto:
201
+ return "blackpink"
202
+ if "bts" in texto or "rm" in texto or "jin" in texto or "suga" in texto or "j-hope" in texto or "jimin" in texto or "v" in texto or "jungkook" in texto:
203
+ return "bts"
204
+ return None
205
+
206
+ def generar_respuesta(prompt, max_length=200, temperature=0.7):
207
+ formatted = f"### Instrucción:\n{prompt}\n\n### Respuesta:\n"
208
+ input_ids = tokenizer.encode(formatted)
209
  generated = input_ids.copy()
210
  eos_id = tokenizer.eos_id()
211
 
212
+ for _ in range(max_length):
213
+ input_tensor = torch.tensor([generated[-model.max_len:]], dtype=torch.long).to(DEVICE)
 
 
 
 
214
  with torch.no_grad():
215
  logits = model(input_tensor)
216
  next_logits = logits[0, -1, :] / temperature
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  probs = F.softmax(next_logits, dim=-1)
219
  next_token = torch.multinomial(probs, 1).item()
220
 
 
221
  if next_token == eos_id:
222
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  generated.append(next_token)
 
 
 
 
 
 
 
224
 
 
225
  response = tokenizer.decode(generated)
 
 
226
  if "### Respuesta:" in response:
227
  response = response.split("### Respuesta:")[-1].strip()
 
 
 
 
228
 
229
+ # Limpiar caracteres basura
230
+ response = re.sub(r'[^\w\s\u00C0-\u00FF.,!?¿¡\-:;"]+', ' ', response)
 
 
 
 
 
231
  response = re.sub(r'\s+', ' ', response).strip()
232
 
233
+ # Verificar si la respuesta es coherente
234
+ if len(response) < 5 or "kSq" in response or "%" in response or "{" in response:
235
+ tema = detectar_tema(prompt)
236
+ if tema:
237
+ response = RESPUESTAS_FALLBACK.get(tema, RESPUESTAS_FALLBACK["default"])
238
+ else:
239
+ response = RESPUESTAS_FALLBACK["default"]
 
 
 
 
 
 
 
240
 
241
  return response
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  @app.post("/generate")
244
  async def generate(req: PromptRequest):
 
 
 
245
  user_input = req.text.strip()
246
  if not user_input:
247
+ return {"reply": ""}
 
 
 
 
248
 
249
  try:
250
+ response = generar_respuesta(user_input)
251
+ return {"reply": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  except Exception as e:
253
+ print(f"Error: {e}")
254
+ tema = detectar_tema(user_input)
255
+ return {"reply": RESPUESTAS_FALLBACK.get(tema, RESPUESTAS_FALLBACK["default"])}
 
 
 
 
256
 
257
+ @app.get("/")
258
+ async def chat_ui():
259
+ return HTMLResponse("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  <!DOCTYPE html>
261
+ <html>
262
  <head>
263
+ <title>MTP Asistente</title>
264
+ <meta charset="UTF-8">
265
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
266
+ <style>
267
+ body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #131314; color: #e3e3e3; }
268
+ #chat { height: 500px; border: 1px solid #333; overflow-y: auto; padding: 10px; margin-bottom: 10px; border-radius: 10px; background: #1E1F20; }
269
+ .user { text-align: right; margin: 10px; }
270
+ .user span { background: #4a9eff; padding: 8px 15px; border-radius: 18px; display: inline-block; }
271
+ .bot { text-align: left; margin: 10px; }
272
+ .bot span { background: #282a2c; padding: 8px 15px; border-radius: 18px; display: inline-block; }
273
+ input { width: 80%; padding: 10px; border-radius: 25px; border: none; background: #1E1F20; color: white; }
274
+ button { padding: 10px 20px; border-radius: 25px; border: none; background: #4a9eff; color: white; cursor: pointer; }
275
+ .typing { opacity: 0.7; font-style: italic; }
276
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  </head>
278
  <body>
279
+ <h1>🤖 MTP Asistente</h1>
280
+ <div id="chat">
281
+ <div class="bot"><span>¡Hola! Soy MTP. Puedo ayudarte con marketing, K-pop (BLACKPINK, BTS), tecnología y más. ¿Qué necesitas?</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
283
+ <div>
284
+ <input type="text" id="input" placeholder="Escribe tu mensaje..." autocomplete="off">
285
+ <button onclick="sendMessage()">Enviar</button>
286
  </div>
287
+ <script>
288
+ const chat = document.getElementById('chat');
289
+ const input = document.getElementById('input');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ function addMessage(text, sender) {
292
+ const div = document.createElement('div');
293
+ div.className = sender;
294
+ div.innerHTML = `<span>${text}</span>`;
295
+ chat.appendChild(div);
296
+ chat.scrollTop = chat.scrollHeight;
 
 
 
297
  }
298
 
299
+ async function sendMessage() {
300
+ const text = input.value.trim();
301
+ if (!text) return;
302
+ input.value = '';
303
+ addMessage(text, 'user');
304
+
305
+ const loadingDiv = document.createElement('div');
306
+ loadingDiv.className = 'bot';
307
+ loadingDiv.innerHTML = '<span class="typing">✍️ Pensando...</span>';
308
+ chat.appendChild(loadingDiv);
309
+ chat.scrollTop = chat.scrollHeight;
310
+
311
+ try {
312
+ const response = await fetch('/generate', {
313
+ method: 'POST',
314
+ headers: { 'Content-Type': 'application/json' },
315
+ body: JSON.stringify({ text: text })
316
+ });
317
+ const data = await response.json();
318
+ loadingDiv.remove();
319
+ addMessage(data.reply, 'bot');
320
+ } catch (error) {
321
+ loadingDiv.innerHTML = '<span class="typing">❌ Error de conexión</span>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
  }
324
+
325
+ input.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); });
326
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  </body>
328
  </html>
329
+ """)
330
 
331
  if __name__ == "__main__":
332
+ uvicorn.run(app, host="0.0.0.0", port=7860)