File size: 14,606 Bytes
f426f44
 
e87005e
ca2062e
b6e544f
f66981a
f426f44
f66981a
fcfd3dc
f426f44
 
f8d4cc1
96c5a47
f66981a
52b303c
f426f44
 
 
8ddbf9f
 
 
 
 
 
f426f44
ff356c7
52b303c
f426f44
52b303c
 
 
b6e544f
f426f44
fcfd3dc
 
f426f44
ff356c7
8b43585
ff356c7
ca2062e
 
87c8442
b6e544f
 
 
fcfd3dc
 
 
 
 
 
 
 
 
 
 
 
52b303c
fcfd3dc
b6e544f
fcfd3dc
b6e544f
 
 
fcfd3dc
 
b6e544f
fcfd3dc
 
 
 
 
 
 
 
 
 
b6e544f
fcfd3dc
 
 
b6e544f
fcfd3dc
 
 
 
 
 
 
 
 
 
87c8442
b6e544f
f66981a
7c6f44f
 
 
 
87c8442
fcfd3dc
f8d4cc1
fcfd3dc
f8d4cc1
 
fcfd3dc
 
 
 
b6e544f
87c8442
b6e544f
fcfd3dc
 
 
 
52b303c
fcfd3dc
 
 
52b303c
f66981a
 
 
 
 
 
 
 
52b303c
f66981a
 
 
 
 
 
52b303c
f66981a
 
 
 
 
52b303c
f66981a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f426f44
 
f8d4cc1
f426f44
 
 
 
 
 
 
f8d4cc1
f426f44
 
fcfd3dc
f8d4cc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52b303c
 
 
 
 
f426f44
 
f8d4cc1
 
 
 
 
 
 
 
cf5d63d
fcfd3dc
 
 
 
 
 
52b303c
f426f44
52b303c
7b0dfb1
f426f44
 
fcfd3dc
f426f44
 
 
f8d4cc1
f426f44
 
fcfd3dc
ff356c7
f8d4cc1
ff356c7
 
 
 
 
fcfd3dc
 
 
 
 
ff356c7
 
52b303c
fcfd3dc
ff356c7
52b303c
ff356c7
fcfd3dc
ff356c7
fcfd3dc
ff356c7
88bb767
ff356c7
f8d4cc1
ff356c7
 
 
 
 
 
 
 
fcfd3dc
ff356c7
fcfd3dc
ff356c7
fcfd3dc
 
f66981a
fcfd3dc
 
 
f66981a
 
fcfd3dc
ff356c7
 
 
f426f44
 
 
 
87c8442
52b303c
f426f44
 
 
f8d4cc1
f426f44
fcfd3dc
 
 
 
 
 
 
 
ca2062e
 
87c8442
ca2062e
fcfd3dc
ca2062e
 
7b0dfb1
ca2062e
 
 
fcfd3dc
 
 
 
f8d4cc1
f426f44
7b0dfb1
f426f44
7c6f44f
f426f44
 
 
 
 
 
 
 
f8d4cc1
f426f44
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import os
import json
import glob
import sys
import time
import uuid
import threading
from pathlib import Path
from collections import deque, defaultdict
import telebot
import gradio as gr
import spaces
from openai import OpenAI
from huggingface_hub import CommitScheduler
from langchain_nvidia_ai_endpoints import NVIDIAEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document

@spaces.GPU
def apreton_de_manos_gpu():
    return "Hugging Face Zero-GPU Satisfecho"

print(f"🤖 {apreton_de_manos_gpu()}")

TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN")
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY")
SYSTEM_PROMPT = os.environ.get("System_Prompt")

# Validación estricta de variables de entorno, incluyendo el System_Prompt
if not TELEGRAM_TOKEN or not NVIDIA_API_KEY or not SYSTEM_PROMPT:
    print("❌ ERROR: Faltan las variables de entorno TELEGRAM_TOKEN, NVIDIA_API_KEY o System_Prompt.")
    sys.exit(1)

# Configuración del bot con un pool de 20 hilos para procesar múltiples usuarios en paralelo
bot = telebot.TeleBot(TELEGRAM_TOKEN, num_threads=20)

nvidia_client = OpenAI(
    base_url="https://integrate.api.nvidia.com/v1",
    api_key=NVIDIA_API_KEY
)

