Skip to main content

Componentes de la Aplicación IRIS

Arquitectura de Componentes

IRIS está construido con una arquitectura modular que separa las responsabilidades en componentes especializados. Cada componente tiene un propósito específico dentro del pipeline de procesamiento de documentos.

Componentes Frontend

Dashboard Principal

El dashboard es el punto de entrada principal para los usuarios.

Características

  • Vista general: Estadísticas de procesamiento en tiempo real
  • Historial: Lista de documentos procesados recientemente
  • Estado de servicios: Indicadores de salud de los microservicios
  • Navegación: Acceso rápido a todas las funcionalidades

Tecnologías

  • React: Biblioteca principal para UI
  • TypeScript: Tipado estático
  • Material-UI: Componentes de interfaz
  • React Router: Navegación entre páginas
  • Axios: Cliente HTTP para comunicación con API
// components/Dashboard.tsx
import React, { useEffect, useState } from 'react';
import { Card, Grid, Typography, CircularProgress } from '@mui/material';
import { DashboardStats, ServiceHealth } from '../types';
import { apiClient } from '../services/api';

export const Dashboard: React.FC = () => {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [services, setServices] = useState<ServiceHealth[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchData = async () => {
try {
const [statsRes, servicesRes] = await Promise.all([
apiClient.get('/admin/stats'),
apiClient.get('/services')
]);

setStats(statsRes.data);
setServices(servicesRes.data);
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};

fetchData();
const interval = setInterval(fetchData, 30000); // Actualizar cada 30s

return () => clearInterval(interval);
}, []);

if (loading) {
return <CircularProgress />;
}

return (
<Grid container spacing={3}>
<Grid item xs={12} md={3}>
<Card>
<Typography variant="h6">Documentos Procesados</Typography>
<Typography variant="h4">{stats?.total_requests}</Typography>
</Card>
</Grid>

<Grid item xs={12} md={3}>
<Card>
<Typography variant="h6">Tasa de Éxito</Typography>
<Typography variant="h4">
{((stats?.successful_requests / stats?.total_requests) * 100).toFixed(1)}%
</Typography>
</Card>
</Grid>

<Grid item xs={12} md={6}>
<Card>
<Typography variant="h6">Estado de Servicios</Typography>
{services.map(service => (
<div key={service.name}>
<Typography>
{service.name}: {service.status === 'healthy' ? '✅' : '❌'}
</Typography>
</div>
))}
</Card>
</Grid>
</Grid>
);
};

Componente de Subida de Archivos

Permite a los usuarios subir documentos para procesamiento.

// components/FileUpload.tsx
import React, { useState } from 'react';
import {
Box,
Button,
LinearProgress,
Typography,
Alert,
FormControl,
InputLabel,
Select,
MenuItem
} from '@mui/material';
import { useDropzone } from 'react-dropzone';
import { uploadDocument } from '../services/documentService';

interface FileUploadProps {
onSuccess: (result: any) => void;
onError: (error: string) => void;
}

export const FileUpload: React.FC<FileUploadProps> = ({ onSuccess, onError }) => {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [documentType, setDocumentType] = useState('auto');
const [language, setLanguage] = useState('es');

const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
'image/*': ['.jpg', '.jpeg', '.png'],
'application/pdf': ['.pdf']
},
maxFiles: 1,
onDrop: handleFileUpload
});

async function handleFileUpload(files: File[]) {
if (files.length === 0) return;

const file = files[0];
setUploading(true);
setProgress(0);

try {
const result = await uploadDocument(file, {
documentType,
language,
onProgress: setProgress
});

onSuccess(result);
} catch (error) {
onError(error.message);
} finally {
setUploading(false);
setProgress(0);
}
}

