import gradio as gr import logging import os import hashlib from typing import List, Tuple, Optional, Dict, Any import pandas as pd from qdrant_client import QdrantClient import re import json from collections import defaultdict, Counter import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from openai import OpenAI # Import your existing modules from document_processor import DocumentProcessor from embeddings_qdrant import EmbeddingsQdrantPipeline import settings # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Global variables for components (will be initialized in main) document_processor = None embeddings_pipeline = None # Global variable to track current document identity current_document_hash = None # OpenAI client (lazy init) _openai_client: Optional[OpenAI] = None def _get_openai_client() -> OpenAI: global _openai_client if _openai_client is None: api_key = getattr(settings, "OPENAI_API_KEY", os.getenv("OPENAI_API_KEY")) _openai_client = OpenAI(api_key=api_key) return _openai_client def get_file_hash(file_path: str) -> str: """ Calculate MD5 hash of a file to track document identity. Args: file_path: Path to the file Returns: MD5 hash string of the file content """ hash_md5 = hashlib.md5() try: with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) return hash_md5.hexdigest() except Exception as e: logger.error(f"Error calculating file hash for {file_path}: {str(e)}") return str(hash(file_path)) # Fallback to simple hash of path def clear_all_collections_for_new_document() -> bool: """ Clear all collections in Qdrant when a new document is detected. Returns: bool: True if clearing was successful or not needed, False on error """ try: logger.info("🗑️ Nuevo documento detectado: limpiando todas las colecciones existentes...") result = delete_all_collections() if "✅" in result: logger.info("✅ Todas las colecciones fueron eliminadas exitosamente") return True else: logger.warning(f"⚠️ Limpieza de colecciones completada con advertencias: {result}") return True except Exception as e: logger.error(f"❌ Error al limpiar colecciones para nuevo documento: {str(e)}") return False def reset_document_tracking() -> str: """ Reset the document tracking hash to force collection clearing on next upload. Returns: Status message """ global current_document_hash current_document_hash = None logger.info("🔄 Document tracking reset - next upload will clear all collections") return "✅ Seguimiento de documentos reiniciado. La próxima carga de documento eliminará todas las colecciones existentes." def initialize_components(): """Initialize the pipeline components with error handling for cloud deployment.""" global document_processor, embeddings_pipeline try: document_processor = DocumentProcessor() logger.info("✅ Document processor initialized") except Exception as e: logger.error(f"❌ Failed to initialize document processor: {str(e)}") document_processor = None try: embeddings_pipeline = EmbeddingsQdrantPipeline() logger.info("✅ Embeddings pipeline initialized") except Exception as e: logger.error(f"❌ Failed to initialize embeddings pipeline: {str(e)}") embeddings_pipeline = None return document_processor is not None and embeddings_pipeline is not None def process_document(file_path: str, chunking_methods: List[str]) -> Tuple[str, pd.DataFrame]: """ Process a document with selected chunking methods. Automatically clears all collections if a different document is detected. Args: file_path: Path to the uploaded file chunking_methods: List of selected chunking methods Returns: Tuple of (status_message, chunks_dataframe) """ global current_document_hash try: if document_processor is None: return "❌ Error: Procesador de documentos no inicializado. Verifica la configuración del sistema.", pd.DataFrame() if embeddings_pipeline is None: return "❌ Error: Pipeline de embeddings no inicializado. Verifica la conexión a Qdrant y la clave API de OpenAI.", pd.DataFrame() if not file_path: return "❌ Error: No se ha cargado ningún archivo", pd.DataFrame() if not chunking_methods: return "❌ Error: Por favor selecciona al menos un método de chunking", pd.DataFrame() # Calculate hash of the current document new_document_hash = get_file_hash(file_path) logger.info(f"Document hash: {new_document_hash}") # Check if this is a different document than the previous one if current_document_hash is not None and current_document_hash != new_document_hash: logger.info(f"🔄 Documento diferente detectado (hash anterior: {current_document_hash[:8]}..., nuevo: {new_document_hash[:8]}...)") # Clear all collections for the new document clear_success = clear_all_collections_for_new_document() if not clear_success: logger.warning("⚠️ Falló la limpieza de colecciones, pero continuando con el procesamiento...") elif current_document_hash is None: logger.info("📄 Primer documento cargado en esta sesión") else: logger.info("📄 Mismo documento que el anterior, manteniendo embeddings existentes") # Update the current document hash current_document_hash = new_document_hash # Process the document logger.info(f"Processing document: {file_path}") chunks_by_method = document_processor.process_document( file_path=file_path, chunking_methods=chunking_methods ) if not chunks_by_method: return "❌ Error: No se generaron chunks del documento", pd.DataFrame() # Store embeddings in Qdrant success = embeddings_pipeline.process_and_store_chunks(chunks_by_method) if not success: return "❌ Error: Falló el almacenamiento de embeddings en Qdrant", pd.DataFrame() # Create a summary dataframe summary_data = [] total_chunks = 0 for method, chunks in chunks_by_method.items(): # Use a shorter sample chunk for better display sample_text = chunks[0][:150] + "..." if chunks and len(chunks[0]) > 150 else (chunks[0] if chunks else "Sin chunks") summary_data.append({ "Método de Chunking": method.capitalize(), "Número de Chunks": len(chunks), "Longitud Promedio de Chunk": round(sum(len(chunk) for chunk in chunks) / len(chunks)) if chunks else 0, "Chunk de Muestra": sample_text }) total_chunks += len(chunks) summary_df = pd.DataFrame(summary_data) status_msg = f"✅ ¡Éxito! Documento procesado con {len(chunking_methods)} métodos. Total de chunks: {total_chunks}" return status_msg, summary_df except Exception as e: logger.error(f"Error processing document: {str(e)}") return f"❌ Error: {str(e)}", pd.DataFrame() except Exception as e: logger.error(f"Error processing document: {str(e)}") return f"❌ Error: {str(e)}", pd.DataFrame() def search_chunks(query: str, top_k: int = None) -> Tuple[str, pd.DataFrame]: """ Search for similar chunks across all collections. Args: query: Search query top_k: Number of results to return (uses settings default if None) Returns: Tuple of (status_message, results_dataframe) """ try: if embeddings_pipeline is None: return "❌ Error: Pipeline de embeddings no inicializado. Verifica la conexión a Qdrant y la clave API de OpenAI.", pd.DataFrame() if not query.strip(): return "❌ Error: Por favor ingresa una consulta de búsqueda", pd.DataFrame() # Use provided top_k or default from settings k = top_k if top_k is not None else settings.TOP_K_RESULTS # Perform search logger.info(f"Searching for: '{query}' (top_k={k})") results = embeddings_pipeline.search_chunks(query=query, top_k=k) if not results: return "⚠️ No se encontraron resultados. Prueba una consulta diferente o verifica si los documentos están procesados.", pd.DataFrame() # Create results dataframe results_data = [] for i, (text, score, method) in enumerate(results, 1): # Better text preview with line breaks preserved text_preview = text[:300] + "..." if len(text) > 300 else text # Clean up text preview for better display text_preview = text_preview.replace('\n', ' ').replace('\r', ' ').strip() results_data.append({ "Rango": i, "Puntuación de Similitud": round(score, 4), "Método de Chunking": method.capitalize(), "Vista Previa de Texto": text_preview, "Texto Completo": text }) results_df = pd.DataFrame(results_data) status_msg = f"✅ Se encontraron {len(results)} resultados para la consulta: '{query}'" return status_msg, results_df except Exception as e: logger.error(f"Error searching chunks: {str(e)}") return f"❌ Error en la búsqueda: {str(e)}", pd.DataFrame() def get_qdrant_dashboard_url() -> str: """ Get the Qdrant dashboard URL with authentication for public access. Returns: Dashboard URL with access instructions """ try: # For Qdrant Cloud, the dashboard is usually at the same URL base_url = settings.QDRANT_URL.replace(':6333', '').replace(':6334', '') dashboard_url = f"{base_url}/dashboard" return f"""🌐 **Acceso al Dashboard de Qdrant:** **🔗 URL Directo:** {dashboard_url} 🔑 **Autenticación (si es necesaria):** - Clave API: `{settings.QDRANT_API_KEY}` - URL del Servidor: `{settings.QDRANT_URL}` 📋 **Cómo Acceder:** 1. Haz clic/copia la URL de arriba para abrir el dashboard de Qdrant 2. Si se solicita autenticación, usa la clave API proporcionada arriba 3. Explora tus colecciones, vectores y datos visualmente ⚡ **Lo que puedes hacer:** - 📊 Ver estadísticas y metadatos de colecciones - 🔍 Explorar chunks de documentos almacenados - 📈 Visualizar vectores de embeddings y relaciones - ⚙️ Monitorear rendimiento y salud del cluster - 🔎 Buscar y filtrar tus datos interactivamente 💡 **Tip:** Bookmark this URL for quick access to your vector database!""" except Exception as e: logger.error(f"Error generating dashboard URL: {str(e)}") return f"❌ Error generating dashboard URL: {str(e)}" def get_collections_summary() -> Tuple[str, pd.DataFrame]: """ Get summary of all Qdrant collections using native APIs. Returns: Tuple of (status_message, collections_dataframe) """ try: if embeddings_pipeline is None: return "❌ Error: Pipeline de embeddings no inicializado", pd.DataFrame() # Use Qdrant native API to get collections info from qdrant_client import QdrantClient # Create client with proper authentication and extended timeout for HF Spaces if hasattr(settings, 'QDRANT_API_KEY') and settings.QDRANT_API_KEY: client = QdrantClient( url=settings.QDRANT_URL, api_key=settings.QDRANT_API_KEY, timeout=60.0, prefer_grpc=False ) else: client = QdrantClient( url=settings.QDRANT_URL, timeout=60.0, prefer_grpc=False ) # Get all collections collections = client.get_collections() collections_data = [] total_chunks = 0 for collection in collections.collections: try: # Get detailed info for each collection info = client.get_collection(collection.name) collections_data.append({ "Nombre de Colección": collection.name, "Cantidad de Puntos": info.points_count, "Cantidad de Vectores": info.vectors_count or info.points_count, "Estado": info.status.value if hasattr(info.status, 'value') else str(info.status), "Estado del Optimizador": "OK" if info.optimizer_status else "Optimizando" }) total_chunks += info.points_count except Exception as e: logger.warning(f"Error getting info for collection {collection.name}: {str(e)}") collections_data.append({ "Nombre de Colección": collection.name, "Cantidad de Puntos": "Error", "Cantidad de Vectores": "Error", "Estado": "Error", "Estado del Optimizador": "Error" }) collections_df = pd.DataFrame(collections_data) status_msg = f"✅ Se encontraron {len(collections.collections)} colecciones con {total_chunks} chunks totales" return status_msg, collections_df except Exception as e: logger.error(f"Error getting collections summary: {str(e)}") return f"❌ Error: {str(e)}", pd.DataFrame() def delete_collection(collection_name: str) -> str: """ Delete a specific collection using Qdrant native API. Args: collection_name: Name of the collection to delete Returns: Status message """ try: if embeddings_pipeline is None: return "❌ Error: Pipeline de embeddings no inicializado" from qdrant_client import QdrantClient # Create client with proper authentication if hasattr(settings, 'QDRANT_API_KEY') and settings.QDRANT_API_KEY: client = QdrantClient(url=settings.QDRANT_URL, api_key=settings.QDRANT_API_KEY) else: client = QdrantClient(url=settings.QDRANT_URL) # Delete the collection result = client.delete_collection(collection_name) logger.info(f"Collection '{collection_name}' deleted successfully") return f"✅ Colección '{collection_name}' eliminada exitosamente" except Exception as e: logger.error(f"Error deleting collection {collection_name}: {str(e)}") return f"❌ Error eliminando colección '{collection_name}': {str(e)}" def delete_all_collections() -> str: """ Delete all collections using Qdrant native API. Returns: Status message """ try: if embeddings_pipeline is None: return "❌ Error: Pipeline de embeddings no inicializado" from qdrant_client import QdrantClient # Create client with proper authentication if hasattr(settings, 'QDRANT_API_KEY') and settings.QDRANT_API_KEY: client = QdrantClient(url=settings.QDRANT_URL, api_key=settings.QDRANT_API_KEY) else: client = QdrantClient(url=settings.QDRANT_URL) # Get all collections first collections = client.get_collections() deleted_count = 0 errors = [] for collection in collections.collections: try: client.delete_collection(collection.name) deleted_count += 1 logger.info(f"Deleted collection: {collection.name}") except Exception as e: error_msg = f"Falló eliminar {collection.name}: {str(e)}" errors.append(error_msg) logger.error(error_msg) if errors: return f"⚠️ Se eliminaron {deleted_count} colecciones, pero {len(errors)} fallaron: {'; '.join(errors)}" else: return f"✅ Se eliminaron exitosamente todas las {deleted_count} colecciones" except Exception as e: logger.error(f"Error deleting all collections: {str(e)}") return f"❌ Error eliminando todas las colecciones: {str(e)}" def get_system_status() -> Tuple[str, pd.DataFrame]: """ Get current system status including collections info. Returns: Tuple of (status_message, status_dataframe) """ try: # Get collection statistics status_data = [] for method in settings.CHUNKING_METHODS: collection_name = f"{settings.QDRANT_COLLECTION_NAME}_{method}" try: status_data.append({ "Colección": collection_name, "Método": method.capitalize(), "Estado": "Disponible", "Descripción": f"Colección para el método de chunking {method}" }) except Exception: status_data.append({ "Colección": collection_name, "Método": method.capitalize(), "Estado": "No Disponible", "Descripción": f"Colección para el método de chunking {method} (no creada)" }) status_df = pd.DataFrame(status_data) # System configuration info config_info = f""" 📊 **Configuración del Sistema:** - Modelo de Embeddings: {settings.OPENAI_EMBEDDING_MODEL} - Dimensiones de Embedding: {settings.EMBEDDING_DIMENSIONS} - URL de Qdrant: {settings.QDRANT_URL} - Top K Resultados: {settings.TOP_K_RESULTS} - Puntuación Mín. de Similitud: {settings.MIN_SIMILARITY_SCORE} - Métodos de Chunking Disponibles: {', '.join(settings.CHUNKING_METHODS)} """ return config_info, status_df except Exception as e: logger.error(f"Error getting system status: {str(e)}") return f"❌ Error obteniendo estado del sistema: {str(e)}", pd.DataFrame() # ================================ # Explainability helpers # ================================ def _tokenize(text: str) -> list: if not text: return [] return re.findall(r"\w+", text.lower()) def _coverage_ratio(query_tokens: list, chunk_tokens: list) -> float: if not query_tokens: return 0.0 qset = set(query_tokens) cset = set(chunk_tokens) return len(qset & cset) / max(1, len(qset)) def _idf_mean_for_query(texts: list, query_tokens: list) -> Optional[float]: try: if not texts or not query_tokens: return None vectorizer = TfidfVectorizer(lowercase=True, token_pattern=r"\b\w+\b") vectorizer.fit(texts) idf_map = {term: idf for term, idf in zip(vectorizer.get_feature_names_out(), vectorizer.idf_)} vals = [idf_map[t] for t in set(query_tokens) if t in idf_map] if not vals: return None return float(np.mean(vals)) except Exception: return None def _truncate_snippet(s: str, max_len: int = 500) -> str: s = (s or "").strip().replace("\n", " ") return s[:max_len] + ("..." if len(s) > max_len else "") def _enforce_combined_char_limit(examples_by_method: Dict[str, list], limit: int = 3000) -> Dict[str, list]: # Flatten, trim from the end until under limit def total_chars(): return sum(len(e.get("snippet", "")) for lst in examples_by_method.values() for e in lst) if total_chars() <= limit: return examples_by_method # Greedy trim: reduce longest snippets first while total_chars() > limit: # find example with max length max_m, max_i, max_len = None, None, 0 for m, lst in examples_by_method.items(): for i, e in enumerate(lst): l = len(e.get("snippet", "")) if l > max_len: max_len = l; max_m = m; max_i = i if max_m is None: break snip = examples_by_method[max_m][max_i]["snippet"] examples_by_method[max_m][max_i]["snippet"] = _truncate_snippet(snip, max(50, len(snip) - 100)) return examples_by_method def prepare_explainability_payload(query: str, results_value: Any) -> Tuple[dict, pd.DataFrame]: # Normalize results to DataFrame with expected headers expected_cols = ["Rank", "Similarity Score", "Chunking Method", "Text Preview", "Full Text"] df: pd.DataFrame if isinstance(results_value, pd.DataFrame): df = results_value.copy() elif isinstance(results_value, list): # try list of dicts first if results_value and isinstance(results_value[0], dict): df = pd.DataFrame(results_value) else: df = pd.DataFrame(results_value, columns=expected_cols[:len(results_value[0]) if results_value else 0]) else: df = pd.DataFrame(columns=expected_cols) if df.empty: return {}, df # Ensure required columns exist for col in expected_cols: if col not in df.columns: df[col] = None # Group by method grouped = df.groupby("Chunking Method") # Build global texts for IDF all_texts = [str(t) for t in df["Full Text"].fillna("").tolist()] query_tokens = _tokenize(query) payload = {"query_text": query, "methods": []} examples_by_method: Dict[str, list] = {} for method, g in grouped: # metrics cos_vals = pd.to_numeric(g["Similarity Score"], errors="coerce").fillna(0.0).tolist() avg_cosine = float(np.mean(cos_vals)) if cos_vals else 0.0 avg_bm25 = None # not available # coverage per chunk coverages = [] lengths = [] snippets = [] # pick top 3 by Similarity Score g_sorted = g.sort_values(by="Similarity Score", ascending=False).head(3) for _, row in g_sorted.iterrows(): text = str(row.get("Full Text", "")) tokens = _tokenize(text) cov = _coverage_ratio(query_tokens, tokens) coverages.append(cov) lengths.append(len(tokens)) snippets.append({ "snippet": _truncate_snippet(text, 500), "similarity": round(float(row.get("Similarity Score", 0.0)), 3), "coverage": round(float(cov), 3), }) avg_cov = float(np.mean(coverages)) if coverages else 0.0 avg_len = float(np.mean(lengths)) if lengths else 0.0 idf_mean = _idf_mean_for_query(all_texts, query_tokens) examples_by_method[method] = snippets payload["methods"].append({ "method": str(method).lower(), "avg_cosine": round(avg_cosine, 3), "avg_bm25": (round(avg_bm25, 3) if isinstance(avg_bm25, (int, float)) else None), "coverage_ratio": round(avg_cov, 3), "idf_mean": (round(idf_mean, 3) if isinstance(idf_mean, (int, float)) else "informacion insuficiente"), "avg_chunk_length": round(avg_len, 1), "examples": snippets, }) # Enforce combined snippet char limit examples_by_method = _enforce_combined_char_limit(examples_by_method, 3000) # Replace examples in payload with possibly trimmed versions for m in payload["methods"]: m_examples = examples_by_method.get(m["method"], m.get("examples", [])) m["examples"] = m_examples return payload, df def explain_ranking(query: str, results_value: Any) -> Tuple[str, str, pd.DataFrame, str]: try: if not query or not str(query).strip(): return ("❌ No query provided.", "", pd.DataFrame(columns=["Method","Reasoning","Evidence","Risks"]), "") payload, df = prepare_explainability_payload(query, results_value) if not payload or df.empty: return ("⚠️ No hay resultados para explicar. Realiza una búsqueda primero.", "", pd.DataFrame(columns=["Method","Reasoning","Evidence","Risks"]), "") # Build prompt messages system_msg = ( "Eres un analista experto en Recuperacion de Informacion y en Explicabilidad de sistemas de embeddings. " "Has sido formado en el MIT y tu estilo es tecnico, preciso y pedagogico. " "Tu tarea es analizar, de forma global y comparativa, los resultados de un sistema de busqueda que usa distintos " "metodos de chunking (fixed, sentence, adaptive). " "IMPORTANTE: No solo compares metricas. Explica pedagogicamente COMO FUNCIONA cada metodo, sus ventajas teoricas, " "limitaciones intrinsecas y por que el patron observado es consistente con la teoria. " "Reglas: No inventes informacion; no modifiques valores; si faltan datos, dilo; tono tecnico; concision; " "apoya con datos y snippets; indica cuando diferencias sean pequenas. " "FORMATO: Devuelve (a) resumen breve (max 4 frases) explicando tendencias y por que, " "(b) un JSON con estructura exacta: " "{\"summary\": \"\", \"per_method\": [{\"method\": \"\", \"reasoning\": \"\", \"evidence\": \"\", \"risks\": \"\"}], \"recommendation\": \"\"}" ) user_msg = ( "Analiza los siguientes resultados globales obtenidos tras una busqueda. Cada metodo de chunking incluye medidas numericas agregadas y fragmentos representativos. " "INSTRUCCIONES ESPECIFICAS: " "1) EXPLICA COMO FUNCIONA cada metodo teoricamente (fixed: tamano fijo + overlap; sentence: agrupa oraciones naturales; adaptive: similitud semantica entre oraciones). " "2) CONECTA la teoria con los resultados observados (ej: 'fixed tiende a chunks uniformes pero puede cortar conceptos'). " "3) USA las metricas para validar/refutar expectativas teoricas. " "4) CITA frases especificas de los snippets como evidencia. " "5) Si las diferencias son pequenas, explicalo y sugiere factores que podrian ser mas determinantes. " "Usa estilo tecnico y pedagogico (nivel MIT-PhD pero entendible). No generes texto fuera del material proporcionado.\n\n" f"QUERY:\n{payload['query_text']}\n\nRESULTADOS (JSON):\n{json.dumps(payload, ensure_ascii=False)}\n\n" "Cada metodo contiene: avg_cosine (similitud coseno media), coverage_ratio (% terminos query en chunks), " "idf_mean (peso IDF medio de terminos), avg_chunk_length (longitud promedio), examples (snippets con metricas).\n\n" "TASK DETALLADA: " "1) Tendencias globales y EXPLICACION TEORICA de cada metodo. " "2) Factores numericos que explican diferencias + conexion con principios de cada metodo. " "3) Frases de snippets que ilustran fortalezas/debilidades de cada enfoque. " "4) Riesgos inherentes a cada metodo (dilucion, perdida contexto, fronteras artificiales, etc.). " "5) Recomendacion fundamentada para este tipo de query y corpus.\n" "FORMATO: resumen tecnico (max 4 frases) + JSON valido con estructura exacta. Si falta info, usa 'informacion insuficiente'." ) client = _get_openai_client() completion = client.chat.completions.create( model="gpt-4o-mini", temperature=0.3, max_tokens=1000, messages=[ {"role": "system", "content": system_msg}, {"role": "user", "content": user_msg}, ], ) content = completion.choices[0].message.content if completion and completion.choices else "" # Try to parse JSON from content summary_text = "" json_obj = None # Heuristic: find JSON block if content: try: # Direct parse json_obj = json.loads(content) except Exception: # Extract first {...} block start = content.find("{") end = content.rfind("}") if start != -1 and end != -1 and end > start: try: json_obj = json.loads(content[start:end+1]) except Exception: json_obj = None if not json_obj: return ( "⚠️ No se pudo generar la explicación en formato JSON válido.", content[:800] if content else "", pd.DataFrame(columns=["Method","Reasoning","Evidence","Risks"]), "", ) summary_text = str(json_obj.get("summary", "")) per_method = json_obj.get("per_method", []) or [] recommendation = str(json_obj.get("recommendation", "")) table_rows = [] for item in per_method: table_rows.append({ "Method": item.get("method", ""), "Reasoning": item.get("reasoning", ""), "Evidence": item.get("evidence", ""), "Risks": item.get("risks", ""), }) table_df = pd.DataFrame(table_rows, columns=["Method","Reasoning","Evidence","Risks"]) return ("", summary_text, table_df, recommendation) except Exception as e: logger.error(f"Explainability error: {str(e)}") return ( "❌ No se pudo generar la explicación. Verifica tu clave de API o la conexión.", "", pd.DataFrame(columns=["Method","Reasoning","Evidence","Risks"]), "", ) """ Get current system status including collections info. Returns: Tuple of (status_message, status_dataframe) """ try: # Get collection statistics status_data = [] total_chunks = 0 for method in settings.CHUNKING_METHODS: collection_name = f"{settings.QDRANT_COLLECTION_NAME}_{method}" try: # This would need a method to get collection info # For now, we'll show configured methods status_data.append({ "Collection": collection_name, "Method": method, "Status": "Available", "Description": f"Collection for {method} chunking method" }) except Exception: status_data.append({ "Collection": collection_name, "Method": method, "Status": "Not Available", "Description": f"Collection for {method} chunking method (not created)" }) status_df = pd.DataFrame(status_data) # System configuration info config_info = f""" 📊 **System Configuration:** - Embedding Model: {settings.OPENAI_EMBEDDING_MODEL} - Embedding Dimensions: {settings.EMBEDDING_DIMENSIONS} - Qdrant URL: {settings.QDRANT_URL} - Top K Results: {settings.TOP_K_RESULTS} - Min Similarity Score: {settings.MIN_SIMILARITY_SCORE} - Available Chunking Methods: {', '.join(settings.CHUNKING_METHODS)} """ return config_info, status_df except Exception as e: logger.error(f"Error getting system status: {str(e)}") return f"❌ Error: {str(e)}", pd.DataFrame() # Create Gradio interface with gr.Blocks(title="Sistema de Chunking y Búsqueda de Documentos", theme=gr.themes.Soft(), css=""" .gradio-container { max-width: 1200px !important; } .dataframe table { table-layout: fixed !important; word-wrap: break-word !important; } .dataframe td { white-space: pre-wrap !important; vertical-align: top !important; padding: 8px !important; } """) as demo: gr.Markdown(""" # 📄 Sistema de Chunking y Búsqueda de Documentos Este sistema procesa documentos utilizando múltiples estrategias de chunking y permite búsqueda semántica usando embeddings de OpenAI almacenados en Qdrant. ## Métodos de Chunking Disponibles: - **Fixed**: Chunks de tamaño fijo con solapamiento - **Sentence**: Chunking basado en oraciones - **Adaptive**: Chunking adaptativo basado en la estructura del contenido """) with gr.Tab("📤 Procesamiento de Documentos"): gr.Markdown("### Cargar y procesar documentos") with gr.Row(): with gr.Column(): file_input = gr.File( label="Cargar Documento", file_types=[".pdf", ".txt", ".docx"], type="filepath" ) chunking_methods = gr.CheckboxGroup( choices=settings.CHUNKING_METHODS, value=settings.CHUNKING_METHODS, # Select all by default label="Seleccionar Métodos de Chunking" ) process_btn = gr.Button("🔄 Procesar Documento", variant="primary") with gr.Column(): process_status = gr.Textbox( label="Estado del Procesamiento", placeholder="Carga un documento y haz clic en 'Procesar Documento'", interactive=False ) chunks_table = gr.Dataframe( label="Resumen de Chunks", headers=["Método de Chunking", "Número de Chunks", "Longitud Promedio de Chunk", "Chunk de Muestra"], interactive=False, wrap=True, column_widths=["15%", "15%", "15%", "55%"], max_height=400 ) process_btn.click( process_document, inputs=[file_input, chunking_methods], outputs=[process_status, chunks_table] ) with gr.Tab("🔍 Búsqueda"): gr.Markdown("### Buscar contenido similar en todos los documentos procesados") with gr.Row(): with gr.Column(): search_input = gr.Textbox( label="Consulta de Búsqueda", placeholder="Ingresa tu consulta de búsqueda aquí...", lines=2 ) top_k_input = gr.Slider( minimum=1, maximum=20, value=settings.TOP_K_RESULTS, step=1, label="Número de Resultados" ) search_btn = gr.Button("🔍 Buscar", variant="primary") with gr.Column(): search_status = gr.Textbox( label="Estado de la Búsqueda", placeholder="Ingresa una consulta y haz clic en 'Buscar'", interactive=False ) results_table = gr.Dataframe( label="Resultados de Búsqueda", headers=["Rango", "Puntuación de Similitud", "Método de Chunking", "Vista Previa de Texto", "Texto Completo"], interactive=False, wrap=True, column_widths=["8%", "12%", "15%", "30%", "35%"], max_height=600 ) # Explainability panel gr.Markdown("### 🧠 Explicabilidad de los resultados") with gr.Row(): with gr.Column(scale=1): explain_btn = gr.Button("🧠 Explicabilidad de los resultados", variant="secondary", interactive=False) explain_status = gr.Textbox(label="Estado", interactive=False) with gr.Column(scale=2): explain_summary = gr.Textbox(label="Resumen técnico", lines=4, interactive=False) per_method_table = gr.Dataframe( label="Análisis por método", headers=["Method", "Reasoning", "Evidence", "Risks"], interactive=False, wrap=True, max_height=300 ) recommendation_text = gr.Textbox(label="Recomendación", lines=2, interactive=False) # Search action search_event = search_btn.click( search_chunks, inputs=[search_input, top_k_input], outputs=[search_status, results_table] ) # After search: enable explain button and clear previous explanation search_event.then( lambda: gr.update(interactive=True), inputs=None, outputs=explain_btn ).then( lambda: ("", "", pd.DataFrame(columns=["Method","Reasoning","Evidence","Risks"]), ""), inputs=None, outputs=[explain_status, explain_summary, per_method_table, recommendation_text] ) # Explain action explain_btn.click( explain_ranking, inputs=[search_input, results_table], outputs=[explain_status, explain_summary, per_method_table, recommendation_text] ) with gr.Tab("⚙️ Estado del Sistema"): gr.Markdown("### Configuración y estado actual del sistema") status_btn = gr.Button("🔄 Actualizar Estado", variant="secondary") system_info = gr.Textbox( label="Información del Sistema", lines=10, interactive=False ) collections_table = gr.Dataframe( label="Collections Status", headers=["Colección", "Método", "Estado", "Descripción"], interactive=False, wrap=True, column_widths=["25%", "15%", "15%", "45%"], max_height=300 ) # Load initial status demo.load( get_system_status, outputs=[system_info, collections_table] ) status_btn.click( get_system_status, outputs=[system_info, collections_table] ) # Data Management Tab with gr.Tab("🗄️ Gestión de Datos"): gr.Markdown("## Gestión de Base de Datos Qdrant") gr.Markdown("Gestiona tus colecciones de documentos y visualiza datos usando las herramientas nativas de Qdrant.") with gr.Row(): with gr.Column(): gr.Markdown("### 📊 Visualización") dashboard_btn = gr.Button("🌐 Obtener Acceso al Dashboard", variant="primary", size="lg") dashboard_output = gr.Textbox( label="Información de Acceso al Dashboard de Qdrant", placeholder="Haz clic en el botón para obtener la URL del dashboard y detalles de acceso", lines=15, interactive=False ) with gr.Column(): gr.Markdown("### 📋 Resumen de Colecciones") summary_btn = gr.Button("📊 Obtener Resumen de Colecciones", variant="secondary") summary_output = gr.Textbox( label="Información de Colecciones", lines=8, interactive=False ) summary_table = gr.Dataframe( label="Detalles de Colecciones", headers=["Nombre de Colección", "Cantidad de Puntos", "Cantidad de Vectores", "Estado", "Estado del Optimizador"], interactive=False, wrap=True, max_height=200 ) gr.Markdown("### 🗑️ Gestión de Colecciones") with gr.Row(): with gr.Column(): gr.Markdown("**Eliminar Colección Específica:**") method_dropdown = gr.Dropdown( choices=settings.CHUNKING_METHODS, label="Seleccionar Método de Chunking", value=settings.CHUNKING_METHODS[0] if settings.CHUNKING_METHODS else None ) delete_specific_btn = gr.Button("🗑️ Eliminar Colección Seleccionada", variant="secondary") with gr.Column(): gr.Markdown("**Eliminar Todas las Colecciones:**") gr.Markdown("⚠️ **Advertencia**: ¡Esto eliminará TODAS las colecciones permanentemente!") delete_all_btn = gr.Button("🗑️ Eliminar Todas las Colecciones", variant="stop") with gr.Row(): with gr.Column(): gr.Markdown("**Reiniciar Seguimiento de Documentos:**") gr.Markdown("🔄 Forzar limpieza de colecciones en la próxima carga de documento (útil cuando se reprocesa el mismo archivo)") reset_tracking_btn = gr.Button("🔄 Reiniciar Seguimiento de Documentos", variant="secondary") delete_output = gr.Textbox( label="Estado de Eliminación", lines=3, interactive=False ) # Event handlers for Data Management dashboard_btn.click( get_qdrant_dashboard_url, outputs=dashboard_output ) summary_btn.click( get_collections_summary, outputs=[summary_output, summary_table] ) def _delete_by_method(method_name: str) -> str: try: if not method_name: return "❌ Por favor selecciona un método" collection_name = f"{settings.QDRANT_COLLECTION_NAME}_{method_name}" return delete_collection(collection_name) except Exception as e: return f"❌ Error eliminando: {str(e)}" delete_specific_btn.click( _delete_by_method, inputs=method_dropdown, outputs=delete_output ) delete_all_btn.click( delete_all_collections, outputs=delete_output ) reset_tracking_btn.click( reset_document_tracking, outputs=delete_output ) if __name__ == "__main__": # Set up environment for deployment port = int(os.environ.get("PORT", 7860)) print("🚀 Iniciando Sistema de Chunking y Búsqueda de Documentos...") # Initialize components with error handling init_success = initialize_components() if not init_success: print("⚠️ Advertencia: Algunos componentes fallaron al inicializar. Verifica la configuración.") print("📋 Asegúrate de tener:") print(" - Variable de entorno OPENAI_API_KEY configurada") print(" - Instancia de Qdrant ejecutándose (QDRANT_URL configurado)") else: print("✅ Todos los componentes inicializados exitosamente") print(f"📊 Configurado para {len(settings.CHUNKING_METHODS)} métodos de chunking") print(f"🔗 URL de Qdrant: {settings.QDRANT_URL}") print(f"🤖 Modelo de Embeddings: {settings.OPENAI_EMBEDDING_MODEL}") demo.launch( server_name="0.0.0.0", server_port=port, share=False # Gradio Spaces will handle public access )