class ControlDeTraficoIA:
    def __init__(self):
        self.lock = threading.Lock()
        
        # Estructuras por usuario individual
        self.peticiones_por_usuario = defaultdict(deque)
        self.tokens_por_usuario = defaultdict(deque)
        
        # Estructuras globales (para proteger la API de NVIDIA)
        self.peticiones_globales = deque()
        self.tokens_globales = deque()
        
        # --- CONFIGURACIÓN DE LÍMITES ---
        self.MAX_USER_RPM = 3       # Máximo 3 peticiones por minuto por usuario
        self.MAX_USER_TPM = 15000   # Máximo 15,000 tokens por minuto por usuario
        
        self.MAX_GLOBAL_RPM = 30    # Límite global seguro
        self.MAX_GLOBAL_TPM = 120000 # Límite global de tokens por minuto entre todos los usuarios

    def verificar_y_esperar(self, user_id, estimado_tokens):
        with self.lock:
            now = time.time()
            
            peticiones_usr = self.peticiones_por_usuario[user_id]
            tokens_usr = self.tokens_por_usuario[user_id]
            
            # Limpieza de registros con antigüedad mayor a 60 segundos
            while peticiones_usr and now - peticiones_usr[0] > 60:
                peticiones_usr.popleft()
            while tokens_usr and now - tokens_usr[0][0] > 60:
                tokens_usr.popleft()
                
            while self.peticiones_globales and now - self.peticiones_globales[0] > 60:
                self.peticiones_globales.popleft()
            while self.tokens_globales and now - self.tokens_globales[0][0] > 60:
                self.tokens_globales.popleft()
            
            # Evaluar límites individuales
            limite_usr_peticiones = len(peticiones_usr) >= self.MAX_USER_RPM
            limite_usr_tokens = (sum(t[1] for t in tokens_usr) + estimado_tokens) > self.MAX_USER_TPM
            
            # Evaluar límites globales de la API
            limite_glob_peticiones = len(self.peticiones_globales) >= self.MAX_GLOBAL_RPM
            limite_glob_tokens = (sum(t[1] for t in self.tokens_globales) + estimado_tokens) > self.MAX_GLOBAL_TPM
            
            # Si ningún límite se sobrepasa, registrar consumo y permitir la solicitud
            if not (limite_usr_peticiones or limite_usr_tokens or limite_glob_peticiones or limite_glob_tokens):
                peticiones_usr.append(now)
                tokens_usr.append((now, estimado_tokens))
                self.peticiones_globales.append(now)
                self.tokens_globales.append((now, estimado_tokens))
                return 0
            
            # Validación preventiva de longitud para evitar errores de tipo 'index out of range'
            espera_usr_pet = int(60 - (now - peticiones_usr[0])) + 1 if (limite_usr_peticiones and len(peticiones_usr) > 0) else 0
            espera_usr_tok = int(60 - (now - tokens_usr[0][0])) + 1 if (limite_usr_tokens and len(tokens_usr) > 0) else 0
            espera_glob_pet = int(60 - (now - self.peticiones_globales[0])) + 1 if (limite_glob_peticiones and len(self.peticiones_globales) > 0) else 0
            espera_glob_tok = int(60 - (now - self.tokens_globales[0][0])) + 1 if (limite_glob_tokens and len(self.tokens_globales) > 0) else 0
            
            return max(espera_usr_pet, espera_usr_tok, espera_glob_pet, espera_glob_tok)

    def registrar_consumo_forzado(self, user_id, estimado_tokens):
        with self.lock:
            now = time.time()
            self.peticiones_por_usuario[user_id].append(now)
            self.tokens_por_usuario[user_id].append((now, estimado_tokens))
            self.peticiones_globales.append(now)
            self.tokens_globales.append((now, estimado_tokens))

control_trafico = ControlDeTraficoIA()

# Control de concurrencia para evitar múltiples solicitudes simultáneas de un mismo usuario
usuarios_procesando = set()
lock_usuarios = threading.Lock()

# Historial de conversación individual en memoria
historial_usuarios = defaultdict(lambda: deque(maxlen=5))
lock_historial = threading.Lock()

# --- ALMACENAMIENTO DE CHATS EN FORMATO JSON TRADICIONAL ---
LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)

LOG_FILE_NAME = f"chat_logs_{uuid.uuid4()}.json"
LOG_FILE_PATH = LOG_DIR / LOG_FILE_NAME

lock_logs = threading.Lock()