return (
<Box>
<Box sx={{ mb: 2 }}>
<FormControl sx={{ mr: 2, minWidth: 120 }}>
<InputLabel>Tipo de Documento</InputLabel>
<Select
value={documentType}
onChange={(e) => setDocumentType(e.target.value)}
>
<MenuItem value="auto">Detectar Automáticamente</MenuItem>
<MenuItem value="ficha_residencia">Ficha de Residencia</MenuItem>
<MenuItem value="documento_identidad">Documento de Identidad</MenuItem>
<MenuItem value="formulario">Formulario</MenuItem>
</Select>
</FormControl>

<FormControl sx={{ minWidth: 120 }}>
<InputLabel>Idioma</InputLabel>
<Select
value={language}
onChange={(e) => setLanguage(e.target.value)}
>
<MenuItem value="es">Español</MenuItem>
<MenuItem value="en">Inglés</MenuItem>
<MenuItem value="fr">Francés</MenuItem>
</Select>
</FormControl>
</Box>

<Box
{...getRootProps()}
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 4,
textAlign: 'center',
cursor: 'pointer',
backgroundColor: isDragActive ? '#f5f5f5' : 'transparent'
}}
>
<input {...getInputProps()} />
{isDragActive ? (
<Typography>Suelta el archivo aquí...</Typography>
) : (
<Typography>
Arrastra un archivo aquí o haz clic para seleccionar
</Typography>
)}
</Box>

{uploading && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2">Procesando documento...</Typography>
<LinearProgress variant="determinate" value={progress} />
<Typography variant="caption">{progress}%</Typography>
</Box>
)}
</Box>
);
};

Visor de Resultados

Muestra los resultados del procesamiento de documentos.

// components/ResultsViewer.tsx
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Tabs,
Tab,
Accordion,
AccordionSummary,
AccordionDetails,
Chip
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { ProcessingResult } from '../types';

interface ResultsViewerProps {
result: ProcessingResult;
}

export const ResultsViewer: React.FC<ResultsViewerProps> = ({ result }) => {
const [currentTab, setCurrentTab] = useState(0);

const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.9) return 'success';
if (confidence >= 0.7) return 'warning';
return 'error';
};

return (
<Card>
<CardContent>
<Box sx={{ mb: 2 }}>
<Typography variant="h6">Resultados del Procesamiento</Typography>
<Chip
label={`Tipo: ${result.document_type}`}
color="primary"
sx={{ mr: 1 }}
/>
<Chip
label={`Confianza: ${(result.confidence * 100).toFixed(1)}%`}
color={getConfidenceColor(result.confidence)}
/>
</Box>

<Tabs value={currentTab} onChange={(_, value) => setCurrentTab(value)}>
<Tab label="Datos Estructurados" />
<Tab label="Texto Extraído" />
<Tab label="Fases de Procesamiento" />
<Tab label="Imágenes" />
</Tabs>

{currentTab === 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Datos Extraídos</Typography>
{Object.entries(result.extracted_data).map(([key, value]) => (
<Box key={key} sx={{ mb: 1 }}>
<Typography variant="subtitle2" component="span">
{key}:
</Typography>
<Typography component="span" sx={{ ml: 1 }}>
{String(value)}
</Typography>
</Box>
))}
</Box>
)}

{currentTab === 1 && (
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Texto Completo</Typography>
<Typography variant="body2" sx={{ whiteSpace: 'pre-line', mt: 1 }}>
{result.extracted_text}
</Typography>
</Box>
)}

{currentTab === 2 && (
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Detalles de Procesamiento</Typography>
{Object.entries(result.phases).map(([phase, data]) => (
<Accordion key={phase}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>
{phase} - {data.processing_time.toFixed(2)}s
</Typography>
<Chip
label={data.status}
color={data.status === 'completed' ? 'success' : 'error'}
size="small"
sx={{ ml: 2 }}
/>
</AccordionSummary>
<AccordionDetails>
<pre>{JSON.stringify(data.output, null, 2)}</pre>
</AccordionDetails>
</Accordion>
))}
</Box>
)}

