import gradio as gr import torch from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM import openai import requests import logging import threading from datetime import datetime import re import time # Configuración de logging más detallada logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ========== PROMPT GENERATOR ========== class PromptGenerator: @staticmethod def detect_intent(prompt: str) -> dict: """Detecta la intención del usuario basado en el prompt""" try: prompt_lower = prompt.lower().strip() # Detección de código code_keywords = [ 'código', 'code', 'programa', 'function', 'def ', 'import ', 'python', 'javascript', 'java', 'c++', 'html', 'css', 'sql', 'algoritmo', 'loop', 'for ', 'while ', 'if ', 'else', 'variable', 'clase', 'class ', 'función', 'method' ] is_code = any(keyword in prompt_lower for keyword in code_keywords) # Detección de tipo de consulta if any(word in prompt_lower for word in ['explica', 'explicar', 'qué es', 'qué son', 'defin']): intent_type = 'explication' elif any(word in prompt_lower for word in ['ejemplo', 'ejemplifica', 'muestra']): intent_type = 'example' elif any(word in prompt_lower for word in ['corrige', 'error', 'bug', 'problema']): intent_type = 'correction' else: intent_type = 'general' return { 'is_code': is_code, 'type': intent_type, 'language': PromptGenerator._detect_language(prompt_lower) } except Exception as e: logger.error(f"Error en detect_intent: {e}") return {'is_code': False, 'type': 'general', 'language': 'unknown'} @staticmethod def _detect_language(prompt: str) -> str: """Detecta el lenguaje de programación mencionado""" try: languages = { 'python': ['python', 'py'], 'javascript': ['javascript', 'js', 'node'], 'java': ['java'], 'html': ['html'], 'css': ['css'], 'sql': ['sql', 'mysql', 'postgresql'], 'c++': ['c++', 'cpp'], 'c#': ['c#', 'csharp'] } for lang, keywords in languages.items(): if any(keyword in prompt for keyword in keywords): return lang return 'unknown' except Exception as e: logger.error(f"Error en _detect_language: {e}") return 'unknown' @staticmethod def enhance_prompt(original_prompt: str, intent: dict) -> str: """Mejora el prompt basado en la intención detectada""" try: if intent['is_code']: if intent['type'] == 'explication': return f'Explica detalladamente este código: {original_prompt}' elif intent['type'] == 'example': lang = intent['language'] if intent['language'] != 'unknown' else 'Python' return f'Da un ejemplo en {lang}: {original_prompt}' elif intent['type'] == 'correction': return f'Corrige este código: {original_prompt}' else: return f'Responde sobre programación: {original_prompt}' else: if intent['type'] == 'explication': return f'Explica claramente: {original_prompt}' elif intent['type'] == 'example': return f'Proporciona ejemplos: {original_prompt}' else: return original_prompt except Exception as e: logger.error(f"Error en enhance_prompt: {e}") return original_prompt # ========== MODEL MANAGER ========== class ModelManager: def __init__(self): self.dialo_model = None self.code_model = None self.dialo_tokenizer = None self.code_tokenizer = None self.loaded = False self.config = {} self.load_attempted = False def load_models(self): """Carga los modelos locales en CPU optimizado para HF""" if self.load_attempted: return self.loaded self.load_attempted = True try: logger.info('🚀 Cargando modelos locales en CPU...') # Cargar solo DialoGPT para simplificar y evitar problemas logger.info('📥 Cargando DialoGPT-small...') self.dialo_model = pipeline( "text-generation", model="microsoft/DialoGPT-small", device="cpu", torch_dtype=torch.float32, model_kwargs={"low_cpu_mem_usage": True} ) self.loaded = True logger.info('✅ DialoGPT-small cargado exitosamente') return True except Exception as e: logger.error(f'❌ Error cargando modelos: {e}') self.loaded = False return False def set_config(self, config): """Configuración para APIs externas""" self.config = config or {} def generate_local_response(self, prompt, is_code=False, max_length=100): """Genera respuesta usando modelos locales optimizados""" if not self.loaded: if not self.load_models(): return '❌ Error: No se pudieron cargar los modelos locales. Intenta recargar la página.' try: logger.info(f'🤖 Generando respuesta local para: {prompt[:50]}...') # Usar DialoGPT para todas las respuestas (más simple) result = self.dialo_model( prompt, max_length=max_length, num_return_sequences=1, temperature=0.7, do_sample=True, pad_token_id=self.dialo_model.tokenizer.eos_token_id, early_stopping=True ) response = result[0]['generated_text'] # Limpiar la respuesta if response.startswith(prompt): response = response[len(prompt):].strip() logger.info(f'✅ Respuesta local generada: {response[:50]}...') return response if response else '🤔 No pude generar una respuesta. Intenta reformular tu pregunta.' except Exception as e: logger.error(f'❌ Error en generación local: {e}') return f'⚠️ Error temporal en el modelo: {str(e)}' # ========== API AGENT ========== class APIAgent: def __init__(self): self.config = {} def set_config(self, config): """Configura las claves API desde Gradio""" self.config = config or {} def call_openai(self, prompt: str, is_code: bool = False): """Intenta llamar a OpenAI API""" api_key = self.config.get('openai_key') if not api_key: return None try: logger.info('🔄 Intentando llamar a OpenAI API...') openai.api_key = api_key model = 'gpt-3.5-turbo' max_tokens = self.config.get('max_tokens', 400) temperature = self.config.get('temperature', 0.7) response = openai.ChatCompletion.create( model=model, messages=[{'role': 'user', 'content': prompt}], max_tokens=max_tokens, temperature=temperature, timeout=10 ) logger.info('✅ OpenAI API respondió exitosamente') return response.choices[0].message.content.strip() except Exception as e: logger.error(f'❌ Error llamando a OpenAI: {e}') return None def call_deepseek(self, prompt: str, is_code: bool = False): """Intenta llamar a DeepSeek API""" api_key = self.config.get('deepseek_key') if not api_key: return None try: logger.info('🔄 Intentando llamar a DeepSeek API...') url = 'https://api.deepseek.com/v1/chat/completions' headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}' } max_tokens = self.config.get('max_tokens', 400) temperature = self.config.get('temperature', 0.7) data = { 'model': 'deepseek-chat', 'messages': [{'role': 'user', 'content': prompt}], 'max_tokens': max_tokens, 'temperature': temperature, 'stream': False } response = requests.post(url, json=data, headers=headers, timeout=15) response.raise_for_status() result = response.json() logger.info('✅ DeepSeek API respondió exitosamente') return result['choices'][0]['message']['content'].strip() except Exception as e: logger.error(f'❌ Error llamando a DeepSeek: {e}') return None def generate_response(self, prompt: str, is_code: bool = False): """ Intenta generar respuesta usando APIs en orden de preferencia Returns: dict con response y source """ # Verificar si hay claves configuradas has_deepseek = bool(self.config.get('deepseek_key')) has_openai = bool(self.config.get('openai_key')) logger.info(f'🔍 APIs disponibles - DeepSeek: {has_deepseek}, OpenAI: {has_openai}') if has_deepseek: deepseek_response = self.call_deepseek(prompt, is_code) if deepseek_response: return {'response': deepseek_response, 'source': 'deepseek'} if has_openai: openai_response = self.call_openai(prompt, is_code) if openai_response: return {'response': openai_response, 'source': 'openai'} logger.info('🔍 Ninguna API disponible, usando modelo local') return {'response': None, 'source': 'none'} # ========== CHATBOT PRINCIPAL ========== class BATUTOChatbot: def __init__(self): self.conversation_history = [] self.config = { 'deepseek_key': '', 'openai_key': '', 'max_tokens': 400, 'temperature': 0.7 } self.model_manager = ModelManager() self.api_agent = APIAgent() self.prompt_generator = PromptGenerator() def update_config(self, deepseek_key, openai_key, max_tokens, temperature): """Actualiza la configuración desde la UI""" updated = False if deepseek_key: self.config['deepseek_key'] = deepseek_key updated = True logger.info('🔑 DeepSeek API Key actualizada') if openai_key: self.config['openai_key'] = openai_key updated = True logger.info('🔑 OpenAI API Key actualizada') if max_tokens: self.config['max_tokens'] = int(max_tokens) updated = True if temperature: self.config['temperature'] = float(temperature) updated = True # Actualizar agentes self.model_manager.set_config(self.config) self.api_agent.set_config(self.config) return '✅ Configuración actualizada' if updated else 'ℹ️ Sin cambios' def get_system_status(self): """Obtiene el estado del sistema""" has_deepseek = bool(self.config.get('deepseek_key')) has_openai = bool(self.config.get('openai_key')) models_loaded = self.model_manager.loaded status_html = f'''

