Safe / app.py
SFL046's picture
Update app.py
8f8cf07 verified
Raw History Blame Contribute Delete
8.07 kB
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("""
<div class="privacy-banner">
🔒 <strong>Sesión Efímera:</strong> Esta conversación existe SOLO en memoria RAM.
Al cerrar/recargar la pestaña, todos los datos se eliminan permanentemente.
No se usa base de datos ni almacenamiento persistente.
</div>
""")
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
)