26. Validación de las fuentes de conocimiento

 

26: Validación de las Fuentes de Conocimiento

En este tutorial, implementaremos la validación completa para las fuentes de conocimiento. Aprenderemos a definir reglas de validación condicionales según el tipo de fuente, manejar archivos y almacenarlos correctamente en el servidor.


1. Configurar la Validación en el Request

1.1. Definir las Reglas de Validación

app/Http/Requests/KnowledgeSourceRequest.php

php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class KnowledgeSourceRequest extends FormRequest
{
    /**
     * Determina si el usuario está autorizado para hacer esta petición.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Obtiene las reglas de validación que se aplican a la petición.
     */
    public function rules(): array
    {
        return [
            // Campo nombre: obligatorio, string, máximo 255 caracteres
            'name' => 'required|string|max:255',
            
            // Campo tipo: obligatorio, string, debe ser 'pdf' o 'website'
            'type' => ['required', 'string', Rule::in(['pdf', 'website'])],
            
            // Campo file (PDF): requerido solo si type es 'pdf'
            'file' => 'required_if:type,pdf|file|mimes:pdf|max:10240',
            
            // Campo url (Website): requerido solo si type es 'website'
            'url' => 'required_if:type,website|url|max:2048',
        ];
    }

    /**
     * Obtiene los mensajes de error personalizados.
     */
    public function messages(): array
    {
        return [
            'name.required' => 'El nombre de la fuente es obligatorio',
            'name.string' => 'El nombre debe ser texto',
            'name.max' => 'El nombre no puede tener más de 255 caracteres',
            
            'type.required' => 'El tipo de fuente es obligatorio',
            'type.in' => 'El tipo de fuente seleccionado no es válido',
            
            'file.required_if' => 'Debes seleccionar un archivo PDF',
            'file.file' => 'El campo debe ser un archivo válido',
            'file.mimes' => 'El archivo debe ser un PDF',
            'file.max' => 'El PDF no puede ser mayor a 10MB',
            
            'url.required_if' => 'La URL del website es obligatoria',
            'url.url' => 'La URL debe ser válida',
            'url.max' => 'La URL no puede tener más de 2048 caracteres',
        ];
    }
}

1.2. Explicación de las Reglas

ReglaDescripción
required|string|max:255Obligatorio, texto, máximo 255 caracteres
required|in:pdf,websiteObligatorio, debe ser 'pdf' o 'website'
required_if:type,pdfObligatorio solo si type es 'pdf'
file|mimes:pdf|max:10240Debe ser un archivo PDF de máximo 10MB
required_if:type,websiteObligatorio solo si type es 'website'
url|max:2048Debe ser una URL válida de máximo 2048 caracteres

2. Procesar el Formulario en el Controlador

2.1. Método Store Actualizado

app/Http/Controllers/KnowledgeSourceController.php

php
<?php

namespace App\Http\Controllers;

use App\Models\Chatbot;
use App\Models\KnowledgeSource;
use App\Http\Requests\KnowledgeSourceRequest;
use Illuminate\Support\Facades\Storage;

class KnowledgeSourceController extends Controller
{
    /**
     * Almacena una nueva fuente de conocimiento.
     */
    public function store(KnowledgeSourceRequest $request, Chatbot $chatbot)
    {
        // 1. Autorizar la acción
        $this->authorize('update', $chatbot);

        // 2. Obtener los datos validados
        $validated = $request->validated();

        // 3. Crear una nueva instancia de KnowledgeSource
        $knowledgeSource = new KnowledgeSource();
        $knowledgeSource->name = $validated['name'];
        $knowledgeSource->type = $validated['type'];
        $knowledgeSource->status = 'pending';

        // 4. Procesar según el tipo
        if ($validated['type'] === 'pdf') {
            // Guardar el archivo PDF
            $file = $request->file('file');
            $path = $file->store("knowledge-sources/{$chatbot->id}/pdfs", 'public');
            $knowledgeSource->path = $path;
        }

        if ($validated['type'] === 'website') {
            // Guardar la URL del website
            $knowledgeSource->path = $validated['url'];
        }

        // 5. Asociar con el chatbot y guardar
        $chatbot->knowledgeSources()->save($knowledgeSource);

        // 6. Disparar job para procesar la fuente (opcional)
        // dispatch(new ProcessKnowledgeSource($knowledgeSource));

        // 7. Redireccionar con mensaje de éxito
        return redirect()
            ->route('chatbots.knowledge-sources.index', $chatbot)
            ->with('success', 'Fuente de conocimiento creada exitosamente.');
    }
}

2.2. Explicación del Código

  1. Autorización: Verificamos que el usuario tenga permisos para actualizar el chatbot

  2. Datos Validados: Obtenemos los datos ya validados del request

  3. Creación: Instanciamos un nuevo KnowledgeSource

  4. Procesamiento por Tipo:

    • PDF: Guardamos el archivo en storage y asignamos la ruta

    • Website: Guardamos la URL directamente

  5. Asociación: Guardamos la fuente asociada al chatbot

  6. Job (opcional): Disparar un job para procesamiento en background

  7. Redirección: Volvemos al listado con mensaje de éxito


