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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +303 -105
app.py CHANGED
@@ -34,7 +34,7 @@ torch.set_grad_enabled(False)
34
  MODEL_REPO = "TeszenAI/MTP-3"
35
 
36
  # ======================
37
- # ARQUITECTURA DEL MODELO (MISMA QUE EN colab.py)
38
  # ======================
39
  class LayerNorm(nn.Module):
40
  def __init__(self, d_model: int, eps: float = 1e-5):
@@ -137,12 +137,117 @@ class MTPModel(nn.Module):
137
  return self.lm_head(x)
138
 
139
  # ======================
140
- # DESCARGA Y CARGA DEL MODELO CON REINTENTOS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ======================
142
  def download_with_retry(repo_id, local_dir, max_retries=3):
143
  for attempt in range(max_retries):
144
  try:
145
- print(f"📦 Intento {attempt + 1}/{max_retries} - Descargando modelo desde {repo_id}...")
146
  repo_path = snapshot_download(
147
  repo_id=repo_id,
148
  repo_type="model",
@@ -150,26 +255,25 @@ def download_with_retry(repo_id, local_dir, max_retries=3):
150
  resume_download=True,
151
  local_files_only=False
152
  )
153
- print(f"✅ Modelo descargado exitosamente en: {repo_path}")
154
  return repo_path
155
  except Exception as e:
156
- print(f"⚠️ Error en intento {attempt + 1}: {str(e)[:200]}")
157
  if attempt < max_retries - 1:
158
  time.sleep(3)
159
  else:
160
  raise
161
  return local_dir
162
 
163
- print(f"🚀 Iniciando carga del modelo desde {MODEL_REPO}...")
164
 
165
  if os.path.exists("mtp_repo") and os.path.exists("mtp_repo/mtp_model.pt"):
166
- print("📁 Modelo encontrado en caché local")
167
  repo_path = "mtp_repo"
168
  else:
169
  try:
170
  repo_path = download_with_retry(MODEL_REPO, "mtp_repo", max_retries=3)
171
- except Exception as e:
172
- print(f"⚠️ Error: {e}")
173
  repo_path = "mtp_repo"
174
 
175
  # Cargar configuración
@@ -178,7 +282,6 @@ if os.path.exists(config_path):
178
  with open(config_path, "r") as f:
179
  config = json.load(f)
180
  else:
