import os import json import tempfile import warnings import gradio as gr from openai import OpenAI from datetime import datetime # ========================================== # SUPRESIÓN DE LOGS COSMÉTICOS (PYTHON 3.13) # ========================================== warnings.filterwarnings("ignore", category=RuntimeWarning, module="asyncio") warnings.filterwarnings("ignore", message=".*Invalid file descriptor.*") # ========================================== # CONFIGURACIÓN DE API (DASHSCOPE INTL) # ========================================== BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" MODEL_NAME = "qwen-plus" QWEN_API_KEY = os.environ.get("QWEN_API_KEY") if not QWEN_API_KEY: raise ValueError( "⚠️ ERROR CRÍTICO: QWEN_API_KEY no encontrada.\n" "Por favor, añade tu clave en Settings > Repository secrets del Space." ) client = OpenAI( api_key=QWEN_API_KEY, base_url=BASE_URL ) SYSTEM_PROMPT = """Eres un asistente experto en programación. Tus capacidades incluyen: explicar código, depurar errores, generar ejemplos y optimizar funciones. REGLAS ESTRICTAS: 1. NO ejecutes código localmente. Solo analízalo o genéralo. 2. Sé conciso, técnico y usa bloques de código con sintaxis resaltada. 3. Si el usuario sube un documento, analízalo exclusivamente en memoria. 4. Respeta la privacidad: no solicites ni almacenes datos personales.""" # ========================================== # LÓGICA DEL BOT # ========================================== def chat_with_qwen(history, uploaded_file): messages = [{"role": "system", "content": SYSTEM_PROMPT}] file_context = "" if uploaded_file is not None: try: with open(uploaded_file.name, "r", encoding="utf-8") as f: content = f.read() file_context = f"\n\n[CONTEXTO DEL ARCHIVO SUBIDO]:\n{content[:15000]}" except UnicodeDecodeError: file_context = "\n\n[ERROR: El archivo subido no es texto plano legible.]" except Exception as e: file_context = f"\n\n[ERROR LEYENDO ARCHIVO: {str(e)}]" for i, msg in enumerate(history): content = msg["content"] if i == len(history) - 1 and msg["role"] == "user": content += file_context messages.append({"role": msg["role"], "content": content}) try: response = client.chat.completions.create( model=MODEL_NAME, messages=messages, temperature=0.7, max_tokens=2048 ) return response.choices[0].message.content except Exception as e: return f"❌ Error en API de Qwen: {str(e)}" def export_conversation(history): if not history: gr.Warning("No hay conversación para exportar.") return gr.update(visible=False) export_data = { "metadata": { "exported_at": datetime.now().isoformat(), "bot_type": "Qwen Programming Assistant", "privacy_note": "This file was generated in-memory and never stored on server." }, "conversation": history } json_str = json.dumps(export_data, indent=2, ensure_ascii=False) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") with tempfile.NamedTemporaryFile( mode='w', suffix=f'_{timestamp}.json', prefix='qwen_session_', delete=False, encoding='utf-8' ) as f: f.write(json_str) return gr.update(value=f.name, visible=True) def clear_chat(): return [], None, None, gr.update(visible=False) # ========================================== # INTERFAZ GRADIO (PALETA DE COLORES "SAFE") # ========================================== CUSTOM_CSS = """ /* Fondo general de la aplicación */ body, .gradio-container { background-color: #0b0f17 !important; } /* Banner de Privacidad Estilo Safe CyberSecurity */ .privacy-banner { background: #111827; color: #9ca3af; padding: 16px; border-radius: 8px; margin-bottom: 20px; font-size: 0.95em; text-align: center; border: 1px solid #1f2937; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } .privacy-banner strong { color: #10b981; /* Verde esmeralda de acento */ } /* Burbujas del Chatbot */ .gradio-container .message.user { background-color: #1f2937 !important; color: #f9fafb !important; border: 1px solid #374151 !important; } .gradio-container .message.bot { background-color: #111827 !important; color: #e5e7eb !important; border: 1px solid #1f2937 !important; } /* Bloques de código embebidos */ .gradio-container pre, .gradio-container code { background-color: #030712 !important; border: 1px solid #1f2937 !important; } /* Cajas de texto y Textareas */ textarea { background-color: #111827 !important; color: #f9fafb !important; border: 1px solid #1f2937 !important; } textarea:focus { border-color: #10b981 !important; } """ # Configuración limpia del tema preestablecido (Emerald/Zinc) safe_theme = gr.themes.Soft( primary_hue="emerald", # Verde esmeralda para los botones principales y focos neutral_hue="zinc", # Escala de grises oscuros profundos y limpios ).set( body_background_fill="#0b0f17", block_background_fill="#111827", block_border_color="#1f2937", button_primary_background_fill="#10b981", button_primary_background_fill_hover="#059669", button_primary_text_color="#ffffff" ) with gr.Blocks(title="Safe - isolated IA assistent", theme=safe_theme, css=CUSTOM_CSS) as demo: gr.HTML("""
""") chatbot = gr.Chatbot(height=450, label="Safe - isolated IA assistent") msg_input = gr.Textbox( placeholder="Pega tu código, describe un error o pide una explicación...", lines=2, show_label=False ) send_btn = gr.Button("💬 Enviar", variant="primary", size="lg") file_upload = gr.File( label="📎 Subir documento/código (Opcional)", file_types=[".py", ".js", ".ts", ".java", ".cpp", ".txt", ".md", ".json"], height=110 ) with gr.Row(): clear_btn = gr.Button("🗑️ Limpiar Conversación", variant="stop") export_btn = gr.Button("📥 Exportar JSON", variant="secondary") download_file = gr.File(label="📥 Descargar sesión exportada", visible=False) # === EVENT HANDLERS === def user_submit(message, history, file): if not message.strip() and file is None: return "", history, file display_msg = message if message.strip() else f"[Archivo subido: {file.name}]" history = history + [{"role": "user", "content": display_msg}] return "", history, file def bot_respond(history, file): if not history: return history response = chat_with_qwen(history, file) history = history + [{"role": "assistant", "content": response}] return history send_btn.click( user_submit, [msg_input, chatbot, file_upload], [msg_input, chatbot, file_upload] ).then( bot_respond, [chatbot, file_upload], chatbot ) msg_input.submit( user_submit, [msg_input, chatbot, file_upload], [msg_input, chatbot, file_upload] ).then( bot_respond, [chatbot, file_upload], chatbot ) clear_btn.click( clear_chat, outputs=[chatbot, msg_input, file_upload, download_file] ) export_btn.click( export_conversation, inputs=[chatbot], outputs=[download_file] ) # ========================================== # PUNTO DE ENTRADA # ========================================== if __name__ == "__main__": demo.launch( server_name="0.0.0.0", share=False )