3. Manejo de Errores en el Frontend

3.1. Mostrar Errores en el Modal

resources/js/Components/KnowledgeSources/KnowledgeSourceCreateModal.vue

vue
<template>
    <DialogModal :show="show" @close="handleClose">
        <template #title>
            <div class="flex items-center">
                <BookIcon class="w-6 h-6 mr-2 text-indigo-600" />
                <span>Agregar Fuente de Conocimiento</span>
            </div>
        </template>

        <template #content>
            <div class="space-y-4">
                <!-- Mostrar errores generales -->
                <div v-if="form.hasErrors" class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
                    <div class="flex">
                        <div class="flex-shrink-0">
                            <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
                            </svg>
                        </div>
                        <div class="ml-3">
                            <h3 class="text-sm font-medium text-red-800 dark:text-red-200">
                                Por favor corrige los siguientes errores:
                            </h3>
                            <div class="mt-2 text-sm text-red-700 dark:text-red-300">
                                <ul class="list-disc pl-5 space-y-1">
                                    <li v-for="error in allErrors" :key="error">
                                        {{ error }}
                                    </li>
                                </ul>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Formulario -->
                <KnowledgeSourceForm :form="form" />
            </div>
        </template>

        <template #footer>
            <div class="flex justify-between w-full">
                <SecondaryButton @click="handleClose">
                    Cerrar
                </SecondaryButton>
                <PrimaryButton 
                    @click="handleSubmit"
                    :disabled="form.processing"
                >
                    <span v-if="form.processing" class="flex items-center">
                        <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                        </svg>
                        Guardando...
                    </span>
                    <span v-else>Guardar</span>
                </PrimaryButton>
            </div>
        </template>
    </DialogModal>
</template>

<script setup>
import DialogModal from '@/Components/DialogModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import BookIcon from '@/Components/Icons/BookIcon.vue';
import KnowledgeSourceForm from './KnowledgeSourceForm.vue';
import { useForm } from '@inertiajs/vue3';
import { watch, computed } from 'vue';

const props = defineProps({
    show: {
        type: Boolean,
        default: false,
    },
    chatbotId: {
        type: String,
        required: true,
    },
});

const emit = defineEmits(['close']);

// Estado del formulario
const form = useForm({
    name: '',
    type: 'pdf',
    pdf_file: null,
    website_url: '',
});

// Obtener todos los errores como array
const allErrors = computed(() => {
    if (!form.errors) return [];
    return Object.values(form.errors).flat();
});

// Manejar el envío
const handleSubmit = () => {
    const data = new FormData();
    data.append('name', form.name);
    data.append('type', form.type);

    if (form.type === 'pdf' && form.pdf_file) {
        data.append('file', form.pdf_file);
    }

    if (form.type === 'website' && form.website_url) {
        data.append('url', form.website_url);
    }

    form.post(route('chatbots.knowledge-sources.store', props.chatbotId), {
        data: data,
        forceFormData: true,
        preserveScroll: true,
        onSuccess: () => {
            handleClose();
            form.reset();
        },
        onError: (errors) => {
            console.error('Errores de validación:', errors);
        },
    });
};

const handleClose = () => {
    emit('close');
};

watch(
    () => props.show,
    (newValue) => {
        if (!newValue) {
            form.reset();
        }
    }
);
</script>

4. Validación en el Frontend (Opcional)

4.1. Validación Básica antes del Envío

javascript
// En el componente KnowledgeSourceForm.vue
const validateForm = () => {
    const errors = {};
    
    if (!form.name || form.name.trim() === '') {
        errors.name = 'El nombre es obligatorio';
    }
    
    if (form.type === 'pdf' && !form.pdf_file) {
        errors.pdf_file = 'Debes seleccionar un archivo PDF';
    }
    
    if (form.type === 'website' && !form.website_url) {
        errors.website_url = 'La URL es obligatoria';
    }
    
    if (form.type === 'website' && form.website_url) {
        try {
            new URL(form.website_url);
        } catch {
            errors.website_url = 'La URL no es válida';
        }
    }
    
    return errors;
};

// En el modal
const handleSubmit = () => {
    // Validación en frontend
    const errors = validateForm();
    if (Object.keys(errors).length > 0) {
        form.errors = errors;
        return;
    }
    
    // Si pasa la validación, enviar
    // ... código de envío
};

5. Visualización de Errores en el Formulario

5.1. Componente InputError

resources/js/Components/InputError.vue

vue
<template>
    <div v-if="message" class="mt-2 text-sm text-red-600 dark:text-red-400">
        {{ message }}
    </div>
</template>

<script setup>
defineProps({
    message: {
        type: String,
        default: null,
    },
});
</script>

5.2. Campos con Errores