{currentTab === 3 && (
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Imágenes Procesadas</Typography>
{result.processed_images?.map((img, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle2">
Fase {index + 1}
</Typography>
<img
src={`data:image/jpeg;base64,${img}`}
alt={`Procesada fase ${index + 1}`}
style={{ maxWidth: '100%', height: 'auto' }}
/>
</Box>
))}
</Box>
)}
</CardContent>
</Card>
);
};

Componentes Backend

Servicios de API

Servicio de Documentos

# services/document_service.py
from typing import Optional, Dict, Any
import httpx
import asyncio
from fastapi import HTTPException
from core.config import settings
from models.document import DocumentProcessingRequest, ProcessingResult

class DocumentService:
def __init__(self):
self.client = httpx.AsyncClient(timeout=300.0)
self.services = settings.SERVICES_CONFIG

async def process_document(
self,
file_content: bytes,
filename: str,
document_type: str = "auto",
language: str = "es"
) -> ProcessingResult:
"""Procesar documento a través del pipeline completo"""

try:
# Fase 1: Preprocesamiento
phase1_result = await self._call_service(
"image_processor",
"/process",
files={"file": (filename, file_content)},
data={"enhance": True, "unwarp": True}
)

# Fase 2: Embeddings y clustering
phase2_result = await self._call_service(
"ml_embeddings",
"/embed",
json={"image": phase1_result["corrected_image"]}
)

# Fase 3: Clasificación
if document_type == "auto":
classification_result = await self._call_service(
"ml_classifier",
"/classify",
json={"image": phase1_result["corrected_image"]}
)
document_type = classification_result["predicted_class"]

# Fase 4: OCR especializado
ocr_result = await self._call_service(
"ocr_extractor",
"/extract",
json={
"image": phase1_result["corrected_image"],
"document_type": document_type,
"language": language
}
)

# Construir resultado final
return ProcessingResult(
success=True,
document_type=document_type,
extracted_text=ocr_result["text"],
extracted_data=ocr_result["structured_data"],
confidence=ocr_result["confidence"],
phases={
"phase_1": phase1_result,
"phase_2": phase2_result,
"phase_3": {"document_type": document_type},
"phase_4": ocr_result
},
processing_time=sum([
phase1_result.get("processing_time", 0),
phase2_result.get("processing_time", 0),
ocr_result.get("processing_time", 0)
])
)

except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error procesando documento: {str(e)}"
)

async def _call_service(
self,
service_name: str,
endpoint: str,
**kwargs
) -> Dict[Any, Any]:
"""Llamar a un microservicio específico"""

service_config = self.services[service_name]
url = f"{service_config['url']}{endpoint}"

try:
response = await self.client.post(url, **kwargs)
response.raise_for_status()
return response.json()
except httpx.HTTPError as e:
raise HTTPException(
status_code=502,
detail=f"Error comunicándose con {service_name}: {str(e)}"
)

async def get_processing_history(
self,
user_id: str,
limit: int = 50
) -> List[ProcessingResult]:
"""Obtener historial de procesamiento del usuario"""
# Implementar consulta a base de datos
pass

async def get_document_details(
self,
document_id: str,
user_id: str
) -> Optional[ProcessingResult]:
"""Obtener detalles de un documento específico"""
# Implementar consulta a base de datos
pass

Servicio de Autenticación

# services/auth_service.py
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, status
from models.user import User, UserCreate, Token
from core.config import settings

class AuthService:
def __init__(self):
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
self.secret_key = settings.JWT_SECRET_KEY
self.algorithm = settings.JWT_ALGORITHM

def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""Verificar password"""
return self.pwd_context.verify(plain_password, hashed_password)

def get_password_hash(self, password: str) -> str:
"""Generar hash de password"""
return self.pwd_context.hash(password)

def create_access_token(
self,
data: dict,
expires_delta: Optional[timedelta] = None
) -> str:
"""Crear token JWT"""
to_encode = data.copy()

if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
)

to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode,
self.secret_key,
algorithm=self.algorithm
)

return encoded_jwt

def verify_token(self, token: str) -> dict:
"""Verificar y decodificar token JWT"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token inválido",
headers={"WWW-Authenticate": "Bearer"},
)