181
- # Configuración por defecto (MISMA que en colab.py)
182
  config = {
183
  "vocab_size": 2000,
184
  "d_model": 256,
@@ -196,42 +299,38 @@ if os.path.exists(tokenizer_path):
196
  sp.load(tokenizer_path)
197
  VOCAB_SIZE = sp.get_piece_size()
198
  config["vocab_size"] = VOCAB_SIZE
199
- print(f"✅ Tokenizador cargado: {VOCAB_SIZE} tokens")
200
  else:
201
- print("❌ No se encontró tokenizador")
202
  sp = None
203
  VOCAB_SIZE = config.get("vocab_size", 2000)
204
 
205
- print(f"🧠 Inicializando modelo MTP...")
206
  print(f" → Vocabulario: {VOCAB_SIZE}")
207
  print(f" → Dimensión: {config['d_model']}")
208
  print(f" → Capas: {config['n_layers']}")
209
- print(f" → Heads: {config['n_heads']}")
210
 
211
  model = MTPModel(**config)
212
  model.to(DEVICE)
213
 
214
- # Cargar pesos del modelo
215
  model_path = os.path.join(repo_path, "mtp_model.pt")
216
  if os.path.exists(model_path):
217
  try:
218
  state_dict = torch.load(model_path, map_location=DEVICE)
219
  model.load_state_dict(state_dict)
220
- print("✅ Pesos del modelo cargados correctamente")
221
  except Exception as e:
222
  print(f"⚠️ Error cargando pesos: {e}")
223
- else:
224
- print("⚠️ No se encontró mtp_model.pt")
225
 
226
  model.eval()
227
 
228
  param_count = sum(p.numel() for p in model.parameters())
229
- print(f"✅ Modelo cargado: {param_count:,} parámetros ({param_count/1e6:.1f}M)")
230
 
231
  # ======================
232
  # API CONFIG
233
  # ======================
234
- app = FastAPI(title="MTP API", description="API para modelo de lenguaje MTP", version="1.0")
235
 
236
  app.add_middleware(
237
  CORSMiddleware,
@@ -246,28 +345,50 @@ class PromptRequest(BaseModel):
246
  temperature: float = Field(default=0.7, ge=0.1, le=2.0)
247
  top_k: int = Field(default=50, ge=1, le=100)
248
  top_p: float = Field(default=0.9, ge=0.1, le=1.0)
249
- repetition_penalty: float = Field(default=1.1, ge=1.0, le=2.0)
 
 
250
 
251
  # ======================
252
- # FUNCIÓN DE GENERACIÓN (IGUAL QUE EN colab.py)
253
  # ======================
254
- def generate_response(model, tokenizer, prompt, max_length=150, temperature=0.7, top_k=50, top_p=0.9, device='cpu'):
255
  model.eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  formatted_prompt = f"### Instrucción:\n{prompt}\n\n### Respuesta:\n"
257
  input_ids = tokenizer.encode(formatted_prompt)
258
  generated = input_ids.copy()
259
  eos_id = tokenizer.eos_id()
260
-
261
- for _ in range(max_length):
 
 
 
 
262
  input_tensor = torch.tensor([generated[-model.max_len:]], dtype=torch.long).to(device)
263
  with torch.no_grad():
264
  logits = model(input_tensor)
265
  next_logits = logits[0, -1, :] / temperature
266
-
 
267
  if top_k > 0:
268
  indices_to_remove = next_logits < torch.topk(next_logits, top_k)[0][..., -1, None]
269
  next_logits[indices_to_remove] = float('-inf')
270
-
 
271
  if top_p < 1.0:
272
  sorted_logits, sorted_indices = torch.sort(next_logits, descending=True)
273
  cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
@@ -276,39 +397,78 @@ def generate_response(model, tokenizer, prompt, max_length=150, temperature=0.7,
276
  sorted_indices_to_remove[..., 0] = 0
277
  indices_to_remove = sorted_indices[sorted_indices_to_remove]
278
  next_logits[indices_to_remove] = float('-inf')
279
-
280
  probs = F.softmax(next_logits, dim=-1)
281
  next_token = torch.multinomial(probs, 1).item()
282
-
 
283
  if next_token == eos_id:
284
  break
285
-
286
- if len(generated) > 20:
287
- last_tokens = generated[-10:]
288
- if len(set(last_tokens)) == 1:
 
 
289
  break
290
-
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  generated.append(next_token)
292
-
 
 
 
 
 
 
 
 
293
  response = tokenizer.decode(generated)
 
 
294
  if "### Respuesta:" in response:
295
  response = response.split("### Respuesta:")[-1].strip()
296
  elif "Respuesta:" in response:
297
  response = response.split("Respuesta:")[-1].strip()
298
  elif "[/INST]" in response:
299
  response = response.split("[/INST]")[-1].strip()
300
-
301
- # Limpiar caracteres basura
302
  garbage_words = ['foompañances', 'ciudadores', 'mejtedon', 'calportedon', 'rápidodcor', 'baon', 'domol']
303
  for word in garbage_words:
304
  response = response.replace(word, '')
305
 
306
- response = re.sub(r'[^\w\s\u00C0-\u00FF\u0100-\u017F.,!?¿¡()\-:;"]+', ' ', response)
 
307
  response = re.sub(r'\s+', ' ', response).strip()
308
-
309
- if len(response) < 2:
310
- response = "Entendido. ¿Algo más en lo que pueda ayudarte?"
311
-
 
 
 
 
 
 
 
 
 
 
 
 
312
  return response
313
 
314
  # ======================
@@ -341,36 +501,38 @@ async def generate(req: PromptRequest):
341
  global ACTIVE_REQUESTS
342
  ACTIVE_REQUESTS += 1
343
 
344
- dyn_max_tokens = req.max_tokens
345
- dyn_temperature = req.temperature
346
-
347
- if ACTIVE_REQUESTS > 2:
348
- print(f"⚠️ Carga alta ({ACTIVE_REQUESTS} requests). Ajustando parámetros.")
349
- dyn_max_tokens = min(dyn_max_tokens, 120)
350
- dyn_temperature = max(0.5, dyn_temperature * 0.9)
351
-
352
  user_input = req.text.strip()
353
  if not user_input:
354
  ACTIVE_REQUESTS -= 1
355
- return {"reply": "", "tokens_generated": 0}
356
-
 
 
 
357
  try:
358
- response = generate_response(
359
  model, tokenizer_wrapper, user_input,
360
- max_length=dyn_max_tokens,
361
- temperature=dyn_temperature,
362
  top_k=req.top_k,
363
  top_p=req.top_p,
364
  device=DEVICE
365
  )
 
 
 
 
366
  return {
367
  "reply": response,
368
  "tokens_generated": len(response.split()),
369
- "model": "MTP"
 
 
 
370
  }
371
  except Exception as e:
372
  print(f"❌ Error: {e}")
373
- return {"reply": "Lo siento, ocurrió un error.", "error": str(e)}
374
  finally:
375
  ACTIVE_REQUESTS -= 1
376
  if DEVICE == "cuda":
@@ -381,7 +543,7 @@ async def generate(req: PromptRequest):
381
  def health_check():
382
  return {
383
  "status": "healthy",
384
- "model": "MTP",
385
  "device": DEVICE,
386
  "active_requests": ACTIVE_REQUESTS,
387
  "vocab_size": VOCAB_SIZE
@@ -390,15 +552,26 @@ def health_check():
390
  @app.get("/info")
391
  def model_info():
392
  return {
393
- "model_name": "MTP",
394
- "version": "1.0",
395
  "architecture": config,
396
  "parameters": sum(p.numel() for p in model.parameters()),
397
- "device": DEVICE
 
 
 
 
 
 
 
 
 
 
 
398
  }
399
 
400
  # ======================
401
- # INTERFAZ WEB COMPLETA (CON TODAS LAS FUNCIONES ORIGINALES)
402
  # ======================
403
  @app.get("/", response_class=HTMLResponse)
404
  def chat_ui():
@@ -408,7 +581,7 @@ def chat_ui():
408
  <head>
409
  <meta charset="UTF-8">
410
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
411
- <title>MTP 3</title>
412
  <link rel="preconnect" href="https://fonts.googleapis.com">
413
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
414
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
@@ -420,8 +593,7 @@ def chat_ui():
420
  --text-primary: #e3e3e3;
421
  --text-secondary: #9aa0a6;
422
  --user-bubble: #282a2c;
423
- --bot-actions-color: #c4c7c5;
424
- --logo-url: url('https://i.postimg.cc/yxS54PF3/IMG-3082.jpg');
425
  }
426
  * { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
427
  body {
@@ -457,10 +629,7 @@ header {
457
  width: 32px;
458
  height: 32px;
459
  border-radius: 50%;
460
- background-image: var(--logo-url);
461
- background-size: cover;
462
- background-position: center;
463
- border: 1px solid rgba(255,255,255,0.1);
464
  }
465
  .brand-text {
466
  font-weight: 500;
@@ -527,8 +696,7 @@ header {
527
  height: 34px;
528
  min-width: 34px;
529
  border-radius: 50%;
530
- background-image: var(--logo-url);
531
- background-size: cover;
532
  box-shadow: 0 2px 6px rgba(0,0,0,0.2);
533
  }
534
  .bot-actions {
@@ -596,8 +764,8 @@ header {
596
  padding: 10px 0;
597
  }
598
  #mainBtn {
599
- background: white;
600
- color: black;
601
  border: none;
602
  width: 36px;
603
  height: 36px;
@@ -609,7 +777,7 @@ header {
609
  margin-left: 8px;
610
  transition: transform 0.2s;
611
  }
612
- #mainBtn:hover { transform: scale(1.05); }
613
  .disclaimer {
614
  text-align: center;
615
  font-size: 0.75rem;
@@ -627,6 +795,15 @@ header {
627
  100% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0); }
628
  }
629
  .pulsing { animation: pulseAvatar 1.5s infinite; }
 
 
 
 
 
 
 
 
 
630
  ::-webkit-scrollbar { width: 8px; }
631
  ::-webkit-scrollbar-track { background: transparent; }
632
  ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
@@ -637,7 +814,7 @@ header {
637
  <div class="brand-wrapper" onclick="location.reload()">
638
  <div class="brand-logo"></div>
639
  <div class="brand-text">
640
- MTP <span class="version-badge">3</span>
641
  </div>
642
  </div>
643
  </header>
@@ -646,18 +823,18 @@ header {
646
  <div class="bot-avatar"></div>
647
  <div class="msg-content-wrapper">
648
  <div class="msg-text">
649
- ¡Hola! Soy MTP 3 ¿En qué puedo ayudarte hoy?
650
  </div>
651
  </div>
652
  </div>
653
  </div>
654
  <div class="footer-container">
655
  <div class="input-box">
656
- <input type="text" id="userInput" placeholder="Escribe un mensaje..." autocomplete="off">
657
- <button id="mainBtn" onclick="handleBtnClick()"></button>
658
  </div>
659
  <div class="disclaimer">
660
- MTP puede cometer errores. Considera verificar la información importante.
661
  </div>
662
  </div>
663
  <script>
@@ -668,22 +845,22 @@ let isGenerating = false;
668
  let abortController = null;
669
  let typingTimeout = null;
670
  let lastUserPrompt = "";
671
- const ICON_SEND = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path></svg>`;
672
- const ICON_STOP = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="0"><rect x="2" y="2" width="20" height="20" rx="4" ry="4"></rect></svg>`;
673
- mainBtn.innerHTML = ICON_SEND;
674
  function scrollToBottom() {
675
  chatScroll.scrollTop = chatScroll.scrollHeight;
676
  }
 
677
  function setBtnState(state) {
678
  if (state === 'sending') {
679
- mainBtn.innerHTML = ICON_STOP;
680
  isGenerating = true;
681
  } else {
682
- mainBtn.innerHTML = ICON_SEND;
683
  isGenerating = false;
684
  abortController = null;
685
  }
686
  }
 
687
  function handleBtnClick() {
688
  if (isGenerating) {
689
  stopGeneration();
@@ -691,6 +868,7 @@ function handleBtnClick() {
691
  sendMessage();
692
  }
693
  }
 
694
  function stopGeneration() {
695
  if (abortController) abortController.abort();
696
  if (typingTimeout) clearTimeout(typingTimeout);
@@ -701,16 +879,20 @@ function stopGeneration() {
701
  setBtnState('idle');
702
  userInput.focus();
703
  }
 
704
  async function sendMessage(textOverride = null) {
705
  const text = textOverride || userInput.value.trim();
706
- if (!text) return;
 
707
  lastUserPrompt = text;
708
  if (!textOverride) {
709
  userInput.value = '';
710
  addMessage(text, 'user');
711
  }
 
712
  setBtnState('sending');
713
  abortController = new AbortController();
 
714
  const botRow = document.createElement('div');
715
  botRow.className = 'msg-row bot';
716
  const avatar = document.createElement('div');
@@ -724,17 +906,35 @@ async function sendMessage(textOverride = null) {
724
  botRow.appendChild(wrapper);
725
  chatScroll.appendChild(botRow);
726
  scrollToBottom();
 
727
  try {
728
  const response = await fetch('/generate', {
729
  method: 'POST',
730
  headers: { 'Content-Type': 'application/json' },
731
- body: JSON.stringify({ text: text, max_tokens: 150, temperature: 0.7 }),
 
 
 
 
 
 
732
  signal: abortController.signal
733
  });
 
734
  const data = await response.json();
735
  if (!isGenerating) return;
 
736
  avatar.classList.remove('pulsing');
737
  const reply = data.reply || "No entendí eso.";
 
 
 
 
 
 
 
 
 
738
  await typeWriter(msgText, reply);
739
  if (isGenerating) {
740
  addActions(wrapper, reply);
@@ -745,12 +945,13 @@ async function sendMessage(textOverride = null) {
745
  msgText.textContent += " [Detenido]";
746
  } else {
747
  avatar.classList.remove('pulsing');
748
- msgText.textContent = "Error de conexión.";
749
  msgText.style.color = "#ff8b8b";
750
  setBtnState('idle');
751
  }
752
  }
753
  }
 
754
  function addMessage(text, sender) {
755
  const row = document.createElement('div');
756
  row.className = `msg-row ${sender}`;
@@ -761,7 +962,8 @@ function addMessage(text, sender) {
761
  chatScroll.appendChild(row);
762
  scrollToBottom();
763
  }
764
- function typeWriter(element, text, speed = 12) {
 
765
  return new Promise(resolve => {
766
  let i = 0;
767
  element.classList.add('typing-cursor');
@@ -784,30 +986,32 @@ function typeWriter(element, text, speed = 12) {
784
  type();
785
  });
786
  }
 
787
  function addActions(wrapperElement, textToCopy) {
788
  const actionsDiv = document.createElement('div');
789
  actionsDiv.className = 'bot-actions';
 
790
  const copyBtn = document.createElement('button');
791
  copyBtn.className = 'action-btn';
792
- copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>`;
793
- copyBtn.onclick = () => {
794
- navigator.clipboard.writeText(textToCopy);
795
- };
796
  const regenBtn = document.createElement('button');
797
  regenBtn.className = 'action-btn';
798
- regenBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>`;
799
- regenBtn.onclick = () => {
800
- sendMessage(lastUserPrompt);
801
- };
802
  actionsDiv.appendChild(copyBtn);
803
  actionsDiv.appendChild(regenBtn);
804
  wrapperElement.appendChild(actionsDiv);
805
  requestAnimationFrame(() => actionsDiv.style.opacity = "1");
806
  scrollToBottom();
807
  }
 
808
  userInput.addEventListener('keydown', (e) => {
809
  if (e.key === 'Enter') handleBtnClick();
810
  });
 
811
  window.onload = () => userInput.focus();
812
  </script>
813
  </body>
@@ -816,13 +1020,7 @@ window.onload = () => userInput.focus();
816
 
817
  if __name__ == "__main__":
818
  port = int(os.environ.get("PORT", 7860))
819
- print(f"\n🚀 Iniciando servidor MTP en puerto {port}...")
820
- print(f"🌐 Interfaz web: http://0.0.0.0:{port}")
821
- print(f"📡 API docs: http://0.0.0.0:{port}/docs")
822
 
823
- uvicorn.run(
824
- app,
825
- host="0.0.0.0",
826
- port=port,
827
- log_level="info"
828
- )
 
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):
 
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",
 
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
 
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,
 
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,
 
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)
 
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
  # ======================
 
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":
 
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
 
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():
 
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">
 
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 {
 
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;
 
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 {
 
764
  padding: 10px 0;
765
  }
766
  #mainBtn {
767
+ background: var(--accent-color);
768
+ color: white;
769
  border: none;
770
  width: 36px;
771
  height: 36px;
 
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;
 
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; }
 
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>
 
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>
 
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();
 
868
  sendMessage();
869
  }
870
  }
871
+
872
  function stopGeneration() {
873
  if (abortController) abortController.abort();
874
  if (typingTimeout) clearTimeout(typingTimeout);
 
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');
 
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);
 
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}`;
 
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');
 
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>
 
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")