diff --git a/backend/app/main.py b/backend/app/main.py index fe90e8d..cd07129 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -209,7 +209,74 @@ def send_completed_inspection_to_n8n(inspection, db): # No lanzamos excepción para no interrumpir el flujo normal -BACKEND_VERSION = "1.0.94" +# ============================================================================ +# UTILIDADES PARA PROCESAMIENTO DE PDFs +# ============================================================================ + +def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict: + """ + Extrae texto de un PDF de forma inteligente, evitando duplicaciones + y manejando PDFs grandes. + + Args: + pdf_content: Contenido del PDF en bytes + max_chars: Límite máximo de caracteres (None = sin límite) + + Returns: + dict con 'text', 'pages', 'total_chars', 'truncated' + """ + from pypdf import PdfReader + from io import BytesIO + + try: + pdf_file = BytesIO(pdf_content) + pdf_reader = PdfReader(pdf_file) + + full_text = "" + pages_processed = 0 + total_pages = len(pdf_reader.pages) + + for page_num, page in enumerate(pdf_reader.pages, 1): + page_text = page.extract_text() + + # Limpiar y validar texto de la página + if page_text and page_text.strip(): + # Evitar duplicación: verificar si el texto ya existe + # (algunos PDFs pueden tener páginas repetidas) + if page_text.strip() not in full_text: + full_text += f"\n--- Página {page_num}/{total_pages} ---\n{page_text.strip()}\n" + pages_processed += 1 + + # Si hay límite y lo alcanzamos, detener + if max_chars and len(full_text) >= max_chars: + break + + total_chars = len(full_text) + truncated = False + + # Aplicar límite si se especificó + if max_chars and total_chars > max_chars: + full_text = full_text[:max_chars] + truncated = True + + return { + 'text': full_text, + 'pages': total_pages, + 'pages_processed': pages_processed, + 'total_chars': total_chars, + 'truncated': truncated, + 'success': True + } + + except Exception as e: + return { + 'text': '', + 'error': str(e), + 'success': False + } + + +BACKEND_VERSION = "1.0.95" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -2598,7 +2665,6 @@ async def analyze_image( # Guardar archivo temporalmente y procesar según tipo import base64 - from pypdf import PdfReader contents = await file.read() file_type = file.content_type @@ -2610,34 +2676,30 @@ async def analyze_image( if is_pdf: print("📕 Detectado PDF - extrayendo texto...") - try: - from io import BytesIO - pdf_file = BytesIO(contents) - pdf_reader = PdfReader(pdf_file) - - # Extraer texto de todas las páginas - pdf_text = "" - for page_num, page in enumerate(pdf_reader.pages): - pdf_text += f"\n--- Página {page_num + 1} ---\n" - pdf_text += page.extract_text() - - print(f"✅ Texto extraído: {len(pdf_text)} caracteres") - - if not pdf_text.strip(): - return { - "status": "error", - "message": "No se pudo extraer texto del PDF. Puede ser un PDF escaneado sin OCR." - } - - # Para PDFs usamos análisis de texto, no de imagen - image_b64 = None - - except Exception as e: - print(f"❌ Error al procesar PDF: {str(e)}") + + # Usar función inteligente de extracción + # Para análisis de imagen usamos hasta 100k caracteres (Gemini soporta mucho más) + pdf_result = extract_pdf_text_smart(contents, max_chars=100000) + + if not pdf_result['success']: return { "status": "error", - "message": f"Error al procesar PDF: {str(e)}" + "message": f"Error al procesar PDF: {pdf_result.get('error', 'Unknown')}" } + + pdf_text = pdf_result['text'] + print(f"✅ Texto extraído: {pdf_result['total_chars']} caracteres de {pdf_result['pages_processed']}/{pdf_result['pages']} páginas") + if pdf_result['truncated']: + print(f"⚠️ PDF truncado a 100k caracteres") + + if not pdf_text.strip(): + return { + "status": "error", + "message": "No se pudo extraer texto del PDF. Puede ser un PDF escaneado sin OCR." + } + + # Para PDFs usamos análisis de texto, no de imagen + image_b64 = None else: # Es una imagen image_b64 = base64.b64encode(contents).decode('utf-8') @@ -2819,7 +2881,7 @@ NOTA: {"role": "system", "content": system_prompt}, { "role": "user", - "content": f"{user_message}\n\n--- CONTENIDO DEL DOCUMENTO PDF ---\n{pdf_text[:4000]}" # Limitar a 4000 chars + "content": f"{user_message}\n\n--- CONTENIDO DEL DOCUMENTO PDF ({len(pdf_text)} caracteres) ---\n{pdf_text[:30000]}" # 30k chars para GPT-4 } ] else: @@ -2860,8 +2922,8 @@ NOTA: prompt = f"{system_prompt}\n\n{user_message}" if is_pdf: - # Para PDF, solo texto - prompt_with_content = f"{prompt}\n\n--- CONTENIDO DEL DOCUMENTO PDF ---\n{pdf_text[:4000]}" + # Para PDF, solo texto - Gemini puede manejar contextos muy largos (2M tokens) + prompt_with_content = f"{prompt}\n\n--- CONTENIDO DEL DOCUMENTO PDF ({len(pdf_text)} caracteres) ---\n{pdf_text[:100000]}" response = model.generate_content(prompt_with_content) else: # Para imagen, incluir imagen @@ -3036,8 +3098,6 @@ async def chat_with_ai_assistant( attached_files_data = [] if files: import base64 - from pypdf import PdfReader - from io import BytesIO for file in files: file_content = await file.read() @@ -3051,18 +3111,22 @@ async def chat_with_ai_assistant( # Si es PDF, extraer texto if file_type == 'application/pdf' or file.filename.lower().endswith('.pdf'): - try: - pdf_file = BytesIO(file_content) - pdf_reader = PdfReader(pdf_file) - pdf_text = "" - for page in pdf_reader.pages: - pdf_text += page.extract_text() + # Usar función inteligente - límite de 50k para chat (balance entre contexto y tokens) + pdf_result = extract_pdf_text_smart(file_content, max_chars=50000) + + if pdf_result['success']: file_info['content_type'] = 'pdf' - file_info['text'] = pdf_text[:2000] # Limitar texto - print(f"📄 PDF procesado: {file.filename} - {len(pdf_text)} caracteres") - except Exception as e: - print(f"❌ Error procesando PDF {file.filename}: {str(e)}") - file_info['error'] = str(e) + file_info['text'] = pdf_result['text'] + file_info['total_chars'] = pdf_result['total_chars'] + file_info['pages'] = pdf_result['pages'] + file_info['pages_processed'] = pdf_result['pages_processed'] + file_info['truncated'] = pdf_result['truncated'] + + truncated_msg = " (TRUNCADO)" if pdf_result['truncated'] else "" + print(f"📄 PDF procesado: {file.filename} - {pdf_result['total_chars']} caracteres, {pdf_result['pages_processed']}/{pdf_result['pages']} páginas{truncated_msg}") + else: + print(f"❌ Error procesando PDF {file.filename}: {pdf_result.get('error', 'Unknown')}") + file_info['error'] = pdf_result.get('error', 'Error desconocido') # Si es imagen, convertir a base64 elif file_type.startswith('image/'): @@ -3120,9 +3184,12 @@ INFORMACIÓN DEL VEHÍCULO: attached_context = f"\n\nARCHIVOS ADJUNTOS EN ESTE MENSAJE ({len(attached_files_data)} archivos):\n" for idx, file_info in enumerate(attached_files_data, 1): if file_info.get('content_type') == 'pdf': - attached_context += f"\n{idx}. PDF: {file_info['filename']}\n" + truncated_indicator = " ⚠️TRUNCADO" if file_info.get('truncated') else "" + pages_info = f" ({file_info.get('pages_processed', '?')}/{file_info.get('pages', '?')} páginas, {file_info.get('total_chars', '?')} caracteres{truncated_indicator})" if 'pages' in file_info else "" + attached_context += f"\n{idx}. PDF: {file_info['filename']}{pages_info}\n" if 'text' in file_info: - attached_context += f" Contenido: {file_info['text'][:500]}...\n" + # Mostrar más contexto del PDF (primeros 2000 caracteres como preview) + attached_context += f" Contenido: {file_info['text'][:2000]}...\n" elif file_info.get('content_type') == 'image': attached_context += f"\n{idx}. Imagen: {file_info['filename']}\n" diff --git a/docs/pdf-extraction-improvements.md b/docs/pdf-extraction-improvements.md new file mode 100644 index 0000000..ac8cc61 --- /dev/null +++ b/docs/pdf-extraction-improvements.md @@ -0,0 +1,155 @@ +# Mejoras en la Extracción de PDFs con IA + +## Versión Backend: 1.0.95 + +## Problema Original + +El sistema tenía limitaciones al procesar PDFs con IA: + +1. **Límites muy pequeños**: Solo extraía 2,000-4,000 caracteres +2. **Sin manejo de duplicaciones**: Páginas repetidas se procesaban múltiples veces +3. **No aprovechaba contextos largos**: Los modelos modernos soportan millones de tokens +4. **Falta de información**: No reportaba páginas procesadas o si el contenido fue truncado + +## Solución Implementada + +### 1. Función Centralizada de Extracción + +Nueva función `extract_pdf_text_smart()` que: +- ✅ Extrae texto de forma inteligente +- ✅ Detecta y evita páginas duplicadas +- ✅ Maneja límites configurables +- ✅ Reporta estadísticas completas (páginas, caracteres, truncado) +- ✅ Manejo robusto de errores + +```python +pdf_result = extract_pdf_text_smart(pdf_content, max_chars=50000) +# Retorna: +# { +# 'text': '...', +# 'pages': 10, +# 'pages_processed': 9, # Si una página estaba duplicada +# 'total_chars': 45000, +# 'truncated': False, +# 'success': True +# } +``` + +### 2. Límites Optimizados por Caso de Uso + +| Endpoint | Límite Anterior | Límite Nuevo | Modelo Objetivo | +|----------|----------------|--------------|-----------------| +| `/api/analyze-image` (OpenAI) | 4,000 chars | 30,000 chars | GPT-4 (128k tokens) | +| `/api/analyze-image` (Gemini) | 4,000 chars | 100,000 chars | Gemini 1.5/2.0 (2M tokens) | +| `/api/ai/chat-assistant` | 2,000 chars | 50,000 chars | Equilibrado para contexto | + +### 3. Detección de Duplicaciones + +El sistema ahora verifica si el contenido de una página ya existe antes de agregarlo: + +```python +if page_text.strip() not in full_text: + full_text += f"\n--- Página {page_num}/{total_pages} ---\n{page_text.strip()}\n" +``` + +Esto previene: +- PDFs con páginas idénticas repetidas +- Documentos mal generados con contenido duplicado +- Uso innecesario de tokens en el análisis IA + +### 4. Información Mejorada + +El sistema ahora reporta: +- **Páginas totales**: Total de páginas en el PDF +- **Páginas procesadas**: Páginas únicas con contenido +- **Caracteres totales**: Tamaño real del texto extraído +- **Indicador de truncado**: Si el PDF fue limitado + +Ejemplo de output: +``` +📄 PDF procesado: manual-vehiculo.pdf - 87450 caracteres, 8/10 páginas (TRUNCADO) +``` + +## Capacidades de Contexto por Modelo + +### OpenAI GPT-4 +- **Contexto**: ~128,000 tokens (~500,000 caracteres) +- **Límite aplicado**: 30,000 caracteres +- **Razón**: Balance entre contexto útil y costo + +### Gemini 1.5/2.0 Pro +- **Contexto**: 2,000,000 tokens (~8,000,000 caracteres) +- **Límite aplicado**: 100,000 caracteres +- **Razón**: Aprovechar contexto masivo sin sobrecargar + +### Chat Assistant +- **Límite**: 50,000 caracteres +- **Razón**: Incluye historial + contexto de fotos + PDF + +## Casos de Uso Soportados + +### ✅ PDFs Pequeños (1-5 páginas) +Extracción completa sin truncado + +### ✅ PDFs Medianos (5-20 páginas) +Extracción completa o parcial según contenido + +### ✅ PDFs Grandes (20+ páginas) +Extracción inteligente con truncado después de límite + +### ✅ PDFs con Páginas Duplicadas +Detección automática y eliminación + +### ✅ Múltiples PDFs en Chat +Cada uno procesado independientemente con su límite + +## Indicadores de Estado + +### En Logs del Servidor +``` +📄 PDF procesado: documento.pdf - 25000 caracteres, 10/10 páginas +📄 PDF procesado: manual.pdf - 50000 caracteres, 15/20 páginas (TRUNCADO) +``` + +### En Respuesta al Cliente +```json +{ + "attached_files": [ + { + "filename": "manual.pdf", + "type": "application/pdf", + "pages": 20, + "pages_processed": 15, + "total_chars": 75000, + "truncated": true + } + ] +} +``` + +## Próximas Mejoras Potenciales + +1. **Chunking Inteligente**: Para PDFs muy grandes, dividir en chunks semánticos +2. **OCR Integrado**: Detectar PDFs escaneados y aplicar OCR automático +3. **Resumen Automático**: Para PDFs grandes, generar resumen antes de análisis +4. **Cache de Extracciones**: Guardar texto extraído en DB para reutilización + +## Migración + +No requiere migración de base de datos. Los cambios son retrocompatibles. + +## Testing + +Para probar las mejoras: + +1. **PDF pequeño** (< 10 páginas): Debe procesarse completo +2. **PDF grande** (> 50 páginas): Debe truncarse y reportar info +3. **PDF con duplicados**: Debe eliminar páginas repetidas +4. **Múltiples PDFs**: Cada uno procesado independientemente + +## Notas Técnicas + +- La función `extract_pdf_text_smart()` está en `main.py` línea ~210 +- Usa `pypdf.PdfReader` para extracción +- Maneja encoding UTF-8 automáticamente +- Thread-safe (usa BytesIO) diff --git a/frontend/public/ayutec_logo.webp b/frontend/public/ayutec_logo.webp new file mode 100644 index 0000000..2fe08dc Binary files /dev/null and b/frontend/public/ayutec_logo.webp differ diff --git a/frontend/src/Sidebar.jsx b/frontend/src/Sidebar.jsx index 767cbaa..72c8071 100644 --- a/frontend/src/Sidebar.jsx +++ b/frontend/src/Sidebar.jsx @@ -20,9 +20,9 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se {sidebarOpen && (
Ayutec

Ayutec

@@ -141,16 +141,21 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se {/* Versión */} {sidebarOpen && (
-
+ Ayutec -

+

Ayutec v1.1.0

-
+
)}