async def authenticate_user(
self,
email: str,
password: str
) -> Optional[User]:
"""Autenticar usuario con email y password"""
# Implementar consulta a base de datos
pass

async def create_user(self, user_data: UserCreate) -> User:
"""Crear nuevo usuario"""
# Verificar que el email no existe
# Hash del password
# Crear en base de datos
pass

async def get_current_user(self, token: str) -> User:
"""Obtener usuario actual desde token"""
payload = self.verify_token(token)
user_id = payload.get("sub")

if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token inválido"
)

# Buscar usuario en base de datos
# return user
pass

Modelos de Datos

# models/document.py
from pydantic import BaseModel, Field
from typing import Dict, Any, Optional, List
from datetime import datetime

class DocumentProcessingRequest(BaseModel):
document_type: str = Field(default="auto", description="Tipo de documento")
language: str = Field(default="es", description="Idioma del documento")
enhance: bool = Field(default=True, description="Mejorar calidad de imagen")
unwarp: bool = Field(default=True, description="Corregir distorsiones")

class PhaseResult(BaseModel):
status: str
processing_time: float
output: Dict[Any, Any]
confidence: Optional[float] = None

class ProcessingResult(BaseModel):
success: bool
processing_id: str
document_type: str
extracted_text: str
extracted_data: Dict[str, Any]
confidence: float
phases: Dict[str, PhaseResult]
processing_time: float
created_at: datetime = Field(default_factory=datetime.utcnow)
user_id: Optional[str] = None

class DocumentMetadata(BaseModel):
filename: str
file_size: int
file_type: str
upload_date: datetime
processing_status: str
error_message: Optional[str] = None

Componentes de Infraestructura

Cache Manager

# infrastructure/cache.py
import redis
import json
from typing import Any, Optional
from core.config import settings

class CacheManager:
def __init__(self):
self.redis_client = redis.Redis.from_url(settings.REDIS_URL)
self.default_ttl = settings.CACHE_TTL

async def get(self, key: str) -> Optional[Any]:
"""Obtener valor del cache"""
try:
value = self.redis_client.get(key)
if value:
return json.loads(value)
return None
except Exception as e:
print(f"Error obteniendo del cache: {e}")
return None

async def set(
self,
key: str,
value: Any,
ttl: Optional[int] = None
) -> bool:
"""Guardar valor en cache"""
try:
ttl = ttl or self.default_ttl
serialized_value = json.dumps(value, default=str)
return self.redis_client.setex(key, ttl, serialized_value)
except Exception as e:
print(f"Error guardando en cache: {e}")
return False

async def delete(self, key: str) -> bool:
"""Eliminar valor del cache"""
try:
return bool(self.redis_client.delete(key))
except Exception as e:
print(f"Error eliminando del cache: {e}")
return False

async def invalidate_pattern(self, pattern: str) -> int:
"""Invalidar todas las claves que coincidan con el patrón"""
try:
keys = self.redis_client.keys(pattern)
if keys:
return self.redis_client.delete(*keys)
return 0
except Exception as e:
print(f"Error invalidando patrón: {e}")
return 0

Database Manager

# infrastructure/database.py
from sqlalchemy import create_engine, MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from core.config import settings

# Crear engine de base de datos
engine = create_engine(
settings.DATABASE_URL,
pool_pre_ping=True,
pool_recycle=300,
echo=settings.DEBUG
)

# Crear sessionmaker
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Base para modelos
Base = declarative_base()

# Metadata
metadata = MetaData()

def get_db():
"""Dependency para obtener sesión de base de datos"""
db = SessionLocal()
try:
yield db
finally:
db.close()

def create_tables():
"""Crear todas las tablas"""
Base.metadata.create_all(bind=engine)

def drop_tables():
"""Eliminar todas las tablas"""
Base.metadata.drop_all(bind=engine)