vue
<template>
    <div class="col-span-6 sm:col-span-4">
        <InputLabel for="name" value="Nombre" />
        <TextInput
            id="name"
            v-model="form.name"
            type="text"
            class="mt-1 block w-full"
            :class="{ 'border-red-500 focus:border-red-500 focus:ring-red-500': form.errors.name }"
            placeholder="Ej: Manual de Usuario"
        />
        <InputError :message="form.errors.name" class="mt-2" />
    </div>
</template>

6. Verificación en la Base de Datos

6.1. Estructura de la Tabla

sql
knowledge_sources (
    id UUID PRIMARY KEY,
    chatbot_id UUID FOREIGN KEY,
    name VARCHAR(255) NOT NULL,
    type ENUM('pdf', 'website') NOT NULL,
    path VARCHAR(2048) NOT NULL,
    status ENUM('pending', 'processing', 'processed', 'failed') DEFAULT 'pending',
    extracted_content LONGTEXT NULL,
    metadata JSON NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);

6.2. Verificación de Datos Guardados

php
// En el controlador, después de guardar
dd($knowledgeSource->toArray());

// Resultado esperado:
[
    'id' => 'uuid...',
    'chatbot_id' => 'uuid...',
    'name' => 'Mi Fuente',
    'type' => 'pdf',
    'path' => 'knowledge-sources/uuid/pdfs/archivo.pdf',
    'status' => 'pending',
    'extracted_content' => null,
    'metadata' => null,
    'created_at' => '2024-01-01 12:00:00',
    'updated_at' => '2024-01-01 12:00:00',
]

7. Mejores Prácticas de Validación

7.1. Validación en el Backend

Siempre validar en el servidor: La validación del lado del servidor es imprescindible para la seguridad
Usar FormRequest: Centraliza las reglas de validación en clases dedicadas
Mensajes personalizados: Proporciona mensajes claros y útiles
Validación condicional: Usa required_if, required_with, etc.
Sanitización: Los datos se limpian automáticamente con los castings del modelo

7.2. Validación en el Frontend

Validación en tiempo real: Mejora la experiencia del usuario
Feedback inmediato: Muestra errores mientras el usuario escribe
Estilos visuales: Resalta los campos con errores
Prevención de envíos: Bloquea el envío hasta que el formulario sea válido


8. Resumen del Flujo de Validación

8.1. Flujo Completo

  1. Usuario completa el formulario

  2. Frontend valida los campos (opcional)

  3. Usuario hace clic en "Guardar"

  4. Datos se envían al servidor vía Inertia

  5. Backend valida con KnowledgeSourceRequest

  6. Si hay errores: Se devuelven al frontend

  7. Frontend muestra los errores en el formulario

  8. Usuario corrige los errores

  9. Se repite el proceso hasta que sea válido

  10. Datos válidos: Se guardan en la base de datos

  11. Redirección con mensaje de éxito

8.2. Posibles Errores y Soluciones

ErrorCausaSolución
"El campo file es obligatorio"No se subió el PDFAsegurar que el archivo se seleccione correctamente
"El archivo debe ser un PDF"Se subió otro tipo de archivoVerificar la extensión del archivo
"La URL debe ser válida"URL mal formadaUsar un validador de URL adecuado
"El nombre ya existe"Nombre duplicadoAgregar validación de unicidad

9. Código Completo del Request

Aquí tienes el código completo para copiar:

app/Http/Requests/KnowledgeSourceRequest.php

php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class KnowledgeSourceRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'type' => ['required', 'string', Rule::in(['pdf', 'website'])],
            'file' => 'required_if:type,pdf|file|mimes:pdf|max:10240',
            'url' => 'required_if:type,website|url|max:2048',
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => 'El nombre de la fuente es obligatorio',
            'name.string' => 'El nombre debe ser texto',
            'name.max' => 'El nombre no puede tener más de 255 caracteres',
            'type.required' => 'El tipo de fuente es obligatorio',
            'type.in' => 'El tipo de fuente seleccionado no es válido',
            'file.required_if' => 'Debes seleccionar un archivo PDF',
            'file.file' => 'El campo debe ser un archivo válido',
            'file.mimes' => 'El archivo debe ser un PDF',
            'file.max' => 'El PDF no puede ser mayor a 10MB',
            'url.required_if' => 'La URL del website es obligatoria',
            'url.url' => 'La URL debe ser válida',
            'url.max' => 'La URL no puede tener más de 2048 caracteres',
        ];
    }
}

10. Resumen

En este tutorial hemos implementado la validación completa para las fuentes de conocimiento:

Reglas de validación con condiciones según el tipo
Mensajes personalizados para cada campo
Procesamiento de archivos y URLs en el controlador
Manejo de errores en el frontend
Visualización de errores en el formulario
Almacenamiento seguro de archivos
Mejores prácticas de validación

¡Excelente trabajo! Ahora tienes un sistema robusto de validación para tus fuentes de conocimiento.

Comentarios

Entradas más populares de este blog

1. Introducción al curso de Chatbots personalizados con Laravel & OpenAI

2. Prerequisitos para el Curso de Chatbots con Laravel y OpenAI

4. Herramientas de Calidad