Primera version de chunkeo completo crud
This commit is contained in:
316
backend/app/services/chunking_service.py
Normal file
316
backend/app/services/chunking_service.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
Servicio de chunking que orquesta todo el proceso:
|
||||
- Descarga PDF desde Azure Blob
|
||||
- Procesa con pipeline de chunking
|
||||
- Genera embeddings con Azure OpenAI
|
||||
- Sube a Qdrant con IDs determinísticos
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from typing import List, Dict, Any, Optional
|
||||
from io import BytesIO
|
||||
|
||||
from azure.storage.blob import BlobServiceClient
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from ..core.config import settings
|
||||
from ..utils.chunking import (
|
||||
process_pdf_with_token_control,
|
||||
get_gemini_client,
|
||||
GeminiClient
|
||||
)
|
||||
from ..services.embedding_service import get_embedding_service
|
||||
from ..vector_db.factory import get_vector_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChunkingService:
|
||||
"""Servicio para procesar PDFs y subirlos a Qdrant"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inicializa el servicio con conexiones a Azure Blob y clientes"""
|
||||
self.blob_service = BlobServiceClient.from_connection_string(
|
||||
settings.AZURE_STORAGE_CONNECTION_STRING
|
||||
)
|
||||
self.container_name = settings.AZURE_CONTAINER_NAME
|
||||
self.embedding_service = get_embedding_service()
|
||||
self.vector_db = get_vector_db()
|
||||
|
||||
def _generate_deterministic_id(
|
||||
self,
|
||||
file_name: str,
|
||||
page: int,
|
||||
chunk_index: int
|
||||
) -> str:
|
||||
"""
|
||||
Genera un ID determinístico para un chunk usando UUID v5.
|
||||
|
||||
Args:
|
||||
file_name: Nombre del archivo
|
||||
page: Número de página
|
||||
chunk_index: Índice del chunk dentro de la página
|
||||
|
||||
Returns:
|
||||
ID en formato UUID válido para Qdrant
|
||||
"""
|
||||
id_string = f"{file_name}_{page}_{chunk_index}"
|
||||
# Usar UUID v5 con namespace DNS para generar UUID determinístico
|
||||
return str(uuid.uuid5(uuid.NAMESPACE_DNS, id_string))
|
||||
|
||||
async def download_pdf_from_blob(
|
||||
self,
|
||||
file_name: str,
|
||||
tema: str
|
||||
) -> bytes:
|
||||
"""
|
||||
Descarga un PDF desde Azure Blob Storage.
|
||||
|
||||
Args:
|
||||
file_name: Nombre del archivo
|
||||
tema: Tema/carpeta del archivo
|
||||
|
||||
Returns:
|
||||
Contenido del PDF en bytes
|
||||
|
||||
Raises:
|
||||
Exception: Si hay error descargando el archivo
|
||||
"""
|
||||
try:
|
||||
blob_path = f"{tema}/{file_name}"
|
||||
logger.info(f"Descargando PDF: {blob_path}")
|
||||
|
||||
blob_client = self.blob_service.get_blob_client(
|
||||
container=self.container_name,
|
||||
blob=blob_path
|
||||
)
|
||||
|
||||
pdf_bytes = blob_client.download_blob().readall()
|
||||
logger.info(f"PDF descargado: {len(pdf_bytes)} bytes")
|
||||
return pdf_bytes
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error descargando PDF: {e}")
|
||||
raise
|
||||
|
||||
async def process_pdf_preview(
|
||||
self,
|
||||
file_name: str,
|
||||
tema: str,
|
||||
max_tokens: int = 950,
|
||||
target_tokens: int = 800,
|
||||
chunk_size: int = 1000,
|
||||
chunk_overlap: int = 200,
|
||||
use_llm: bool = True,
|
||||
custom_instructions: str = ""
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Procesa un PDF y genera exactamente 3 chunks de preview.
|
||||
|
||||
Args:
|
||||
file_name: Nombre del archivo PDF
|
||||
tema: Tema/carpeta del archivo
|
||||
max_tokens: Límite máximo de tokens por chunk
|
||||
target_tokens: Tokens objetivo
|
||||
chunk_size: Tamaño del chunk
|
||||
chunk_overlap: Solapamiento
|
||||
use_llm: Si True, usa Gemini para procesamiento inteligente
|
||||
custom_instructions: Instrucciones personalizadas (solo si use_llm=True)
|
||||
|
||||
Returns:
|
||||
Lista con exactamente 3 chunks de preview con metadata
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generando preview para {file_name} (tema: {tema})")
|
||||
|
||||
# Descargar PDF
|
||||
pdf_bytes = await self.download_pdf_from_blob(file_name, tema)
|
||||
|
||||
# Configurar cliente Gemini si está habilitado
|
||||
gemini_client = get_gemini_client() if use_llm else None
|
||||
|
||||
# Si LLM está deshabilitado, ignorar custom_instructions
|
||||
instructions = custom_instructions if use_llm else ""
|
||||
|
||||
# Procesar PDF
|
||||
chunks = process_pdf_with_token_control(
|
||||
pdf_bytes=pdf_bytes,
|
||||
file_name=file_name,
|
||||
max_tokens=max_tokens,
|
||||
target_tokens=target_tokens,
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
merge_related=True,
|
||||
gemini_client=gemini_client,
|
||||
custom_instructions=instructions,
|
||||
extract_images=False # Deshabilitado según requerimientos
|
||||
)
|
||||
|
||||
# Tomar los primeros chunks para preview (máximo 3, mínimo 1)
|
||||
preview_chunks = chunks[:min(3, len(chunks))] if chunks else []
|
||||
|
||||
# Formatear para respuesta
|
||||
result = []
|
||||
for idx, chunk in enumerate(preview_chunks):
|
||||
result.append({
|
||||
"index": idx,
|
||||
"text": chunk.page_content,
|
||||
"page": chunk.metadata.get("page", 0),
|
||||
"file_name": chunk.metadata.get("file_name", file_name),
|
||||
"tokens": len(chunk.page_content.split()) # Aproximación
|
||||
})
|
||||
|
||||
logger.info(f"Preview generado: {len(result)} chunks")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generando preview: {e}")
|
||||
raise
|
||||
|
||||
async def process_pdf_full(
|
||||
self,
|
||||
file_name: str,
|
||||
tema: str,
|
||||
collection_name: str,
|
||||
max_tokens: int = 950,
|
||||
target_tokens: int = 800,
|
||||
chunk_size: int = 1000,
|
||||
chunk_overlap: int = 200,
|
||||
use_llm: bool = True,
|
||||
custom_instructions: str = "",
|
||||
progress_callback: Optional[callable] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Procesa un PDF completo y lo sube a Qdrant.
|
||||
|
||||
Args:
|
||||
file_name: Nombre del archivo PDF
|
||||
tema: Tema/carpeta del archivo
|
||||
collection_name: Nombre de la colección en Qdrant
|
||||
max_tokens: Límite máximo de tokens por chunk
|
||||
target_tokens: Tokens objetivo
|
||||
chunk_size: Tamaño del chunk
|
||||
chunk_overlap: Solapamiento
|
||||
use_llm: Si True, usa Gemini para procesamiento inteligente
|
||||
custom_instructions: Instrucciones personalizadas (solo si use_llm=True)
|
||||
progress_callback: Callback para reportar progreso
|
||||
|
||||
Returns:
|
||||
Diccionario con resultados del procesamiento
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Procesando PDF completo: {file_name} (tema: {tema})")
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback({"status": "downloading", "progress": 0})
|
||||
|
||||
# 1. Descargar PDF
|
||||
pdf_bytes = await self.download_pdf_from_blob(file_name, tema)
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback({"status": "chunking", "progress": 20})
|
||||
|
||||
# 2. Configurar cliente Gemini
|
||||
gemini_client = get_gemini_client() if use_llm else None
|
||||
instructions = custom_instructions if use_llm else ""
|
||||
|
||||
# 3. Procesar PDF
|
||||
chunks = process_pdf_with_token_control(
|
||||
pdf_bytes=pdf_bytes,
|
||||
file_name=file_name,
|
||||
max_tokens=max_tokens,
|
||||
target_tokens=target_tokens,
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
merge_related=True,
|
||||
gemini_client=gemini_client,
|
||||
custom_instructions=instructions,
|
||||
extract_images=False
|
||||
)
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback({"status": "embedding", "progress": 50})
|
||||
|
||||
# 4. Generar embeddings
|
||||
texts = [chunk.page_content for chunk in chunks]
|
||||
logger.info(f"Generando embeddings para {len(texts)} chunks")
|
||||
embeddings = await self.embedding_service.generate_embeddings_batch(texts)
|
||||
logger.info(f"Embeddings generados: {len(embeddings)} vectores de dimensión {len(embeddings[0]) if embeddings else 0}")
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback({"status": "uploading", "progress": 80})
|
||||
|
||||
# 5. Preparar chunks para Qdrant con IDs determinísticos
|
||||
qdrant_chunks = []
|
||||
page_chunk_count = {} # Contador de chunks por página
|
||||
|
||||
logger.info(f"Preparando {len(chunks)} chunks con {len(embeddings)} embeddings para subir")
|
||||
for chunk, embedding in zip(chunks, embeddings):
|
||||
page = chunk.metadata.get("page", 0)
|
||||
|
||||
# Incrementar contador para esta página
|
||||
if page not in page_chunk_count:
|
||||
page_chunk_count[page] = 0
|
||||
chunk_index = page_chunk_count[page]
|
||||
page_chunk_count[page] += 1
|
||||
|
||||
# Generar ID determinístico
|
||||
chunk_id = self._generate_deterministic_id(
|
||||
file_name=file_name,
|
||||
page=page,
|
||||
chunk_index=chunk_index
|
||||
)
|
||||
|
||||
qdrant_chunks.append({
|
||||
"id": chunk_id,
|
||||
"vector": embedding,
|
||||
"payload": {
|
||||
"page_content": chunk.page_content,
|
||||
"metadata": {
|
||||
"page": page,
|
||||
"file_name": file_name
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# 6. Subir a Qdrant
|
||||
logger.info(f"Subiendo {len(qdrant_chunks)} chunks a Qdrant colección '{collection_name}'")
|
||||
result = await self.vector_db.add_chunks(collection_name, qdrant_chunks)
|
||||
logger.info(f"Resultado de upsert: {result}")
|
||||
|
||||
if progress_callback:
|
||||
await progress_callback({"status": "completed", "progress": 100})
|
||||
|
||||
logger.info(f"Procesamiento completo: {result['chunks_added']} chunks subidos")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"collection_name": collection_name,
|
||||
"file_name": file_name,
|
||||
"total_chunks": len(chunks),
|
||||
"chunks_added": result['chunks_added'],
|
||||
"message": "PDF procesado y subido exitosamente"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error procesando PDF completo: {e}")
|
||||
if progress_callback:
|
||||
await progress_callback({"status": "error", "progress": 0, "error": str(e)})
|
||||
raise
|
||||
|
||||
|
||||
# Instancia global singleton
|
||||
_chunking_service: ChunkingService | None = None
|
||||
|
||||
|
||||
def get_chunking_service() -> ChunkingService:
|
||||
"""
|
||||
Obtiene la instancia singleton del servicio de chunking.
|
||||
|
||||
Returns:
|
||||
Instancia de ChunkingService
|
||||
"""
|
||||
global _chunking_service
|
||||
if _chunking_service is None:
|
||||
_chunking_service = ChunkingService()
|
||||
return _chunking_service
|
||||
127
backend/app/services/embedding_service.py
Normal file
127
backend/app/services/embedding_service.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Servicio de embeddings usando Azure OpenAI.
|
||||
Genera embeddings para chunks de texto usando text-embedding-3-large (3072 dimensiones).
|
||||
"""
|
||||
import logging
|
||||
from typing import List
|
||||
from openai import AzureOpenAI
|
||||
from ..core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmbeddingService:
|
||||
"""Servicio para generar embeddings usando Azure OpenAI"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inicializa el cliente de Azure OpenAI"""
|
||||
try:
|
||||
self.client = AzureOpenAI(
|
||||
api_key=settings.AZURE_OPENAI_API_KEY,
|
||||
api_version=settings.AZURE_OPENAI_API_VERSION,
|
||||
azure_endpoint=settings.AZURE_OPENAI_ENDPOINT
|
||||
)
|
||||
self.model = settings.AZURE_OPENAI_EMBEDDING_DEPLOYMENT
|
||||
self.embedding_dimension = 3072
|
||||
logger.info(f"EmbeddingService inicializado con modelo {self.model}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error inicializando EmbeddingService: {e}")
|
||||
raise
|
||||
|
||||
async def generate_embedding(self, text: str) -> List[float]:
|
||||
"""
|
||||
Genera un embedding para un texto individual.
|
||||
|
||||
Args:
|
||||
text: Texto para generar embedding
|
||||
|
||||
Returns:
|
||||
Vector de embedding (3072 dimensiones)
|
||||
|
||||
Raises:
|
||||
Exception: Si hay error al generar el embedding
|
||||
"""
|
||||
try:
|
||||
response = self.client.embeddings.create(
|
||||
input=[text],
|
||||
model=self.model
|
||||
)
|
||||
embedding = response.data[0].embedding
|
||||
|
||||
if len(embedding) != self.embedding_dimension:
|
||||
raise ValueError(
|
||||
f"Dimensión incorrecta: esperada {self.embedding_dimension}, "
|
||||
f"obtenida {len(embedding)}"
|
||||
)
|
||||
|
||||
return embedding
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generando embedding: {e}")
|
||||
raise
|
||||
|
||||
async def generate_embeddings_batch(
|
||||
self,
|
||||
texts: List[str],
|
||||
batch_size: int = 100
|
||||
) -> List[List[float]]:
|
||||
"""
|
||||
Genera embeddings para múltiples textos en lotes.
|
||||
|
||||
Args:
|
||||
texts: Lista de textos para generar embeddings
|
||||
batch_size: Tamaño del lote para procesamiento (default: 100)
|
||||
|
||||
Returns:
|
||||
Lista de vectores de embeddings
|
||||
|
||||
Raises:
|
||||
Exception: Si hay error al generar los embeddings
|
||||
"""
|
||||
try:
|
||||
embeddings = []
|
||||
|
||||
for i in range(0, len(texts), batch_size):
|
||||
batch = texts[i:i + batch_size]
|
||||
logger.info(f"Procesando lote {i//batch_size + 1}/{(len(texts)-1)//batch_size + 1}")
|
||||
|
||||
response = self.client.embeddings.create(
|
||||
input=batch,
|
||||
model=self.model
|
||||
)
|
||||
|
||||
batch_embeddings = [item.embedding for item in response.data]
|
||||
|
||||
# Validar dimensiones
|
||||
for idx, emb in enumerate(batch_embeddings):
|
||||
if len(emb) != self.embedding_dimension:
|
||||
raise ValueError(
|
||||
f"Dimensión incorrecta en índice {i + idx}: "
|
||||
f"esperada {self.embedding_dimension}, obtenida {len(emb)}"
|
||||
)
|
||||
|
||||
embeddings.extend(batch_embeddings)
|
||||
|
||||
logger.info(f"Generados {len(embeddings)} embeddings exitosamente")
|
||||
return embeddings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generando embeddings en lote: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# Instancia global singleton
|
||||
_embedding_service: EmbeddingService | None = None
|
||||
|
||||
|
||||
def get_embedding_service() -> EmbeddingService:
|
||||
"""
|
||||
Obtiene la instancia singleton del servicio de embeddings.
|
||||
|
||||
Returns:
|
||||
Instancia de EmbeddingService
|
||||
"""
|
||||
global _embedding_service
|
||||
if _embedding_service is None:
|
||||
_embedding_service = EmbeddingService()
|
||||
return _embedding_service
|
||||
442
backend/app/services/vector_service.py
Normal file
442
backend/app/services/vector_service.py
Normal file
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
Servicio de lógica de negocio para operaciones con bases de datos vectoriales.
|
||||
|
||||
Este módulo contiene toda la lógica de negocio relacionada con la gestión
|
||||
de colecciones y chunks en bases de datos vectoriales.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from app.vector_db import get_vector_db
|
||||
from app.models.vector_models import (
|
||||
CollectionCreateRequest,
|
||||
CollectionCreateResponse,
|
||||
CollectionDeleteResponse,
|
||||
CollectionExistsResponse,
|
||||
CollectionInfoResponse,
|
||||
FileExistsInCollectionResponse,
|
||||
GetChunksByFileResponse,
|
||||
DeleteFileFromCollectionResponse,
|
||||
AddChunksResponse,
|
||||
VectorDBHealthResponse,
|
||||
VectorDBErrorResponse
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VectorService:
|
||||
"""
|
||||
Servicio para gestionar operaciones con bases de datos vectoriales.
|
||||
|
||||
Este servicio actúa como una capa intermedia entre los routers y
|
||||
la implementación de la base de datos vectorial.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inicializa el servicio con la instancia de la base de datos vectorial."""
|
||||
self.vector_db = get_vector_db()
|
||||
|
||||
async def check_collection_exists(self, collection_name: str) -> CollectionExistsResponse:
|
||||
"""
|
||||
Verifica si una colección existe.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
|
||||
Returns:
|
||||
CollectionExistsResponse: Response con el resultado
|
||||
"""
|
||||
try:
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
logger.info(f"Verificación de colección '{collection_name}': {exists}")
|
||||
|
||||
return CollectionExistsResponse(
|
||||
exists=exists,
|
||||
collection_name=collection_name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al verificar colección '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def create_collection(
|
||||
self,
|
||||
request: CollectionCreateRequest
|
||||
) -> CollectionCreateResponse:
|
||||
"""
|
||||
Crea una nueva colección.
|
||||
|
||||
Args:
|
||||
request: Request con los datos de la colección
|
||||
|
||||
Returns:
|
||||
CollectionCreateResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección ya existe
|
||||
"""
|
||||
try:
|
||||
# Verificar si ya existe
|
||||
exists = await self.vector_db.collection_exists(request.collection_name)
|
||||
|
||||
if exists:
|
||||
logger.warning(f"Intento de crear colección existente: '{request.collection_name}'")
|
||||
raise ValueError(f"La colección '{request.collection_name}' ya existe")
|
||||
|
||||
# Crear la colección
|
||||
success = await self.vector_db.create_collection(
|
||||
collection_name=request.collection_name,
|
||||
vector_size=request.vector_size,
|
||||
distance=request.distance
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Colección '{request.collection_name}' creada exitosamente")
|
||||
return CollectionCreateResponse(
|
||||
success=True,
|
||||
collection_name=request.collection_name,
|
||||
message=f"Colección '{request.collection_name}' creada exitosamente"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Fallo al crear colección '{request.collection_name}'")
|
||||
raise Exception(f"No se pudo crear la colección '{request.collection_name}'")
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al crear colección '{request.collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def delete_collection(self, collection_name: str) -> CollectionDeleteResponse:
|
||||
"""
|
||||
Elimina una colección completa.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
|
||||
Returns:
|
||||
CollectionDeleteResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe
|
||||
"""
|
||||
try:
|
||||
# Verificar que existe
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not exists:
|
||||
logger.warning(f"Intento de eliminar colección inexistente: '{collection_name}'")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Eliminar la colección
|
||||
success = await self.vector_db.delete_collection(collection_name)
|
||||
|
||||
if success:
|
||||
logger.info(f"Colección '{collection_name}' eliminada exitosamente")
|
||||
return CollectionDeleteResponse(
|
||||
success=True,
|
||||
collection_name=collection_name,
|
||||
message=f"Colección '{collection_name}' eliminada exitosamente"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Fallo al eliminar colección '{collection_name}'")
|
||||
raise Exception(f"No se pudo eliminar la colección '{collection_name}'")
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al eliminar colección '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def get_collection_info(self, collection_name: str) -> Optional[CollectionInfoResponse]:
|
||||
"""
|
||||
Obtiene información de una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
|
||||
Returns:
|
||||
Optional[CollectionInfoResponse]: Información de la colección o None
|
||||
"""
|
||||
try:
|
||||
info = await self.vector_db.get_collection_info(collection_name)
|
||||
|
||||
if info is None:
|
||||
logger.warning(f"Colección '{collection_name}' no encontrada")
|
||||
return None
|
||||
|
||||
return CollectionInfoResponse(**info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener info de colección '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def check_file_exists_in_collection(
|
||||
self,
|
||||
collection_name: str,
|
||||
file_name: str
|
||||
) -> FileExistsInCollectionResponse:
|
||||
"""
|
||||
Verifica si un archivo existe en una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
file_name: Nombre del archivo
|
||||
|
||||
Returns:
|
||||
FileExistsInCollectionResponse: Response con el resultado
|
||||
"""
|
||||
try:
|
||||
# Primero verificar que la colección existe
|
||||
collection_exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not collection_exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
return FileExistsInCollectionResponse(
|
||||
exists=False,
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunk_count=0
|
||||
)
|
||||
|
||||
# Verificar si el archivo existe
|
||||
file_exists = await self.vector_db.file_exists_in_collection(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
chunk_count = None
|
||||
if file_exists:
|
||||
chunk_count = await self.vector_db.count_chunks_in_file(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Archivo '{file_name}' en colección '{collection_name}': "
|
||||
f"existe={file_exists}, chunks={chunk_count}"
|
||||
)
|
||||
|
||||
return FileExistsInCollectionResponse(
|
||||
exists=file_exists,
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunk_count=chunk_count
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error al verificar archivo '{file_name}' "
|
||||
f"en colección '{collection_name}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def get_chunks_by_file(
|
||||
self,
|
||||
collection_name: str,
|
||||
file_name: str,
|
||||
limit: Optional[int] = None
|
||||
) -> GetChunksByFileResponse:
|
||||
"""
|
||||
Obtiene todos los chunks de un archivo.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
file_name: Nombre del archivo
|
||||
limit: Límite opcional de chunks
|
||||
|
||||
Returns:
|
||||
GetChunksByFileResponse: Response con los chunks
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe
|
||||
"""
|
||||
try:
|
||||
# Verificar que la colección existe
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Obtener chunks
|
||||
chunks = await self.vector_db.get_chunks_by_file(
|
||||
collection_name,
|
||||
file_name,
|
||||
limit
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Obtenidos {len(chunks)} chunks del archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}'"
|
||||
)
|
||||
|
||||
return GetChunksByFileResponse(
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunks=chunks,
|
||||
total_chunks=len(chunks)
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error al obtener chunks del archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def delete_file_from_collection(
|
||||
self,
|
||||
collection_name: str,
|
||||
file_name: str
|
||||
) -> DeleteFileFromCollectionResponse:
|
||||
"""
|
||||
Elimina todos los chunks de un archivo de una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
file_name: Nombre del archivo
|
||||
|
||||
Returns:
|
||||
DeleteFileFromCollectionResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe o el archivo no está en la colección
|
||||
"""
|
||||
try:
|
||||
# Verificar que la colección existe
|
||||
collection_exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not collection_exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Verificar que el archivo existe en la colección
|
||||
file_exists = await self.vector_db.file_exists_in_collection(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
if not file_exists:
|
||||
logger.warning(
|
||||
f"Archivo '{file_name}' no existe en colección '{collection_name}'"
|
||||
)
|
||||
raise ValueError(
|
||||
f"El archivo '{file_name}' no existe en la colección '{collection_name}'"
|
||||
)
|
||||
|
||||
# Eliminar el archivo
|
||||
chunks_deleted = await self.vector_db.delete_file_from_collection(
|
||||
collection_name,
|
||||
file_name
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Eliminados {chunks_deleted} chunks del archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}'"
|
||||
)
|
||||
|
||||
return DeleteFileFromCollectionResponse(
|
||||
success=True,
|
||||
collection_name=collection_name,
|
||||
file_name=file_name,
|
||||
chunks_deleted=chunks_deleted,
|
||||
message=f"Archivo '{file_name}' eliminado exitosamente ({chunks_deleted} chunks)"
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error al eliminar archivo '{file_name}' "
|
||||
f"de la colección '{collection_name}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def add_chunks(
|
||||
self,
|
||||
collection_name: str,
|
||||
chunks: List[Dict[str, Any]]
|
||||
) -> AddChunksResponse:
|
||||
"""
|
||||
Agrega chunks a una colección.
|
||||
|
||||
Args:
|
||||
collection_name: Nombre de la colección
|
||||
chunks: Lista de chunks a agregar
|
||||
|
||||
Returns:
|
||||
AddChunksResponse: Response con el resultado
|
||||
|
||||
Raises:
|
||||
ValueError: Si la colección no existe
|
||||
"""
|
||||
try:
|
||||
# Verificar que la colección existe
|
||||
exists = await self.vector_db.collection_exists(collection_name)
|
||||
|
||||
if not exists:
|
||||
logger.warning(f"Colección '{collection_name}' no existe")
|
||||
raise ValueError(f"La colección '{collection_name}' no existe")
|
||||
|
||||
# Agregar chunks
|
||||
success = await self.vector_db.add_chunks(collection_name, chunks)
|
||||
|
||||
if success:
|
||||
logger.info(
|
||||
f"Agregados {len(chunks)} chunks a la colección '{collection_name}'"
|
||||
)
|
||||
return AddChunksResponse(
|
||||
success=True,
|
||||
collection_name=collection_name,
|
||||
chunks_added=len(chunks),
|
||||
message=f"Se agregaron {len(chunks)} chunks exitosamente"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Fallo al agregar chunks a '{collection_name}'")
|
||||
raise Exception(f"No se pudieron agregar los chunks a '{collection_name}'")
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al agregar chunks a '{collection_name}': {e}")
|
||||
raise
|
||||
|
||||
async def health_check(self) -> VectorDBHealthResponse:
|
||||
"""
|
||||
Verifica el estado de la conexión con la base de datos vectorial.
|
||||
|
||||
Returns:
|
||||
VectorDBHealthResponse: Response con el estado
|
||||
"""
|
||||
try:
|
||||
is_healthy = await self.vector_db.health_check()
|
||||
|
||||
if is_healthy:
|
||||
return VectorDBHealthResponse(
|
||||
status="healthy",
|
||||
db_type="qdrant",
|
||||
message="Conexión exitosa con la base de datos vectorial"
|
||||
)
|
||||
else:
|
||||
return VectorDBHealthResponse(
|
||||
status="unhealthy",
|
||||
db_type="qdrant",
|
||||
message="No se pudo conectar con la base de datos vectorial"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en health check: {e}")
|
||||
return VectorDBHealthResponse(
|
||||
status="error",
|
||||
db_type="qdrant",
|
||||
message=f"Error al verificar conexión: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# Instancia global del servicio
|
||||
vector_service = VectorService()
|
||||
Reference in New Issue
Block a user