# Inicializador asíncrono del scheduler para subir cambios al Dataset
try:
    scheduler = CommitScheduler(
        repo_id="danielgx300/ttdatset",  
        repo_type="dataset",
        folder_path=LOG_DIR,
        path_in_repo="data",
        every=5,
        private=True,
        token=os.environ.get("HF_TOKEN")
    )
    print("✅ CommitScheduler cargado para el dataset 'danielgx300/ttdatset'.")
except Exception as e:
    print(f"⚠️ Advertencia al iniciar CommitScheduler: {e}")
    scheduler = None

def registrar_conversacion_en_dataset(user_id, pregunta, respuesta):
    with lock_logs:
        try:
            log_entry = {
                "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
                "user_id": user_id,
                "pregunta": pregunta,
                "respuesta": respuesta
            }
            
            datos_existentes = []
            if LOG_FILE_PATH.exists() and LOG_FILE_PATH.stat().st_size > 0:
                try:
                    with open(LOG_FILE_PATH, "r", encoding="utf-8") as f:
                        datos_existentes = json.load(f)
                except Exception:
                    datos_existentes = []
            
            datos_existentes.append(log_entry)
            
            with open(LOG_FILE_PATH, "w", encoding="utf-8") as f:
                json.dump(datos_existentes, f, ensure_ascii=False, indent=4)
                
        except Exception as e:
            print(f"❌ Error al escribir log de chat local: {e}")


def parse_telegram_text(text_obj):
    if isinstance(text_obj, str):
        return text_obj
    elif isinstance(text_obj, list):
        out = ""
        for item in text_obj:
            if isinstance(item, str):
                out += item
            elif isinstance(item, dict):
                out += item.get('text', '')
        return out
    return ""

# Carga e indexación de archivos JSON (Arranque inicial)
json_files = glob.glob("result*.json")
print(f"📥 Archivos de historial encontrados: {json_files}")

valid_messages = []
seen_messages = set()

for file_path in sorted(json_files):
    print(f"📖 Leyendo {file_path}...")
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            chat_data = json.load(f)
        
        messages = chat_data.get('messages', [])
        file_messages_count = 0
        
        for msg in messages:
            if msg.get('type') != 'message': 
                continue
            text = parse_telegram_text(msg.get('text', ''))
            if not text.strip(): 
                continue
            
            user = msg.get('from', 'Desconocido')
            date = msg.get('date', '').replace('T', ' ')
            msg_str = f"[{date}] {user}: {text}"
            
            if msg_str not in seen_messages:
                seen_messages.add(msg_str)
                valid_messages.append(msg_str)
                file_messages_count += 1
                
        print(f"✅ Se cargaron {file_messages_count} mensajes únicos desde {file_path}")
    except Exception as e:
        print(f"❌ Error al procesar el archivo {file_path}: {e}")

del seen_messages

CHUNK_SIZE = 30
OVERLAP = 10
documents = []
for i in range(0, len(valid_messages), CHUNK_SIZE - OVERLAP):
    chunk_msgs = valid_messages[i : i + CHUNK_SIZE]
    chunk_text = "\n".join(chunk_msgs)
    documents.append(Document(page_content=chunk_text))

print(f"📊 Total consolidado: {len(documents)} bloques de conversación únicos.")

# --- CARGA DE EMBEDDINGS DESDE NVIDIA NIM ---
print("🚀 Cargando embeddings desde NVIDIA NIM...")
embeddings = NVIDIAEmbeddings(
    model="nvidia/nemotron-3-embed-1b",
    nvidia_api_key=NVIDIA_API_KEY
)

if documents:
    print("🧠 Creando base de datos unificada...")
    vectorstore = FAISS.from_documents(documents, embeddings)
    retriever = vectorstore.as_retriever(search_kwargs={"k": 15})
    print("✅ Base de datos lista.")
else:
    print("⚠️ No hay documentos para indexar.")
    retriever = None

def construir_prompt(context_text, chat_history_list, user_query):
    historial_str = ""
    if chat_history_list:
        historial_str = "\nHISTORIAL DE CONVERSACIÓN RECIENTE CON ESTE USUARIO:\n"
        for user_msg, assistant_msg in chat_history_list:
            historial_str += f"- Usuario: {user_msg}\n- Asistente (tú): {assistant_msg}\n"
        historial_str += "\nNota: Utiliza este historial de conversación únicamente para interpretar pronombres o dar continuidad. Prioriza siempre la información del CONTEXTO DEL CHAT general.\n"

    return f"""{SYSTEM_PROMPT}

CONTEXTO DEL CHAT:
{context_text}
{historial_str}
PREGUNTA DEL USUARIO:
{user_query}

Responde de forma estructurada en texto plano siguiendo estrictamente todas las reglas anteriores:
"""

