| | import os |
| | import sys |
| | import torch |
| | import pickle |
| | import time |
| | import gc |
| | import asyncio |
| | import aiohttp |
| | from typing import Optional, Dict, List |
| | from fastapi import FastAPI, Request |
| | from fastapi.responses import HTMLResponse, StreamingResponse |
| | from fastapi.middleware.cors import CORSMiddleware |
| | from pydantic import BaseModel, Field |
| | from huggingface_hub import snapshot_download |
| | import uvicorn |
| | import wikipedia |
| | from duckduckgo_search import DDGS |
| |
|
| | |
| | |
| | |
| | if torch.cuda.is_available(): |
| | DEVICE = "cuda" |
| | print("✅ GPU NVIDIA detectada. Usando CUDA.") |
| | else: |
| | DEVICE = "cpu" |
| | print("⚠️ GPU no detectada. Usando CPU (puede ser más lento).") |
| |
|
| | if DEVICE == "cpu": |
| | torch.set_num_threads(max(1, os.cpu_count() // 2)) |
| |
|
| | torch.set_grad_enabled(False) |
| |
|
| | MODEL_REPO = "TeszenAI/MTPw" |
| |
|
| | |
| | |
| | |
| | class WebSearchEngine: |
| | """Motor de búsqueda integrado con Wikipedia y DuckDuckGo""" |
| | |
| | def __init__(self): |
| | self.ddgs = DDGS() |
| | wikipedia.set_lang('es') |
| | |
| | async def search_wikipedia(self, query: str, sentences: int = 4) -> Optional[Dict]: |
| | """Buscar en Wikipedia""" |
| | try: |
| | search_results = wikipedia.search(query, results=3) |
| | if not search_results: |
| | return None |
| | |
| | page = wikipedia.page(search_results[0], auto_suggest=False) |
| | summary = wikipedia.summary(search_results[0], sentences=sentences) |
| | |
| | return { |
| | "source": "Wikipedia", |
| | "title": page.title, |
| | "url": page.url, |
| | "summary": summary, |
| | "success": True |
| | } |
| | except wikipedia.exceptions.DisambiguationError as e: |
| | try: |
| | page = wikipedia.page(e.options[0], auto_suggest=False) |
| | summary = wikipedia.summary(e.options[0], sentences=sentences) |
| | return { |
| | "source": "Wikipedia", |
| | "title": page.title, |
| | "url": page.url, |
| | "summary": summary, |
| | "success": True |
| | } |
| | except: |
| | return None |
| | except: |
| | return None |
| | |
| | async def search_duckduckgo(self, query: str, max_results: int = 5) -> List[Dict]: |
| | """Buscar en DuckDuckGo""" |
| | try: |
| | results = [] |
| | ddg_results = self.ddgs.text(query, max_results=max_results) |
| | |
| | for r in ddg_results: |
| | results.append({ |
| | "title": r.get("title", ""), |
| | "url": r.get("href", ""), |
| | "snippet": r.get("body", "") |
| | }) |
| | |
| | return results |
| | except Exception as e: |
| | print(f"Error en DuckDuckGo: {e}") |
| | return [] |
| | |
| | async def search(self, query: str) -> Dict: |
| | """Búsqueda combinada""" |
| | wiki_task = self.search_wikipedia(query) |
| | ddg_task = self.search_duckduckgo(query) |
| | |
| | wiki_result, ddg_results = await asyncio.gather(wiki_task, ddg_task) |
| | |
| | return { |
| | "query": query, |
| | "wikipedia": wiki_result, |
| | "web_results": ddg_results, |
| | "timestamp": time.time() |
| | } |
| |
|
| | |
| | search_engine = WebSearchEngine() |
| |
|
| | |
| | |
| | |
| | print(f"📦 Descargando modelo desde {MODEL_REPO}...") |
| | repo_path = snapshot_download( |
| | repo_id=MODEL_REPO, |
| | repo_type="model", |
| | local_dir="mtptz_repo" |
| | ) |
| |
|
| | sys.path.insert(0, repo_path) |
| |
|
| | from model import MTPMiniModel |
| | from tokenizer import MTPTokenizer |
| |
|
| | print("🔧 Cargando tensores y configuración...") |
| | with open(os.path.join(repo_path, "mtp_mini.pkl"), "rb") as f: |
| | model_data = pickle.load(f) |
| |
|
| | tokenizer = MTPTokenizer(os.path.join(repo_path, "mtp_tokenizer.model")) |
| | VOCAB_SIZE = tokenizer.sp.get_piece_size() |
| | config = model_data["config"] |
| |
|
| | use_swiglu = config["model"].get("use_swiglu", False) |
| |
|
| | print(f"🧠 Inicializando modelo...") |
| | print(f" → Vocabulario: {VOCAB_SIZE}") |
| | print(f" → Dimensión: {config['model']['d_model']}") |
| | print(f" → Capas: {config['model']['n_layers']}") |
| | print(f" → Cabezas: {config['model']['n_heads']}") |
| | print(f" → SwiGLU: {'✓' if use_swiglu else '✗'}") |
| |
|
| | model = MTPMiniModel( |
| | vocab_size=VOCAB_SIZE, |
| | d_model=config["model"]["d_model"], |
| | n_layers=config["model"]["n_layers"], |
| | n_heads=config["model"]["n_heads"], |
| | d_ff=config["model"]["d_ff"], |
| | max_seq_len=config["model"]["max_seq_len"], |
| | dropout=0.0, |
| | use_swiglu=use_swiglu |
| | ) |
| |
|
| | model.load_state_dict(model_data["model_state_dict"]) |
| | model.eval() |
| |
|
| | if DEVICE == "cpu": |
| | print("⚡ Aplicando cuantización dinámica para CPU...") |
| | model = torch.quantization.quantize_dynamic( |
| | model, |
| | {torch.nn.Linear}, |
| | dtype=torch.qint8 |
| | ) |
| |
|
| | model.to(DEVICE) |
| |
|
| | param_count = sum(p.numel() for p in model.parameters()) |
| | print(f"✅ Modelo cargado: {param_count:,} parámetros ({param_count/1e6:.1f}M)") |
| | print(f"🔍 Motor de búsqueda web inicializado (Wikipedia + DuckDuckGo)") |
| |
|
| | |
| | |
| | |
| | app = FastAPI( |
| | title="MTP-3.5 Enhanced API", |
| | description="API mejorada con capacidades de búsqueda web (Wikipedia + DuckDuckGo)", |
| | version="3.5-web" |
| | ) |
| |
|
| | app.add_middleware( |
| | CORSMiddleware, |
| | allow_origins=["*"], |
| | allow_methods=["*"], |
| | allow_headers=["*"], |
| | ) |
| |
|
| | class PromptRequest(BaseModel): |
| | text: str = Field(..., max_length=2000, description="Texto de entrada") |
| | max_tokens: int = Field(default=150, ge=10, le=300, description="Tokens máximos a generar") |
| | temperature: float = Field(default=0.7, ge=0.1, le=2.0, description="Temperatura de muestreo") |
| | top_k: int = Field(default=40, ge=1, le=100, description="Top-k sampling") |
| | top_p: float = Field(default=0.92, ge=0.1, le=1.0, description="Top-p (nucleus) sampling") |
| | repetition_penalty: float = Field(default=1.15, ge=1.0, le=2.0, description="Penalización por repetición") |
| | min_length: int = Field(default=20, ge=5, le=100, description="Longitud mínima de respuesta") |
| | use_web_search: bool = Field(default=False, description="Activar búsqueda web") |
| |
|
| | class SearchRequest(BaseModel): |
| | query: str = Field(..., max_length=500, description="Consulta de búsqueda") |
| |
|
| | def build_prompt(user_input: str, web_context: Optional[str] = None) -> str: |
| | """Construye el prompt con contexto web opcional""" |
| | if web_context: |
| | |
| | return f"""### Instrucción del sistema: |
| | Eres un asistente que busca información en internet. Debes resumir la información encontrada de forma clara y útil. |
| | |
| | ### Información encontrada en la web: |
| | {web_context} |
| | |
| | ### Pregunta del usuario: |
| | {user_input} |
| | |
| | ### Respuesta: |
| | Hola, encontré esto en la web: |
| | |
| | """ |
| | return f"### Instrucción:\n{user_input}\n\n### Respuesta:\n" |
| |
|
| | def format_search_results(search_data: Dict) -> str: |
| | """Formatea resultados de búsqueda para el contexto del modelo""" |
| | context_parts = [] |
| | |
| | if search_data.get("wikipedia") and search_data["wikipedia"].get("success"): |
| | wiki = search_data["wikipedia"] |
| | context_parts.append(f"[Wikipedia - {wiki['title']}]\n{wiki['summary']}\nFuente: {wiki['url']}") |
| | |
| | if search_data.get("web_results"): |
| | for i, result in enumerate(search_data["web_results"][:4], 1): |
| | snippet = result['snippet'][:300].strip() |
| | context_parts.append(f"[Resultado {i}: {result['title']}]\n{snippet}\nFuente: {result['url']}") |
| | |
| | return "\n\n".join(context_parts) if context_parts else "" |
| |
|
| | |
| | |
| | |
| | ACTIVE_REQUESTS = 0 |
| | MAX_CONCURRENT_REQUESTS = 3 |
| |
|
| | @app.post("/search") |
| | async def web_search(req: SearchRequest): |
| | """Endpoint de búsqueda web""" |
| | try: |
| | search_results = await search_engine.search(req.query) |
| | |
| | formatted_context = format_search_results(search_results) |
| | |
| | return { |
| | "query": req.query, |
| | "results": search_results, |
| | "formatted_context": formatted_context, |
| | "has_results": bool(formatted_context), |
| | "sources_used": [] |
| | } |
| | except Exception as e: |
| | print(f"Error en búsqueda: {e}") |
| | return { |
| | "query": req.query, |
| | "error": str(e), |
| | "has_results": False |
| | } |
| |
|
| | @app.post("/generate") |
| | async def generate(req: PromptRequest): |
| | """Endpoint principal con búsqueda web integrada""" |
| | global ACTIVE_REQUESTS |
| | |
| | if ACTIVE_REQUESTS >= MAX_CONCURRENT_REQUESTS: |
| | return { |
| | "reply": "El servidor está ocupado. Por favor, intenta de nuevo en unos segundos.", |
| | "error": "too_many_requests", |
| | "active_requests": ACTIVE_REQUESTS |
| | } |
| | |
| | ACTIVE_REQUESTS += 1 |
| | |
| | dyn_max_tokens = req.max_tokens |
| | dyn_temperature = req.temperature |
| | |
| | if ACTIVE_REQUESTS > 1: |
| | print(f"⚠️ Carga alta ({ACTIVE_REQUESTS} requests). Ajustando parámetros.") |
| | dyn_max_tokens = min(dyn_max_tokens, 120) |
| | dyn_temperature = max(0.6, dyn_temperature * 0.95) |
| |
|
| | user_input = req.text.strip() |
| | if not user_input: |
| | ACTIVE_REQUESTS -= 1 |
| | return {"reply": "", "tokens_generated": 0} |
| |
|
| | web_context = "" |
| | search_results = None |
| | |
| | |
| | if req.use_web_search: |
| | try: |
| | |
| | search_query = user_input |
| | |
| | |
| | if len(user_input.split()) > 8: |
| | |
| | words = user_input.lower().split() |
| | |
| | stop_words = {'qué', 'cuál', 'cómo', 'dónde', 'cuándo', 'por', 'para', 'el', 'la', 'los', 'las', 'un', 'una', 'es', 'sobre', 'me', 'puedes', 'explicar', 'decir', 'información'} |
| | keywords = [w for w in words if w not in stop_words][:5] |
| | search_query = ' '.join(keywords) |
| | |
| | search_results = await search_engine.search(search_query) |
| | web_context = format_search_results(search_results) |
| | |
| | if web_context: |
| | print(f"🔍 Búsqueda web completada para: '{search_query}'") |
| | print(f" Contexto agregado: {len(web_context)} caracteres") |
| | except Exception as e: |
| | print(f"Error en búsqueda web: {e}") |
| |
|
| | full_prompt = build_prompt(user_input, web_context if web_context else None) |
| | tokens = [tokenizer.bos_id()] + tokenizer.encode(full_prompt) |
| | input_ids = torch.tensor([tokens], device=DEVICE) |
| |
|
| | try: |
| | start_time = time.time() |
| | |
| | with torch.no_grad(): |
| | output_ids = model.generate( |
| | input_ids, |
| | max_new_tokens=dyn_max_tokens, |
| | temperature=dyn_temperature, |
| | top_k=req.top_k, |
| | top_p=req.top_p, |
| | repetition_penalty=req.repetition_penalty, |
| | min_length=req.min_length, |
| | eos_token_id=tokenizer.eos_id() |
| | ) |
| |
|
| | gen_tokens = output_ids[0, len(tokens):].tolist() |
| | |
| | safe_tokens = [] |
| | for t in gen_tokens: |
| | if 0 <= t < VOCAB_SIZE and t != tokenizer.eos_id(): |
| | safe_tokens.append(t) |
| | elif t == tokenizer.eos_id(): |
| | break |
| | |
| | response = tokenizer.decode(safe_tokens).strip() |
| | |
| | if "###" in response: |
| | response = response.split("###")[0].strip() |
| | |
| | if response.endswith(("...", ". . .", "…")): |
| | response = response.rstrip(".") |
| | |
| | generation_time = time.time() - start_time |
| | tokens_per_second = len(safe_tokens) / generation_time if generation_time > 0 else 0 |
| |
|
| | result = { |
| | "reply": response, |
| | "tokens_generated": len(safe_tokens), |
| | "generation_time": round(generation_time, 2), |
| | "tokens_per_second": round(tokens_per_second, 1), |
| | "model": "MTP-3.5", |
| | "device": DEVICE, |
| | "web_search_used": req.use_web_search |
| | } |
| | |
| | if req.use_web_search and search_results: |
| | sources = [] |
| | if search_results.get("wikipedia") and search_results["wikipedia"].get("success"): |
| | sources.append({ |
| | "type": "wikipedia", |
| | "title": search_results["wikipedia"]["title"], |
| | "url": search_results["wikipedia"]["url"] |
| | }) |
| | |
| | if search_results.get("web_results"): |
| | for r in search_results["web_results"][:3]: |
| | sources.append({ |
| | "type": "web", |
| | "title": r["title"], |
| | "url": r["url"] |
| | }) |
| | |
| | result["sources"] = sources |
| | result["search_query"] = user_input |
| |
|
| | return result |
| | |
| | except Exception as e: |
| | print(f"❌ Error durante generación: {e}") |
| | import traceback |
| | traceback.print_exc() |
| | return { |
| | "reply": "Lo siento, ocurrió un error al procesar tu solicitud.", |
| | "error": str(e) |
| | } |
| | |
| | finally: |
| | ACTIVE_REQUESTS -= 1 |
| | if DEVICE == "cuda": |
| | torch.cuda.empty_cache() |
| | gc.collect() |
| |
|
| | |
| | |
| | |
| | @app.get("/health") |
| | def health_check(): |
| | """Check del estado del servicio""" |
| | memory_info = {} |
| | if DEVICE == "cuda": |
| | memory_info = { |
| | "gpu_memory_allocated_mb": round(torch.cuda.memory_allocated() / 1024**2, 2), |
| | "gpu_memory_reserved_mb": round(torch.cuda.memory_reserved() / 1024**2, 2) |
| | } |
| | |
| | return { |
| | "status": "healthy", |
| | "model": "MTP-3.5-Web", |
| | "device": DEVICE, |
| | "active_requests": ACTIVE_REQUESTS, |
| | "max_concurrent_requests": MAX_CONCURRENT_REQUESTS, |
| | "vocab_size": VOCAB_SIZE, |
| | "parameters": sum(p.numel() for p in model.parameters()), |
| | "web_search_enabled": True, |
| | **memory_info |
| | } |
| |
|
| | @app.get("/info") |
| | def model_info(): |
| | """Información detallada del modelo""" |
| | improvements = [ |
| | "RoPE (Rotary Position Embedding)", |
| | "RMSNorm (Root Mean Square Normalization)", |
| | "Label Smoothing (0.1)", |
| | "Repetition Penalty", |
| | "Early Stopping", |
| | "EOS Loss Weight", |
| | "Length Control", |
| | "Gradient Accumulation", |
| | "Web Search Integration (Wikipedia + DuckDuckGo)" |
| | ] |
| | |
| | if config["model"].get("use_swiglu", False): |
| | improvements.append("SwiGLU Activation") |
| | |
| | return { |
| | "model_name": "MTP-3.5-Web", |
| | "version": "3.5-web", |
| | "architecture": { |
| | "d_model": config["model"]["d_model"], |
| | "n_layers": config["model"]["n_layers"], |
| | "n_heads": config["model"]["n_heads"], |
| | "d_ff": config["model"]["d_ff"], |
| | "max_seq_len": config["model"]["max_seq_len"], |
| | "vocab_size": VOCAB_SIZE, |
| | "use_swiglu": config["model"].get("use_swiglu", False), |
| | "dropout": config["model"]["dropout"] |
| | }, |
| | "parameters": sum(p.numel() for p in model.parameters()), |
| | "parameters_human": f"{sum(p.numel() for p in model.parameters())/1e6:.1f}M", |
| | "device": DEVICE, |
| | "improvements": improvements, |
| | "web_search": { |
| | "enabled": True, |
| | "sources": ["Wikipedia (ES)", "DuckDuckGo"] |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | @app.get("/", response_class=HTMLResponse) |
| | def chat_ui(): |
| | return """ |
| | <!DOCTYPE html> |
| | <html lang="es"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| | <title>MTP 3.5 Web - Chat Interface</title> |
| | <link rel="preconnect" href="https://fonts.googleapis.com"> |
| | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| | <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"> |
| | <style> |
| | :root { |
| | --bg-color: #131314; |
| | --surface-color: #1E1F20; |
| | --accent-color: #4a9eff; |
| | --text-primary: #e3e3e3; |
| | --text-secondary: #9aa0a6; |
| | --user-bubble: #282a2c; |
| | --success-color: #34a853; |
| | --warning-color: #fbbc04; |
| | --error-color: #ea4335; |
| | --search-active: #ff6b35; |
| | --logo-url: url('https://i.postimg.cc/yxS54PF3/IMG-3082.jpg'); |
| | } |
| | * { |
| | box-sizing: border-box; |
| | outline: none; |
| | -webkit-tap-highlight-color: transparent; |
| | } |
| | body { |
| | margin: 0; |
| | background-color: var(--bg-color); |
| | font-family: 'Inter', sans-serif; |
| | color: var(--text-primary); |
| | height: 100dvh; |
| | display: flex; |
| | flex-direction: column; |
| | overflow: hidden; |
| | } |
| | header { |
| | padding: 12px 20px; |
| | display: flex; |
| | align-items: center; |
| | justify-content: space-between; |
| | background: rgba(19, 19, 20, 0.85); |
| | backdrop-filter: blur(12px); |
| | position: fixed; |
| | top: 0; |
| | width: 100%; |
| | z-index: 50; |
| | border-bottom: 1px solid rgba(255,255,255,0.05); |
| | } |
| | .brand-wrapper { |
| | display: flex; |
| | align-items: center; |
| | gap: 12px; |
| | cursor: pointer; |
| | } |
| | .brand-logo { |
| | width: 32px; |
| | height: 32px; |
| | border-radius: 50%; |
| | background-image: var(--logo-url); |
| | background-size: cover; |
| | background-position: center; |
| | border: 1px solid rgba(255,255,255,0.1); |
| | } |
| | .brand-text { |
| | font-weight: 500; |
| | font-size: 1.05rem; |
| | display: flex; |
| | align-items: center; |
| | gap: 8px; |
| | } |
| | .version-badge { |
| | font-size: 0.75rem; |
| | background: rgba(74, 158, 255, 0.15); |
| | color: #8ab4f8; |
| | padding: 2px 8px; |
| | border-radius: 12px; |
| | font-weight: 600; |
| | } |
| | .web-badge { |
| | font-size: 0.7rem; |
| | background: rgba(255, 107, 53, 0.15); |
| | color: #ff8a65; |
| | padding: 2px 6px; |
| | border-radius: 8px; |
| | font-weight: 600; |
| | } |
| | .status-indicator { |
| | width: 8px; |
| | height: 8px; |
| | border-radius: 50%; |
| | background: var(--success-color); |
| | animation: pulse 2s infinite; |
| | } |
| | @keyframes pulse { |
| | 0%, 100% { opacity: 1; } |
| | 50% { opacity: 0.5; } |
| | } |
| | .chat-scroll { |
| | flex: 1; |
| | overflow-y: auto; |
| | padding: 80px 20px 40px 20px; |
| | display: flex; |
| | flex-direction: column; |
| | gap: 30px; |
| | max-width: 850px; |
| | margin: 0 auto; |
| | width: 100%; |
| | scroll-behavior: smooth; |
| | } |
| | .msg-row { |
| | display: flex; |
| | gap: 16px; |
| | width: 100%; |
| | opacity: 0; |
| | transform: translateY(10px); |
| | animation: slideUpFade 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards; |
| | } |
| | .msg-row.user { justify-content: flex-end; } |
| | .msg-row.bot { justify-content: flex-start; align-items: flex-start; } |
| | .msg-content { |
| | line-height: 1.6; |
| | font-size: 1rem; |
| | word-wrap: break-word; |
| | max-width: 85%; |
| | } |
| | .user .msg-content { |
| | background-color: var(--user-bubble); |
| | padding: 10px 18px; |
| | border-radius: 18px; |
| | border-top-right-radius: 4px; |
| | color: #fff; |
| | } |
| | .bot .msg-content-wrapper { |
| | display: flex; |
| | flex-direction: column; |
| | gap: 8px; |
| | width: 100%; |
| | } |
| | .bot .msg-text { |
| | padding-top: 6px; |
| | color: var(--text-primary); |
| | white-space: pre-wrap; |
| | } |
| | .bot-avatar { |
| | width: 34px; |
| | height: 34px; |
| | min-width: 34px; |
| | border-radius: 50%; |
| | background-image: var(--logo-url); |
| | background-size: cover; |
| | box-shadow: 0 2px 6px rgba(0,0,0,0.2); |
| | } |
| | .search-indicator { |
| | display: inline-flex; |
| | align-items: center; |
| | gap: 6px; |
| | font-size: 0.85rem; |
| | color: var(--search-active); |
| | padding: 4px 10px; |
| | background: rgba(255, 107, 53, 0.1); |
| | border-radius: 12px; |
| | margin-bottom: 8px; |
| | } |
| | .search-indicator svg { |
| | width: 14px; |
| | height: 14px; |
| | fill: currentColor; |
| | animation: spin 1s linear infinite; |
| | } |
| | @keyframes spin { |
| | from { transform: rotate(0deg); } |
| | to { transform: rotate(360deg); } |
| | } |
| | .sources-list { |
| | margin-top: 10px; |
| | padding: 10px; |
| | background: rgba(74, 158, 255, 0.05); |
| | border-left: 3px solid var(--accent-color); |
| | border-radius: 6px; |
| | font-size: 0.85rem; |
| | } |
| | .sources-list .source-title { |
| | font-weight: 600; |
| | color: var(--accent-color); |
| | margin-bottom: 6px; |
| | } |
| | .sources-list a { |
| | color: #8ab4f8; |
| | text-decoration: none; |
| | display: block; |
| | margin: 4px 0; |
| | transition: color 0.2s; |
| | } |
| | .sources-list a:hover { |
| | color: var(--accent-color); |
| | text-decoration: underline; |
| | } |
| | .bot-actions { |
| | display: flex; |
| | gap: 10px; |
| | opacity: 0; |
| | transition: opacity 0.3s; |
| | margin-top: 5px; |
| | } |
| | .action-btn { |
| | background: transparent; |
| | border: none; |
| | color: var(--text-secondary); |
| | cursor: pointer; |
| | padding: 4px; |
| | border-radius: 4px; |
| | display: flex; |
| | align-items: center; |
| | transition: color 0.2s, background 0.2s; |
| | font-size: 0.85rem; |
| | } |
| | .action-btn:hover { |
| | color: var(--text-primary); |
| | background: rgba(255,255,255,0.08); |
| | } |
| | .action-btn svg { |
| | width: 16px; |
| | height: 16px; |
| | fill: currentColor; |
| | margin-right: 4px; |
| | } |
| | .typing-cursor::after { |
| | content: ''; |
| | display: inline-block; |
| | width: 10px; |
| | height: 10px; |
| | background: var(--accent-color); |
| | border-radius: 50%; |
| | margin-left: 5px; |
| | vertical-align: middle; |
| | animation: blink 1s infinite; |
| | } |
| | .footer-container { |
| | padding: 0 20px 20px 20px; |
| | background: linear-gradient(to top, var(--bg-color) 85%, transparent); |
| | position: relative; |
| | z-index: 60; |
| | } |
| | .search-toggle-wrapper { |
| | max-width: 850px; |
| | margin: 0 auto 10px auto; |
| | display: flex; |
| | align-items: center; |
| | gap: 10px; |
| | padding: 8px 16px; |
| | background: var(--surface-color); |
| | border-radius: 16px; |
| | border: 1px solid rgba(255,255,255,0.08); |
| | } |
| | .search-toggle { |
| | display: flex; |
| | align-items: center; |
| | gap: 8px; |
| | cursor: pointer; |
| | user-select: none; |
| | } |
| | .toggle-switch { |
| | position: relative; |
| | width: 40px; |
| | height: 22px; |
| | background: rgba(255,255,255,0.1); |
| | border-radius: 11px; |
| | transition: background 0.3s; |
| | } |
| | .toggle-switch.active { |
| | background: var(--search-active); |
| | } |
| | .toggle-switch::after { |
| | content: ''; |
| | position: absolute; |
| | width: 18px; |
| | height: 18px; |
| | background: white; |
| | border-radius: 50%; |
| | top: 2px; |
| | left: 2px; |
| | transition: left 0.3s; |
| | } |
| | .toggle-switch.active::after { |
| | left: 20px; |
| | } |
| | .toggle-label { |
| | font-size: 0.9rem; |
| | color: var(--text-secondary); |
| | display: flex; |
| | align-items: center; |
| | gap: 6px; |
| | } |
| | .toggle-label svg { |
| | width: 16px; |
| | height: 16px; |
| | fill: currentColor; |
| | } |
| | .toggle-switch.active + .toggle-label { |
| | color: var(--search-active); |
| | } |
| | .input-box { |
| | max-width: 850px; |
| | margin: 0 auto; |
| | background: var(--surface-color); |
| | border-radius: 28px; |
| | padding: 8px 10px 8px 20px; |
| | display: flex; |
| | align-items: center; |
| | border: 1px solid rgba(255,255,255,0.1); |
| | transition: border-color 0.2s, box-shadow 0.2s; |
| | } |
| | .input-box:focus-within { |
| | border-color: rgba(74, 158, 255, 0.5); |
| | box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.1); |
| | } |
| | #userInput { |
| | flex: 1; |
| | background: transparent; |
| | border: none; |
| | color: white; |
| | font-size: 1rem; |
| | font-family: inherit; |
| | padding: 10px 0; |
| | resize: none; |
| | max-height: 120px; |
| | } |
| | #mainBtn { |
| | background: white; |
| | color: black; |
| | border: none; |
| | width: 36px; |
| | height: 36px; |
| | border-radius: 50%; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | cursor: pointer; |
| | margin-left: 8px; |
| | transition: transform 0.2s; |
| | } |
| | #mainBtn:hover { transform: scale(1.05); } |
| | #mainBtn:disabled { |
| | opacity: 0.5; |
| | cursor: not-allowed; |
| | } |
| | .disclaimer { |
| | text-align: center; |
| | font-size: 0.75rem; |
| | color: #666; |
| | margin-top: 12px; |
| | } |
| | .stats-badge { |
| | font-size: 0.7rem; |
| | color: var(--text-secondary); |
| | margin-top: 4px; |
| | font-family: 'Monaco', monospace; |
| | } |
| | @keyframes slideUpFade { |
| | from { opacity: 0; transform: translateY(15px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | @keyframes blink { |
| | 0%, 100% { opacity: 1; } |
| | 50% { opacity: 0; } |
| | } |
| | @keyframes pulseAvatar { |
| | 0% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.4); } |
| | 70% { box-shadow: 0 0 0 8px rgba(74, 158, 255, 0); } |
| | 100% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0); } |
| | } |
| | .pulsing { animation: pulseAvatar 1.5s infinite; } |
| | ::-webkit-scrollbar { width: 8px; } |
| | ::-webkit-scrollbar-track { background: transparent; } |
| | ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; } |
| | </style> |
| | </head> |
| | <body> |
| | <header> |
| | <div class="brand-wrapper" onclick="location.reload()"> |
| | <div class="brand-logo"></div> |
| | <div class="brand-text"> |
| | MTP <span class="version-badge">3.5</span> <span class="web-badge">WEB</span> |
| | </div> |
| | </div> |
| | <div class="status-indicator" title="Sistema operativo"></div> |
| | </header> |
| | <div id="chatScroll" class="chat-scroll"> |
| | <div class="msg-row bot" style="animation-delay: 0.1s;"> |
| | <div class="bot-avatar"></div> |
| | <div class="msg-content-wrapper"> |
| | <div class="msg-text">¡Hola! Soy MTP 3.5 Web, un modelo mejorado con capacidades de búsqueda en internet. |
| | |
| | 🔍 Nueva función: Búsqueda Web |
| | • Activa el botón "Búsqueda Web" para que pueda buscar información en Wikipedia y DuckDuckGo |
| | • Te proporcionaré respuestas basadas en información actualizada de internet |
| | • Incluiré las fuentes consultadas en cada respuesta |
| | |
| | Características del modelo: |
| | • RoPE (Rotary Position Embedding) |
| | • RMSNorm para estabilidad |
| | • Control de repetición inteligente |
| | • Generación coherente y fluida |
| | |
| | ¿En qué puedo ayudarte hoy?</div> |
| | </div> |
| | </div> |
| | </div> |
| | <div class="footer-container"> |
| | <div class="search-toggle-wrapper"> |
| | <div class="search-toggle" onclick="toggleWebSearch()"> |
| | <div class="toggle-switch" id="searchToggle"></div> |
| | <div class="toggle-label"> |
| | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | <circle cx="11" cy="11" r="8"></circle> |
| | <path d="m21 21-4.35-4.35"></path> |
| | </svg> |
| | <span id="searchLabel">Búsqueda Web: Desactivada</span> |
| | </div> |
| | </div> |
| | </div> |
| | <div class="input-box"> |
| | <textarea id="userInput" placeholder="Escribe un mensaje..." rows="1" autocomplete="off"></textarea> |
| | <button id="mainBtn" onclick="handleBtnClick()"></button> |
| | </div> |
| | <div class="disclaimer"> |
| | MTP 3.5 Web puede cometer errores. Verifica la información importante en las fuentes originales. |
| | </div> |
| | </div> |
| | <script> |
| | const chatScroll = document.getElementById('chatScroll'); |
| | const userInput = document.getElementById('userInput'); |
| | const mainBtn = document.getElementById('mainBtn'); |
| | const searchToggle = document.getElementById('searchToggle'); |
| | const searchLabel = document.getElementById('searchLabel'); |
| | |
| | let isGenerating = false; |
| | let webSearchEnabled = false; |
| | let lastUserPrompt = ""; |
| | |
| | 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>`; |
| | const ICON_STOP = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="2" y="2" width="20" height="20" rx="4"></rect></svg>`; |
| | mainBtn.innerHTML = ICON_SEND; |
| | |
| | userInput.addEventListener('input', function() { |
| | this.style.height = 'auto'; |
| | this.style.height = Math.min(this.scrollHeight, 120) + 'px'; |
| | }); |
| | |
| | function toggleWebSearch() { |
| | webSearchEnabled = !webSearchEnabled; |
| | searchToggle.classList.toggle('active', webSearchEnabled); |
| | searchLabel.textContent = webSearchEnabled ? 'Búsqueda Web: Activada' : 'Búsqueda Web: Desactivada'; |
| | } |
| | |
| | function scrollToBottom() { |
| | chatScroll.scrollTop = chatScroll.scrollHeight; |
| | } |
| | |
| | function setBtnState(state) { |
| | if (state === 'sending') { |
| | mainBtn.innerHTML = ICON_STOP; |
| | mainBtn.disabled = false; |
| | isGenerating = true; |
| | } else if (state === 'disabled') { |
| | mainBtn.disabled = true; |
| | isGenerating = false; |
| | } else { |
| | mainBtn.innerHTML = ICON_SEND; |
| | mainBtn.disabled = false; |
| | isGenerating = false; |
| | } |
| | } |
| | |
| | function handleBtnClick() { |
| | if (isGenerating) { |
| | stopGeneration(); |
| | } else { |
| | sendMessage(); |
| | } |
| | } |
| | |
| | function stopGeneration() { |
| | const activeCursor = document.querySelector('.typing-cursor'); |
| | if (activeCursor) activeCursor.classList.remove('typing-cursor'); |
| | const activeAvatar = document.querySelector('.pulsing'); |
| | if (activeAvatar) activeAvatar.classList.remove('pulsing'); |
| | setBtnState('idle'); |
| | userInput.focus(); |
| | } |
| | |
| | async function sendMessage(textOverride = null) { |
| | const text = textOverride || userInput.value.trim(); |
| | if (!text) return; |
| | |
| | lastUserPrompt = text; |
| | |
| | if (!textOverride) { |
| | userInput.value = ''; |
| | userInput.style.height = 'auto'; |
| | addMessage(text, 'user'); |
| | } |
| | |
| | setBtnState('sending'); |
| | |
| | const botRow = document.createElement('div'); |
| | botRow.className = 'msg-row bot'; |
| | |
| | const avatar = document.createElement('div'); |
| | avatar.className = 'bot-avatar pulsing'; |
| | |
| | const wrapper = document.createElement('div'); |
| | wrapper.className = 'msg-content-wrapper'; |
| | |
| | if (webSearchEnabled) { |
| | const searchInd = document.createElement('div'); |
| | searchInd.className = 'search-indicator'; |
| | searchInd.innerHTML = ` |
| | <svg viewBox="0 0 24 24"> |
| | <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"></circle> |
| | <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" fill="none"></path> |
| | </svg> |
| | <span>Buscando en la web...</span> |
| | `; |
| | wrapper.appendChild(searchInd); |
| | } |
| | |
| | const msgText = document.createElement('div'); |
| | msgText.className = 'msg-text'; |
| | |
| | wrapper.appendChild(msgText); |
| | botRow.appendChild(avatar); |
| | botRow.appendChild(wrapper); |
| | chatScroll.appendChild(botRow); |
| | scrollToBottom(); |
| | |
| | try { |
| | const startTime = performance.now(); |
| | |
| | const response = await fetch('/generate', { |
| | method: 'POST', |
| | headers: { 'Content-Type': 'application/json' }, |
| | body: JSON.stringify({ |
| | text: text, |
| | max_tokens: 200, |
| | temperature: 0.7, |
| | top_k: 40, |
| | top_p: 0.92, |
| | repetition_penalty: 1.15, |
| | min_length: 20, |
| | use_web_search: webSearchEnabled |
| | }) |
| | }); |
| | |
| | const data = await response.json(); |
| | |
| | if (!isGenerating) return; |
| | |
| | avatar.classList.remove('pulsing'); |
| | |
| | const searchInd = wrapper.querySelector('.search-indicator'); |
| | if (searchInd) searchInd.remove(); |
| | |
| | if (data.error) { |
| | msgText.innerHTML = `<span style="color: var(--error-color);">Error: ${data.error}</span>`; |
| | setBtnState('idle'); |
| | return; |
| | } |
| | |
| | const reply = data.reply || "No entendí eso."; |
| | const endTime = performance.now(); |
| | const totalTime = ((endTime - startTime) / 1000).toFixed(2); |
| | |
| | await typeWriter(msgText, reply); |
| | |
| | if (isGenerating) { |
| | if (data.sources && data.sources.length > 0) { |
| | const sourcesDiv = document.createElement('div'); |
| | sourcesDiv.className = 'sources-list'; |
| | sourcesDiv.innerHTML = '<div class="source-title">📚 Fuentes consultadas:</div>'; |
| | |
| | data.sources.forEach(source => { |
| | const link = document.createElement('a'); |
| | link.href = source.url; |
| | link.target = '_blank'; |
| | link.textContent = `• ${source.title}`; |
| | sourcesDiv.appendChild(link); |
| | }); |
| | |
| | wrapper.appendChild(sourcesDiv); |
| | } |
| | |
| | const stats = document.createElement('div'); |
| | stats.className = 'stats-badge'; |
| | let statsText = `${data.tokens_generated} tokens • ${data.tokens_per_second} t/s • ${totalTime}s • ${data.device}`; |
| | if (data.web_search_used) { |
| | statsText += ' • 🔍 Web'; |
| | } |
| | stats.textContent = statsText; |
| | wrapper.appendChild(stats); |
| | |
| | addActions(wrapper, reply); |
| | setBtnState('idle'); |
| | } |
| | } catch (error) { |
| | console.error('Error:', error); |
| | avatar.classList.remove('pulsing'); |
| | msgText.innerHTML = `<span style="color: var(--error-color);">Error de conexión. Por favor, intenta de nuevo.</span>`; |
| | setBtnState('idle'); |
| | } |
| | } |
| | |
| | function addMessage(text, sender) { |
| | const row = document.createElement('div'); |
| | row.className = `msg-row ${sender}`; |
| | |
| | const content = document.createElement('div'); |
| | content.className = 'msg-content'; |
| | content.textContent = text; |
| | |
| | row.appendChild(content); |
| | chatScroll.appendChild(row); |
| | scrollToBottom(); |
| | } |
| | |
| | function typeWriter(element, text, speed = 12) { |
| | return new Promise(resolve => { |
| | let i = 0; |
| | element.classList.add('typing-cursor'); |
| | |
| | function type() { |
| | if (!isGenerating) { |
| | element.classList.remove('typing-cursor'); |
| | resolve(); |
| | return; |
| | } |
| | |
| | if (i < text.length) { |
| | element.textContent += text.charAt(i); |
| | i++; |
| | scrollToBottom(); |
| | setTimeout(type, speed + Math.random() * 5); |
| | } else { |
| | element.classList.remove('typing-cursor'); |
| | resolve(); |
| | } |
| | } |
| | |
| | type(); |
| | }); |
| | } |
| | |
| | function addActions(wrapperElement, textToCopy) { |
| | const actionsDiv = document.createElement('div'); |
| | actionsDiv.className = 'bot-actions'; |
| | |
| | const copyBtn = document.createElement('button'); |
| | copyBtn.className = 'action-btn'; |
| | 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>Copiar`; |
| | copyBtn.onclick = () => { |
| | navigator.clipboard.writeText(textToCopy).then(() => { |
| | copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Copiado`; |
| | setTimeout(() => { |
| | 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>Copiar`; |
| | }, 2000); |
| | }); |
| | }; |
| | |
| | const regenBtn = document.createElement('button'); |
| | regenBtn.className = 'action-btn'; |
| | 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>Regenerar`; |
| | regenBtn.onclick = () => { |
| | sendMessage(lastUserPrompt); |
| | }; |
| | |
| | actionsDiv.appendChild(copyBtn); |
| | actionsDiv.appendChild(regenBtn); |
| | wrapperElement.appendChild(actionsDiv); |
| | |
| | requestAnimationFrame(() => actionsDiv.style.opacity = "1"); |
| | scrollToBottom(); |
| | } |
| | |
| | userInput.addEventListener('keydown', (e) => { |
| | if (e.key === 'Enter' && !e.shiftKey) { |
| | e.preventDefault(); |
| | handleBtnClick(); |
| | } |
| | }); |
| | |
| | window.onload = () => { |
| | userInput.focus(); |
| | |
| | fetch('/info') |
| | .then(r => r.json()) |
| | .then(data => { |
| | console.log('Modelo cargado:', data); |
| | }) |
| | .catch(e => console.error('Error cargando info:', e)); |
| | }; |
| | </script> |
| | </body> |
| | </html> |
| | """ |
| |
|
| | if __name__ == "__main__": |
| | port = int(os.environ.get("PORT", 7860)) |
| | print(f"\n🚀 Iniciando servidor MTP-3.5 Web...") |
| | print(f"🌐 Interfaz web: http://0.0.0.0:{port}") |
| | print(f"📡 API docs: http://0.0.0.0:{port}/docs") |
| | print(f"📊 Health check: http://0.0.0.0:{port}/health") |
| | print(f"ℹ️ Model info: http://0.0.0.0:{port}/info") |
| | print(f"🔍 Búsqueda web: Wikipedia + DuckDuckGo") |
| | print(f"\n✅ Sistema listo. Presiona Ctrl+C para detener.") |
| | |
| | uvicorn.run( |
| | app, |
| | host="0.0.0.0", |
| | port=port, |
| | log_level="info" |
| | ) |