| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import logging |
| import os |
| import pickle |
| import json |
| import time |
| import networkx as nx |
| import requests |
| from langchain_community.document_loaders import PyMuPDFLoader |
| from langchain_text_splitters import RecursiveCharacterTextSplitter |
|
|
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(message)s" |
| ) |
| logger = logging.getLogger("build_graph") |
|
|
| |
| |
| |
| RAIZ_PROYECTO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) |
| CARPETA_PDFS = os.path.join(RAIZ_PROYECTO, "papers_ia") |
| RUTA_GRAFO = os.path.join(RAIZ_PROYECTO, "knowledge_graph.pkl") |
|
|
|
|
| def extraer_relaciones_con_llm(texto: str, groq_key: str) -> list: |
| """ |
| Le pide al LLM de Groq que lea un fragmento de texto científico |
| y extraiga las relaciones entre conceptos en formato JSON. |
| |
| Una relación tiene tres partes: |
| - sujeto: el concepto principal (ej: "Transformer") |
| - relacion: cómo se conectan (ej: "es_tipo_de") |
| - objeto: el concepto relacionado(ej: "Red Neuronal") |
| """ |
| prompt = f""" |
| Eres un extractor de conocimiento científico. |
| Lee este texto sobre Inteligencia Artificial y extrae las relaciones |
| más importantes entre conceptos científicos. |
| |
| TEXTO: |
| {texto[:1500]} |
| |
| INSTRUCCIONES: |
| - Extrae entre 3 y 6 relaciones importantes |
| - Cada relación debe tener: sujeto, relacion, objeto |
| - Los conceptos deben ser términos técnicos concretos |
| - Las relaciones deben ser verbos como: es_tipo_de, usa, desarrollado_por, |
| base_de, mejora_a, relacionado_con, parte_de, aplicado_en |
| - Responde SOLO con un JSON válido, sin explicaciones |
| |
| FORMATO EXACTO: |
| [ |
| {{"sujeto": "Transformer", "relacion": "es_tipo_de", "objeto": "Red Neuronal"}}, |
| {{"sujeto": "BERT", "relacion": "desarrollado_por", "objeto": "Google"}} |
| ] |
| """ |
|
|
| try: |
| resp = requests.post( |
| "https://api.groq.com/openai/v1/chat/completions", |
| headers={ |
| "Authorization": f"Bearer {groq_key}", |
| "Content-Type": "application/json" |
| }, |
| json={ |
| "model": "llama-3.1-8b-instant", |
| "messages": [{"role": "user", "content": prompt}], |
| "max_tokens": 500, |
| "temperature": 0 |
| }, |
| timeout=15 |
| ) |
|
|
| if resp.status_code == 200: |
| contenido = resp.json()["choices"][0]["message"]["content"].strip() |
|
|
| |
| inicio = contenido.find("[") |
| fin = contenido.rfind("]") + 1 |
| if inicio != -1 and fin > 0: |
| json_limpio = contenido[inicio:fin] |
| relaciones = json.loads(json_limpio) |
| return relaciones |
|
|
| except Exception as e: |
| logger.warning(f" Error extrayendo relaciones: {e}") |
|
|
| return [] |
|
|
|
|
| def construir_grafo(): |
| """ |
| Función principal que construye y guarda el grafo de conocimiento. |
| """ |
| logger.info("=" * 60) |
| logger.info("INICIANDO CONSTRUCCIÓN DEL GRAFO DE CONOCIMIENTO") |
| logger.info("=" * 60) |
|
|
| |
| groq_key = os.getenv("GROQ_API_KEY", "") |
| if not groq_key: |
| logger.error("No se encontró GROQ_API_KEY en el .env — necesaria para extraer relaciones") |
| return |
|
|
| |
| |
| |
| |
| logger.info("PASO 1: Leyendo PDFs de papers_ia/...") |
|
|
| if not os.path.exists(CARPETA_PDFS): |
| logger.error(f"No existe la carpeta: {CARPETA_PDFS}") |
| return |
|
|
| archivos_pdf = [f for f in os.listdir(CARPETA_PDFS) if f.endswith(".pdf")] |
| if not archivos_pdf: |
| logger.error("No hay PDFs en papers_ia/") |
| return |
|
|
| todos_los_documentos = [] |
| for nombre_pdf in archivos_pdf: |
| try: |
| loader = PyMuPDFLoader(os.path.join(CARPETA_PDFS, nombre_pdf)) |
| docs = loader.load() |
| todos_los_documentos.extend(docs) |
| logger.info(f" ✅ '{nombre_pdf}' leído ({len(docs)} páginas)") |
| except Exception as e: |
| logger.error(f" ❌ Error leyendo '{nombre_pdf}': {e}") |
|
|
| logger.info(f"PASO 1 OK: {len(todos_los_documentos)} páginas leídas") |
|
|
| |
| |
| |
| |
| |
| logger.info("PASO 2: Troceando documentos en chunks...") |
|
|
| text_splitter = RecursiveCharacterTextSplitter( |
| chunk_size=2000, |
| chunk_overlap=200 |
| ) |
| chunks = text_splitter.split_documents(todos_los_documentos) |
|
|
| |
| chunks_a_procesar = chunks[:30] |
| logger.info(f"PASO 2 OK: Procesaremos {len(chunks_a_procesar)} chunks de {len(chunks)} totales") |
|
|
| |
| |
| |
| |
| |
| logger.info("PASO 3: Extrayendo relaciones con el LLM...") |
| logger.info("(Esto puede tardar unos minutos — hace una llamada por chunk)") |
|
|
| todas_las_relaciones = [] |
|
|
| for i, chunk in enumerate(chunks_a_procesar): |
| logger.info(f" Procesando chunk {i+1}/{len(chunks_a_procesar)}...") |
|
|
| relaciones = extraer_relaciones_con_llm(chunk.page_content, groq_key) |
| todas_las_relaciones.extend(relaciones) |
|
|
| logger.info(f" ✅ {len(relaciones)} relaciones extraídas de este chunk") |
|
|
| |
| time.sleep(1) |
|
|
| logger.info(f"PASO 3 OK: {len(todas_las_relaciones)} relaciones extraídas en total") |
|
|
| |
| |
| |
| |
| |
| logger.info("PASO 4: Construyendo el grafo con NetworkX...") |
|
|
| |
| |
| |
| |
| grafo = nx.DiGraph() |
|
|
| relaciones_añadidas = 0 |
| for rel in todas_las_relaciones: |
| try: |
| sujeto = rel.get("sujeto", "").strip() |
| relacion = rel.get("relacion", "").strip() |
| objeto = rel.get("objeto", "").strip() |
|
|
| |
| if sujeto and relacion and objeto: |
| |
| |
| grafo.add_edge(sujeto, objeto, label=relacion) |
| relaciones_añadidas += 1 |
|
|
| except Exception: |
| continue |
|
|
| logger.info(f"PASO 4 OK: Grafo construido con {grafo.number_of_nodes()} nodos " |
| f"y {grafo.number_of_edges()} aristas") |
|
|
| |
| logger.info("Ejemplos de relaciones encontradas:") |
| for u, v, data in list(grafo.edges(data=True))[:10]: |
| logger.info(f" '{u}' --[{data.get('label','')}]--> '{v}'") |
|
|
| |
| |
| |
| |
| logger.info("PASO 5: Guardando grafo en disco...") |
|
|
| with open(RUTA_GRAFO, "wb") as f: |
| pickle.dump(grafo, f) |
|
|
| logger.info(f"PASO 5 OK: Grafo guardado en '{RUTA_GRAFO}'") |
| logger.info("=" * 60) |
| logger.info("✅ GRAFO DE CONOCIMIENTO CONSTRUIDO CON ÉXITO") |
| logger.info(f" Nodos (conceptos): {grafo.number_of_nodes()}") |
| logger.info(f" Aristas (relaciones): {grafo.number_of_edges()}") |
| logger.info(f" Guardado en: knowledge_graph.pkl") |
| logger.info("=" * 60) |
| logger.info("Ahora ejecuta: python src/tools/build_graph.py") |
| logger.info("Y después arranca: uv run streamlit run src/ui/app.py") |
|
|
|
|
| if __name__ == "__main__": |
| |
| from dotenv import load_dotenv |
| load_dotenv(override=True) |
| construir_grafo() |