import os import re import pdfplumber import gradio as gr from dotenv import load_dotenv from huggingface_hub import hf_hub_download, list_repo_files from openai import OpenAI # --- Excepciones (compatibles con distintas versiones del SDK) --- try: from openai import ( APIConnectionError as _APIConnectionError, APIStatusError as _APIStatusError, RateLimitError as _RateLimitError, AuthenticationError as _AuthenticationError, APITimeoutError as _APITimeoutError, ) except Exception: _APIConnectionError = _APIStatusError = _RateLimitError = _AuthenticationError = _APITimeoutError = Exception # ------------------------------------------------------------ # CONFIG: OpenAI # ------------------------------------------------------------ load_dotenv() client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), timeout=30, # evita cuelgues largos max_retries=1, # sin reintentos largos ) # ------------------------------------------------------------ # SYSTEM PROMPT # ------------------------------------------------------------ system_prompt = """ Eres un Asistente de IA especializado en Auditoría Interna, conforme a las Normas del IIA. Apoyas en análisis, planeación, ejecución y documentación de auditorías y en la preparación para el CIA. Responde con rigor técnico, ejemplos claros y lenguaje profesional. Si la consulta menciona un PDF, integra fragmentos pertinentes del documento. """ # ------------------------------------------------------------ # CARGA DE PDFs (dataset en Hugging Face) # ------------------------------------------------------------ REPO_ID = "vecervantes89/auditoria_interna_pdfs" REPO_TYPE = "dataset" def extract_pdf_text(local_path: str) -> str: parts = [] with pdfplumber.open(local_path) as pdf: for p in pdf.pages: parts.append(p.extract_text() or "") return "\n".join(parts) def load_hf_pdfs_text(repo_id: str, repo_type: str = "dataset"): try: files = [f for f in list_repo_files(repo_id=repo_id, repo_type=repo_type) if f.lower().endswith(".pdf")] except Exception as e: print(f"[ERROR] No se pudo listar '{repo_id}': {e}") return {"files": [], "by_name": {}, "all_text": ""} entries = [] for f in files: try: path = hf_hub_download(repo_id=repo_id, filename=f, repo_type=repo_type) text = extract_pdf_text(path) entries.append({"name": f, "text": text}) print(f"[OK] Cargado {f}") except Exception as e: print(f"[ERROR] Falló la carga de {f}: {e}") by_name = {e["name"]: e["text"] for e in entries} all_text = "\n\n".join(e["text"] for e in entries) print(f"[INFO] Se cargaron {len(entries)} PDFs desde {repo_id}.") return {"files": entries, "by_name": by_name, "all_text": all_text} HF_DOCS = load_hf_pdfs_text(REPO_ID, REPO_TYPE) # ------------------------------------------------------------ # BÚSQUEDA SIMPLE DE CONTEXTO # ------------------------------------------------------------ def buscar_mejor_fragmento(pregunta: str, docs: dict, max_chars: int = 3000): q = (pregunta or "").lower() # 1) Si menciona explícitamente un nombre de archivo for name, text in docs.get("by_name", {}).items(): if name.lower() in q: return name, (text or "")[:max_chars] # 2) Coincidencia por términos tokens = [t for t in re.findall(r"[a-záéíóúüñ0-9]+", q) if len(t) > 2] best_name, best_score, best_text = "", 0, "" for e in docs.get("files", []): t = (e.get("text") or "").lower() s = sum(t.count(tok) for tok in tokens) if s > best_score: best_score, best_name, best_text = s, e.get("name", ""), e.get("text", "") return (best_name, (best_text or "")[:max_chars]) if best_score > 0 else ("", "") # ------------------------------------------------------------ # HANDLER DEL CHAT (type="messages") # ------------------------------------------------------------ def responder(user_text: str, history: list | None): try: history = history or [] history.append({"role": "user", "content": user_text}) # Contexto desde PDFs nombre_pdf, fragmento = buscar_mejor_fragmento(user_text, HF_DOCS) if fragmento: contenido_usuario = ( f"El siguiente texto proviene del documento '{nombre_pdf}'. " "Úsalo como contexto y responde de forma clara, breve y profesional:\n\n" f"{fragmento}\n\n" f"Pregunta del usuario:\n{user_text}" ) else: contenido_usuario = user_text # Construimos los mensajes: system + historial (sin el último) + user contextualizado mensajes = [{"role": "system", "content": system_prompt}] + history[:-1] + [ {"role": "user", "content": contenido_usuario} ] # Modelo ligero para Spaces gratis resp = client.chat.completions.create( model="gpt-4o-mini", messages=mensajes, temperature=0.3, ) bot = resp.choices[0].message.content history.append({"role": "assistant", "content": bot}) return "", history except _AuthenticationError: history.append({"role": "assistant", "content": "⚠️ Error de autenticación con OpenAI.\nRevisa **OPENAI_API_KEY** en Settings → Variables."}) return "", history except _APIConnectionError: history.append({"role": "assistant", "content": "⚠️ Error de conexión saliente.\nActiva **Allow internet access** en Settings → Runtime/Networking."}) return "", history except _RateLimitError: history.append({"role": "assistant", "content": "⚠️ Límite/ cuota de OpenAI alcanzado. Intenta más tarde o cambia de modelo."}) return "", history except _APITimeoutError: history.append({"role": "assistant", "content": "⚠️ La solicitud a OpenAI excedió el tiempo de espera. Intenta de nuevo."}) return "", history except _APIStatusError as e: history.append({"role": "assistant", "content": f"⚠️ Error de API: {e}"} ) return "", history except Exception as e: history.append({"role": "assistant", "content": f"⚠️ Error inesperado: {e}"} ) return "", history def limpiar_chat(): return [] # Chatbot(type="messages") espera una lista de dicts # ------------------------------------------------------------ # UI GRADIO # ------------------------------------------------------------ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo: gr.HTML("""
Logo

Asistente IA de Auditoría Interna

Basado en GPT-4o y los valores del IIA y Gentera

""") chat = gr.Chatbot( label="Chat Asistente Auditoría", type="messages", value=[{"role": "assistant", "content": "¡Hola! Soy tu Asistente IA de Auditoría Interna. ¿En qué te ayudo hoy?"}] ) msg = gr.Textbox(placeholder="Escribe tu consulta aquí...", label="Tu mensaje") clear = gr.Button("🧹 Limpiar chat") msg.submit(responder, inputs=[msg, chat], outputs=[msg, chat]) clear.click(fn=limpiar_chat, inputs=None, outputs=chat) gr.HTML("

© 2025 Gentera AI · Desarrollado por Verónica Cervantes

") # Lanzar en Spaces (sin share, sin puerto fijo) demo.launch(server_name="0.0.0.0", ssr_mode=False)