shell1809/Asistente_UNEFA
0
1"""2rag.py — Núcleo del Asistente de Optimización Administrativa3y Orientación Normativa UNEFA Lara4 5Contiene:6- Búsqueda semántica de los fragmentos más relevantes ante una pregunta7- Generación de la respuesta final vía Groq, basada SOLO en esos fragmentos8"""9 10import os11import json12import requests13import numpy as np14from dotenv import load_dotenv15 16load_dotenv()17GEMINI_KEY = os.getenv("GEMINI_API_KEY")18GROQ_KEY = os.getenv("GROQ_API_KEY")19 20if not GEMINI_KEY or not GROQ_KEY:21 raise SystemExit(22 "ERROR: Falta GEMINI_API_KEY o GROQ_API_KEY en el archivo .env"23 )24 25INDICE_DIR = "indice"26CHUNKS_PATH = os.path.join(INDICE_DIR, "chunks.json")27EMBEDDINGS_PATH = os.path.join(INDICE_DIR, "embeddings.npy")28 29if not os.path.exists(CHUNKS_PATH) or not os.path.exists(EMBEDDINGS_PATH):30 raise SystemExit(31 "ERROR: No se encontró el índice. Corre primero 'python indexar.py' "32 "para procesar los PDFs de la carpeta 'documentos/'."33 )34 35with open(CHUNKS_PATH, encoding="utf-8") as f:36 CHUNKS = json.load(f)37EMBEDDINGS = np.load(EMBEDDINGS_PATH)38 39# Prompt del sistema: define el rol institucional del asistente40SYSTEM_PROMPT = """Eres el Asistente de Optimización Administrativa y Orientación Normativa41de la UNEFA Núcleo Lara. Tu función es ayudar a estudiantes, profesores y personal42administrativo a entender y aplicar correctamente los reglamentos, normativas y43procedimientos institucionales, basándote EXCLUSIVAMENTE en los documentos oficiales44que se te proporcionan como contexto.45 46Reglas que debes seguir siempre:471. Responde SOLO con base en el CONTEXTO proporcionado. No inventes artículos,48 procedimientos, plazos ni requisitos que no estén en el contexto.492. Si la información solicitada no aparece en el contexto, dilo claramente:50 indica que no encontraste esa información en los documentos disponibles y51 recomienda consultar directamente con la unidad administrativa correspondiente.523. Cuando cites una norma o procedimiento, menciona el documento fuente y,53 si está disponible, la página o artículo correspondiente.544. Usa un tono formal, claro y orientado a resolver trámites administrativos55 de forma práctica (qué hacer, en qué orden, qué se necesita).565. Si la pregunta es ambigua, pide la aclaración mínima necesaria antes de responder,57 o responde a la interpretación más probable y ofrece la alternativa.58"""59 60 61def embedding_consulta(texto):62 """Genera el embedding de la pregunta del usuario."""63 url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent"64 params = {"key": GEMINI_KEY}65 body = {66 "model": "models/gemini-embedding-001",67 "content": {"parts": [{"text": texto}]},68 "task_type": "RETRIEVAL_QUERY",69 }70 r = requests.post(url, params=params, json=body, timeout=30)71 r.raise_for_status()72 return np.array(r.json()["embedding"]["values"], dtype=np.float32)73 74 75def buscar_relevantes(pregunta, k=5):76 """Devuelve los k fragmentos más relevantes para la pregunta, por similitud coseno."""77 q_emb = embedding_consulta(pregunta)78 79 normas_docs = np.linalg.norm(EMBEDDINGS, axis=1)80 norma_q = np.linalg.norm(q_emb)81 similitudes = (EMBEDDINGS @ q_emb) / (normas_docs * norma_q + 1e-8)82 83 top_indices = np.argsort(similitudes)[::-1][:k]84 resultados = []85 for idx in top_indices:86 item = dict(CHUNKS[idx])87 item["similitud"] = float(similitudes[idx])88 resultados.append(item)89 return resultados90 91 92def generar_respuesta(pregunta, contexto_chunks):93 """Genera la respuesta final vía Groq usando solo el contexto recuperado."""94 contexto = "\n\n---\n\n".join(95 f"[Documento: {c['fuente']} | Fragmento {c['chunk_id']}]\n{c['texto']}"96 for c in contexto_chunks97 )98 99 prompt_usuario = f"""CONTEXTO (extraído de documentos oficiales de UNEFA):100 101{contexto}102 103PREGUNTA DEL USUARIO: {pregunta}104 105Responde siguiendo las reglas del sistema. Cita el documento fuente cuando corresponda."""106 107 url = "https://api.groq.com/openai/v1/chat/completions"108 headers = {109 "Authorization": f"Bearer {GROQ_KEY}",110 "Content-Type": "application/json",111 }112 body = {113 "model": "llama-3.3-70b-versatile",114 "messages": [115 {"role": "system", "content": SYSTEM_PROMPT},116 {"role": "user", "content": prompt_usuario},117 ],118 "temperature": 0.2,119 }120 r = requests.post(url, headers=headers, json=body, timeout=60)121 r.raise_for_status()122 return r.json()["choices"][0]["message"]["content"]123 124 125def responder(pregunta, k=5):126 """Función principal: busca contexto relevante y genera la respuesta."""127 relevantes = buscar_relevantes(pregunta, k=k)128 respuesta = generar_respuesta(pregunta, relevantes)129 return respuesta, relevantes130 