Estado del Sistema

Modelos locales: {'✅ Cargados' if models_loaded else '🔄 Cargando...'}

DeepSeek API: {'✅ Configurada' if has_deepseek else '❌ No configurada'}

OpenAI API: {'✅ Configurada' if has_openai else '❌ No configurada'}

Mensajes en sesión: {len(self.conversation_history)}

''' return status_html def chat_response(self, message, history): """Genera respuesta del chatbot optimizado para HF""" if not message.strip(): return '' # Mostrar indicador de typing yield '🔄 Procesando...' try: # Detectar intención y mejorar prompt intent = self.prompt_generator.detect_intent(message) enhanced_prompt = self.prompt_generator.enhance_prompt(message, intent) # Intentar usar APIs primero api_result = self.api_agent.generate_response(enhanced_prompt, intent['is_code']) if api_result['response']: # Usar respuesta de API response_text = api_result['response'] source = api_result['source'] else: # Usar modelo local como fallback response_text = self.model_manager.generate_local_response( enhanced_prompt, intent['is_code'], max_length=150 ) source = 'local' # Agregar metadata a la respuesta metadata = f'\n\n---\n🔧 *Fuente: {source.upper()}*' if intent['is_code']: metadata += f' | 💻 *Tipo: Código*' else: metadata += f' | 💬 *Tipo: Conversación*' full_response = response_text + metadata # Guardar en historial self.conversation_history.append({ 'timestamp': datetime.now().isoformat(), 'user': message, 'bot': response_text, 'source': source, 'intent': intent }) yield full_response except Exception as e: error_msg = f'❌ Error: {str(e)}' logger.error(f'Error en chat_response: {e}') yield error_msg def clear_conversation(self): """Limpia la conversación""" self.conversation_history.clear() return None, [] # ========== CONFIGURACIÓN GRADIO ========== # Crear instancia del chatbot chatbot = BATUTOChatbot() # Cargar modelos al inicio (async) def load_models_async(): logger.info('Cargando modelos en segundo plano...') chatbot.model_manager.load_models() logger.info('Modelos cargados exitosamente') # Iniciar carga de modelos model_loader = threading.Thread(target=load_models_async, daemon=True) model_loader.start() # Configuración de la interfaz Gradio para HF with gr.Blocks( title='BATUTO Chatbot - Asistente Educativo', theme=gr.themes.Soft() ) as demo: gr.Markdown(''' # BATUTO Chatbot - Asistente Educativo **Sistema inteligente con modelos locales y APIs externas** *Desplegado en Hugging Face Spaces - Versión Optimizada* ''') with gr.Row(): with gr.Column(scale=2): # Área de chat gr.Markdown('### 💬 Conversación') chatbot_interface = gr.Chatbot( label='Chat con BATUTO', height=400, show_copy_button=True ) msg = gr.Textbox( label='Escribe tu mensaje', placeholder='Pregunta sobre programación, explica conceptos, pide ejemplos...', lines=2 ) with gr.Row(): submit_btn = gr.Button('🚀 Enviar', variant='primary') clear_btn = gr.Button('🧹 Limpiar', variant='secondary') with gr.Column(scale=1): # Panel de estado gr.Markdown('### 📊 Estado del Sistema') status_display = gr.HTML() # Configuración rápida with gr.Accordion('⚙️ Configuración Rápida', open=False): deepseek_key = gr.Textbox( label='DeepSeek API Key', type='password', placeholder='sk-...' ) openai_key = gr.Textbox( label='OpenAI API Key', type='password', placeholder='sk-...' ) with gr.Row(): max_tokens = gr.Slider( label='Tokens máx', minimum=100, maximum=800, value=400, step=50 ) temperature = gr.Slider( label='Temperatura', minimum=0.1, maximum=1.0, value=0.7, step=0.1 ) save_config_btn = gr.Button('💾 Guardar Config', size='sm') config_output = gr.Textbox(label='Estado', interactive=False) # Información with gr.Accordion('ℹ️ Cómo usar', open=True): gr.Markdown(''' **Ejemplos:** - Muéstrame una función Python para ordenar listas - Explica qué es machine learning - Corrige este código: [tu código] ''') # Event handlers def handle_submit(message, history): if not message.strip(): return '', history return '', history + [[message, None]] # Conectar el botón de enviar submit_btn.click( handle_submit, inputs=[msg, chatbot_interface], outputs=[msg, chatbot_interface] ).then( chatbot.chat_response, inputs=[msg, chatbot_interface], outputs=[chatbot_interface] ) # Enter también envía msg.submit( handle_submit, inputs=[msg, chatbot_interface], outputs=[msg, chatbot_interface] ).then( chatbot.chat_response, inputs=[msg, chatbot_interface], outputs=[chatbot_interface] ) # Limpiar chat clear_btn.click( chatbot.clear_conversation, outputs=[msg, chatbot_interface] ) # Configuración save_config_btn.click( chatbot.update_config, inputs=[deepseek_key, openai_key, max_tokens, temperature], outputs=[config_output] ).then( chatbot.get_system_status, outputs=[status_display] ) # Actualizar estado al cargar demo.load( chatbot.get_system_status, outputs=[status_display] ) if __name__ == '__main__': demo.launch()