def obtener_respuesta_rag(user_query, user_id):
    if not retriever:
        return "❌ El sistema de base de datos no está disponible temporalmente."
        
    try:
        docs = retriever.invoke(user_query)
        context_text = "\n\n---\n\n".join([doc.page_content for doc in docs])
        
        # Obtener historial individual del usuario
        with lock_historial:
            chat_history_list = list(historial_usuarios[user_id])
        
        prompt = construir_prompt(context_text, chat_history_list, user_query)
        estimacion_tokens = len(prompt) // 3
        
        # Verificar límites de tráfico
        espera_necesaria = control_trafico.verificar_y_esperar(user_id, estimacion_tokens)
        if espera_necesaria > 0:
            print(f"⚠️ [COLA DE ESPERA - USUARIO {user_id}] Esperando {espera_necesaria} segundos...")
            time.sleep(espera_necesaria)
            control_trafico.registrar_consumo_forzado(user_id, estimacion_tokens)

        print(f"🚀 [BOT] Enviando prompt a NVIDIA NIM para el usuario {user_id}...")
        completion = nvidia_client.chat.completions.create(
            model="thinkingmachines/inkling",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.6,
            top_p=0.95,
            max_tokens=16384,
            extra_body={"chat_template_kwargs": {"thinking": True, "reasoning_effort": "high"}},
            stream=False
        )
        
        reasoning = getattr(completion.choices[0].message, "reasoning", None) or getattr(completion.choices[0].message, "reasoning_content", None)
        if reasoning:
            print(f"\n🧠 === PROCESO DE RAZONAMIENTO - USUARIO {user_id} ===")
            print(reasoning)
            print("==================================================\n")
            
        respuesta_ia = completion.choices[0].message.content
        
        # Guardar interacción en la memoria en caché del usuario
        with lock_historial:
            historial_usuarios[user_id].append((user_query, respuesta_ia))
            
        registrar_conversacion_en_dataset(user_id, user_query, respuesta_ia)
            
        return respuesta_ia
        
    except Exception as e:
        return f"❌ Error en el proceso de consulta: {e}"

@bot.message_handler(commands=['start', 'help'])
def send_welcome(message):
    bienvenida = (
        "¡Hola! Soy tu asistente inteligente del grupo HDL. 🌶️\n\n"
        "Pregúntame lo que quieras sobre el historial de conversaciones consolidado y buscaré la información para ti de inmediato."
    )
    bot.reply_to(message, bienvenida)

@bot.message_handler(func=lambda message: True)
def handle_query(message):
    user_id = message.from_user.id
    
    with lock_usuarios:
        if user_id in usuarios_procesando:
            bot.reply_to(message, "⚠️ Ya estoy procesando una consulta para ti. Por favor, espera a que termine de responderte.")
            return
        usuarios_procesando.add(user_id)
        
    try:
        bot.send_chat_action(message.chat.id, 'typing')
        msg_espera = bot.reply_to(message, "🔍 Buscando en el historial y procesando...")
        
        respuesta_ia = obtener_respuesta_rag(message.text, user_id)
        
        if len(respuesta_ia) > 4000:
            respuesta_ia = respuesta_ia[:4000] + "\n\n⚠️ Respuesta truncada por longitud máxima..."
            
        bot.edit_message_text(chat_id=message.chat.id, message_id=msg_espera.message_id, text=respuesta_ia)
    except Exception as e:
        print(f"❌ Error al gestionar mensaje de Telegram para el usuario {user_id}: {e}")
    finally:
        with lock_usuarios:
            usuarios_procesando.discard(user_id)

def run_telegram_bot():
    print("🤖 Polling del Bot de Telegram activo...")
    try:
        bot.delete_webhook(drop_pending_updates=True)
        bot.infinity_polling()
    except Exception as e:
        print(f"❌ Error en el loop del bot: {e}")

threading.Thread(target=run_telegram_bot, daemon=True).start()

with gr.Blocks() as demo:
    gr.Markdown("# 🌶️ Servidor del Asistente HDL activo")
    gr.Markdown("El bot de Telegram está escuchando preguntas en segundo plano utilizando el modelo de IA.")

demo.launch()