25. Formulario para crear fuentes de conocimientos

 

Tutorial 25: Formulario para Crear Fuentes de Conocimiento - Parte 2

En este tutorial, continuaremos con la implementación del formulario para crear fuentes de conocimiento, agregando los botones de acción, configurando el envío del formulario y estableciendo las rutas necesarias.


1. Agregar los Botones del Modal

1.1. Estructura de los Botones

resources/js/Components/KnowledgeSources/KnowledgeSourceCreateModal.vue

vue
<template>
    <DialogModal :show="show" @close="handleClose">
        <!-- Título -->
        <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>

        <!-- Contenido -->
        <template #content>
            <!-- ... contenido del formulario ... -->
        </template>

        <!-- Footer con botones -->
        <template #footer>
            <div class="flex justify-between w-full">
                <!-- Botón Cerrar (lado izquierdo) -->
                <SecondaryButton @click="handleClose">
                    Cerrar
                </SecondaryButton>

                <!-- Botón Guardar (lado derecho) -->
                <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 { useForm } from '@inertiajs/vue3';
import { watch } from 'vue';

// Props
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',
    description: '',
    tags: '',
    // PDF
    pdf_file: null,
    // Website
    website_url: '',
    // Texto
    content: '',
    // CSV
    csv_file: null,
    csv_has_header: true,
    csv_delimiter: ',',
    // Excel
    excel_file: null,
    excel_has_header: true,
    excel_sheet: '0',
    // JSON
    json_file: null,
    json_is_array: true,
});

// Manejar el envío del formulario
const handleSubmit = () => {
    console.log('Datos del formulario:', form.data());
    
    // Aquí se enviará el formulario
    // form.post(route('chatbots.knowledge-sources.store', props.chatbotId), {
    //     preserveScroll: true,
    //     onSuccess: () => {
    //         handleClose();
    //     },
    // });
};

// Manejar el cierre del modal
const handleClose = () => {
    emit('close');
};

// Resetear el formulario cuando se cierra el modal
watch(
    () => props.show,
    (newValue) => {
        if (!newValue) {
            form.reset();
        }
    }
);
</script>

2. Configurar las Rutas

2.1. Definir las Rutas Anidadas

routes/web.php

php
<?php

use App\Http\Controllers\ChatbotController;
use App\Http\Controllers\KnowledgeSourceController;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth'])->group(function () {
    // Rutas principales de chatbots
    Route::resource('chatbots', ChatbotController::class);
    
    // Rutas anidadas para fuentes de conocimiento
    Route::prefix('chatbots/{chatbot}')->group(function () {
        Route::get('/knowledge-sources', [KnowledgeSourceController::class, 'index'])
            ->name('chatbots.knowledge-sources.index');
        Route::get('/knowledge-sources/create', [KnowledgeSourceController::class, 'create'])
            ->name('chatbots.knowledge-sources.create');
        Route::post('/knowledge-sources', [KnowledgeSourceController::class, 'store'])
            ->name('chatbots.knowledge-sources.store');
        Route::get('/knowledge-sources/{knowledgeSource}', [KnowledgeSourceController::class, 'show'])
            ->name('chatbots.knowledge-sources.show');
        Route::get('/knowledge-sources/{knowledgeSource}/edit', [KnowledgeSourceController::class, 'edit'])
            ->name('chatbots.knowledge-sources.edit');
        Route::put('/knowledge-sources/{knowledgeSource}', [KnowledgeSourceController::class, 'update'])
            ->name('chatbots.knowledge-sources.update');
        Route::delete('/knowledge-sources/{knowledgeSource}', [KnowledgeSourceController::class, 'destroy'])
            ->name('chatbots.knowledge-sources.destroy');
        Route::post('/knowledge-sources/{knowledgeSource}/reprocess', [KnowledgeSourceController::class, 'reprocess'])
            ->name('chatbots.knowledge-sources.reprocess');
    });
});

2.2. Verificar las Rutas

bash
php artisan route:list --name=knowledge

Resultado esperado:

text
GET|HEAD  chatbots/{chatbot}/knowledge-sources ....................... chatbots.knowledge-sources.index
GET|HEAD  chatbots/{chatbot}/knowledge-sources/create ................ chatbots.knowledge-sources.create
POST      chatbots/{chatbot}/knowledge-sources ........................ chatbots.knowledge-sources.store
GET|HEAD  chatbots/{chatbot}/knowledge-sources/{knowledgeSource} ..... chatbots.knowledge-sources.show
GET|HEAD  chatbots/{chatbot}/knowledge-sources/{knowledgeSource}/edit  chatbots.knowledge-sources.edit
PUT|PATCH chatbots/{chatbot}/knowledge-sources/{knowledgeSource} ..... chatbots.knowledge-sources.update
DELETE    chatbots/{chatbot}/knowledge-sources/{knowledgeSource} ..... chatbots.knowledge-sources.destroy
POST      chatbots/{chatbot}/knowledge-sources/{knowledgeSource}/reprocess  chatbots.knowledge-sources.reprocess

3. Configurar el Controlador

3.1. Actualizar el Controlador

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\Http\Request;
use Illuminate\Support\Facades\Storage;
use Inertia\Inertia;

class KnowledgeSourceController extends Controller
{
    /**
     * Muestra la lista de fuentes de conocimiento.
     */
    public function index(Chatbot $chatbot)
    {
        $this->authorize('view', $chatbot);

        $knowledgeSources = $chatbot->knowledgeSources()
            ->orderBy('created_at', 'desc')
            ->paginate(10);

        return Inertia::render('KnowledgeSources/Index', [
            'chatbot' => $chatbot,
            'knowledgeSources' => $knowledgeSources,
        ]);
    }

    /**
     * Muestra el formulario para crear una nueva fuente.
     */
    public function create(Chatbot $chatbot)
    {
        $this->authorize('update', $chatbot);

        return Inertia::render('KnowledgeSources/Create', [
            'chatbot' => $chatbot,
        ]);
    }

    /**
     * Almacena una nueva fuente de conocimiento.
     */
    public function store(KnowledgeSourceRequest $request, Chatbot $chatbot)
    {
        $this->authorize('update', $chatbot);

        $validated = $request->validated();

        // Procesar archivo si existe
        $filePath = null;
        if ($request->hasFile('file')) {
            $filePath = $this->uploadFile($request->file('file'), $chatbot);
        }

        // Preparar datos para la base de datos
        $data = [
            'name' => $validated['name'],
            'type' => $validated['type'],
            'description' => $validated['description'] ?? null,
            'tags' => $validated['tags'] ?? null,
            'path' => $filePath,
            'status' => 'pending',
            'metadata' => $this->prepareMetadata($validated),
        ];

        // Para tipo texto, el contenido ya está disponible
        if ($validated['type'] === 'text') {
            $data['extracted_content'] = $validated['content'];
            $data['status'] = 'processed';
        }

        // Para website, guardamos la URL
        if ($validated['type'] === 'website') {
            $data['path'] = $validated['url'];
        }

        // Crear la fuente de conocimiento
        $knowledgeSource = $chatbot->knowledgeSources()->create($data);

        // Si no es texto, disparar job de procesamiento
        if ($validated['type'] !== 'text') {
            // dispatch(new ProcessKnowledgeSource($knowledgeSource));
        }

        return redirect()
            ->route('chatbots.knowledge-sources.index', $chatbot)
            ->with('success', 'Fuente de conocimiento creada exitosamente.');
    }

    /**
     * Muestra una fuente de conocimiento específica.
     */
    public function show(Chatbot $chatbot, KnowledgeSource $knowledgeSource)
    {
        $this->authorize('view', $chatbot);

        return Inertia::render('KnowledgeSources/Show', [
            'chatbot' => $chatbot,
            'knowledgeSource' => $knowledgeSource,
        ]);
    }

    /**
     * Muestra el formulario para editar una fuente.
     */
    public function edit(Chatbot $chatbot, KnowledgeSource $knowledgeSource)
    {
        $this->authorize('update', $chatbot);

        return Inertia::render('KnowledgeSources/Edit', [
            'chatbot' => $chatbot,
            'knowledgeSource' => $knowledgeSource,
        ]);
    }

    /**
     * Actualiza una fuente de conocimiento.
     */
    public function update(KnowledgeSourceRequest $request, Chatbot $chatbot, KnowledgeSource $knowledgeSource)
    {
        $this->authorize('update', $chatbot);

        $validated = $request->validated();

        $knowledgeSource->update([
            'name' => $validated['name'],
            'description' => $validated['description'] ?? $knowledgeSource->description,
            'tags' => $validated['tags'] ?? $knowledgeSource->tags,
        ]);

        return redirect()
            ->route('chatbots.knowledge-sources.index', $chatbot)
            ->with('success', 'Fuente de conocimiento actualizada exitosamente.');
    }

    /**
     * Elimina una fuente de conocimiento.
     */
    public function destroy(Chatbot $chatbot, KnowledgeSource $knowledgeSource)
    {
        $this->authorize('delete', $chatbot);

        // Eliminar archivo si existe
        if ($knowledgeSource->path && Storage::exists($knowledgeSource->path)) {
            Storage::delete($knowledgeSource->path);
        }

        $knowledgeSource->delete();

        return redirect()
            ->route('chatbots.knowledge-sources.index', $chatbot)
            ->with('success', 'Fuente de conocimiento eliminada exitosamente.');
    }

    /**
     * Reprocesa una fuente de conocimiento.
     */
    public function reprocess(Chatbot $chatbot, KnowledgeSource $knowledgeSource)
    {
        $this->authorize('update', $chatbot);

        $knowledgeSource->update([
            'status' => 'pending',
            'extracted_content' => null,
        ]);

        // dispatch(new ProcessKnowledgeSource($knowledgeSource));

        return back()->with('success', 'La fuente de conocimiento está siendo reprocesada.');
    }

    /**
     * Sube un archivo al almacenamiento.
     */
    private function uploadFile($file, Chatbot $chatbot): string
    {
        $path = $file->store("knowledge-sources/{$chatbot->id}", 'public');
        return $path;
    }

    /**
     * Prepara los metadatos según el tipo de fuente.
     */
    private function prepareMetadata(array $data): array
    {
        $metadata = [];

        switch ($data['type']) {
            case 'pdf':
                // No hay metadatos adicionales por ahora
                break;
            case 'website':
                $metadata['url'] = $data['url'];
                break;
            case 'csv':
                $metadata['has_header'] = $data['has_header'] ?? true;
                $metadata['delimiter'] = $data['delimiter'] ?? ',';
                break;
            case 'excel':
                $metadata['has_header'] = $data['has_header'] ?? true;
                $metadata['sheet'] = $data['sheet'] ?? 0;
                break;
            case 'json':
                $metadata['is_array'] = $data['is_array'] ?? true;
                break;
            case 'text':
                $metadata['char_count'] = strlen($data['content'] ?? '');
                $metadata['word_count'] = str_word_count($data['content'] ?? '');
                break;
        }

        return $metadata;
    }
}

4. Actualizar el Request 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
{
    public function authorize(): bool
    {
        return true; // Cambiar a true para permitir la autorización
    }

    public function rules(): array
    {
        $rules = [
            'name' => 'required|string|max:255',
            'type' => ['required', Rule::in(['pdf', 'website', 'text', 'csv', 'excel', 'json'])],
            'description' => 'nullable|string|max:1000',
            'tags' => 'nullable|string|max:255',
        ];

        // Reglas según el tipo
        switch ($this->input('type')) {
            case 'pdf':
                $rules['file'] = 'required|file|mimes:pdf|max:10240';
                break;
            case 'website':
                $rules['url'] = 'required|url|max:2048';
                break;
            case 'text':
                $rules['content'] = 'required|string|min:10|max:50000';
                break;
            case 'csv':
                $rules['file'] = 'required|file|mimes:csv,txt|max:5120';
                $rules['has_header'] = 'sometimes|boolean';
                $rules['delimiter'] = ['sometimes', 'string', Rule::in([',', ';', "\t"])];
                break;
            case 'excel':
                $rules['file'] = 'required|file|mimes:xlsx,xls|max:5120';
                $rules['has_header'] = 'sometimes|boolean';
                $rules['sheet'] = 'sometimes|integer|min:0';
                break;
            case 'json':
                $rules['file'] = 'required|file|mimes:json|max:5120';
                $rules['is_array'] = 'sometimes|boolean';
                break;
        }

        return $rules;
    }

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

5. Implementar el Envío del Formulario

5.1. Actualizar el Modal con Envío Funcional

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">
                <p class="text-sm text-gray-600 dark:text-gray-400">
                    Agrega una nueva fuente de conocimiento para tu chatbot. 
                    El contenido será procesado para mejorar las respuestas del chatbot.
                </p>

                <!-- Formulario reutilizable -->
                <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 } 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',
    description: '',
    tags: '',
    // PDF
    pdf_file: null,
    // Website
    website_url: '',
    // Texto
    content: '',
    // CSV
    csv_file: null,
    csv_has_header: true,
    csv_delimiter: ',',
    // Excel
    excel_file: null,
    excel_has_header: true,
    excel_sheet: '0',
    // JSON
    json_file: null,
    json_is_array: true,
});

// Manejar el envío del formulario
const handleSubmit = () => {
    // Crear FormData para enviar archivos
    const data = new FormData();
    
    // Campos comunes
    data.append('name', form.name);
    data.append('type', form.type);
    data.append('description', form.description);
    data.append('tags', form.tags);

    // Campos según el tipo
    switch (form.type) {
        case 'pdf':
            if (form.pdf_file) {
                data.append('file', form.pdf_file);
            }
            break;
        case 'website':
            data.append('url', form.website_url);
            break;
        case 'text':
            data.append('content', form.content);
            break;
        case 'csv':
            if (form.csv_file) {
                data.append('file', form.csv_file);
                data.append('has_header', form.csv_has_header ? '1' : '0');
                data.append('delimiter', form.csv_delimiter);
            }
            break;
        case 'excel':
            if (form.excel_file) {
                data.append('file', form.excel_file);
                data.append('has_header', form.excel_has_header ? '1' : '0');
                data.append('sheet', form.excel_sheet);
            }
            break;
        case 'json':
            if (form.json_file) {
                data.append('file', form.json_file);
                data.append('is_array', form.json_is_array ? '1' : '0');
            }
            break;
    }

    // Enviar el formulario
    form.post(route('chatbots.knowledge-sources.store', props.chatbotId), {
        data: data,
        forceFormData: true,
        preserveScroll: true,
        onSuccess: () => {
            // Cerrar el modal en caso de éxito
            handleClose();
            form.reset();
        },
        onError: (errors) => {
            console.error('Errores de validación:', errors);
            // Mostrar mensaje de error
            if (Object.keys(errors).length > 0) {
                const firstError = Object.values(errors)[0];
                alert(firstError);
            }
        },
    });
};

// Manejar el cierre del modal
const handleClose = () => {
    emit('close');
};

// Resetear el formulario cuando se cierra el modal
watch(
    () => props.show,
    (newValue) => {
        if (!newValue) {
            form.reset();
        }
    }
);
</script>

6. Verificar la Autorización

6.1. Crear Política de Autorización

app/Policies/KnowledgeSourcePolicy.php

php
<?php

namespace App\Policies;

use App\Models\User;
use App\Models\KnowledgeSource;

class KnowledgeSourcePolicy
{
    public function view(User $user, KnowledgeSource $knowledgeSource): bool
    {
        return $user->id === $knowledgeSource->chatbot->user_id;
    }

    public function create(User $user, $chatbotId): bool
    {
        // Verificar que el usuario sea dueño del chatbot
        return $user->chatbots()->where('id', $chatbotId)->exists();
    }

    public function update(User $user, KnowledgeSource $knowledgeSource): bool
    {
        return $user->id === $knowledgeSource->chatbot->user_id;
    }

    public function delete(User $user, KnowledgeSource $knowledgeSource): bool
    {
        return $user->id === $knowledgeSource->chatbot->user_id;
    }
}

6.2. Registrar la Política

app/Providers/AuthServiceProvider.php

php
<?php

namespace App\Providers;

use App\Models\KnowledgeSource;
use App\Policies\KnowledgeSourcePolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        KnowledgeSource::class => KnowledgeSourcePolicy::class,
    ];

    public function boot(): void
    {
        $this->registerPolicies();
    }
}

7. Resumen del Flujo Complet

7.1. Flujo de Usuario

  1. Usuario navega a la página de detalles del chatbot

  2. Usuario hace clic en "Gestionar Conocimiento"

  3. Modal se abre con el formulario

  4. Usuario selecciona el tipo de fuente

  5. Usuario completa los campos según el tipo

  6. Usuario hace clic en "Guardar"

  7. Formulario se envía mediante Inertia

  8. Validación en el servidor

  9. Archivo se procesa y almacena

  10. Modal se cierra automáticamente

  11. Mensaje de éxito se muestra al usuario

7.2. Estructura de Datos

text
KnowledgeSource
├── id (UUID)
├── chatbot_id (UUID)
├── name (string)
├── type (enum: pdf, website, text, csv, excel, json)
├── description (string, nullable)
├── tags (string, nullable)
├── path (string, nullable) - Ruta del archivo o URL
├── extracted_content (longText, nullable)
├── status (enum: pending, processing, processed, failed)
├── metadata (json, nullable)
├── created_at (timestamp)
└── updated_at (timestamp)

8. Próximos Pasos

  1. Procesamiento en Background: Implementar Jobs para procesar archivos y URLs

  2. Extracción de Contenido: Implementar la lógica de extracción para cada tipo

  3. Embeddings: Generar embeddings del contenido para búsqueda semántica

  4. Interfaz de Usuario: Mostrar el progreso del procesamiento

  5. Edición: Implementar la edición de fuentes existentes

  6. Búsqueda: Buscar dentro del contenido de las fuentes


9. Resumen

En este tutorial hemos completado la implementación del formulario para crear fuentes de conocimiento:

✅ Botones de acción con diseño consistente
✅ Envío del formulario con Inertia
✅ Configuración de rutas anidadas
✅ Controlador con todas las operaciones CRUD
✅ Validación completa del lado del servidor
✅ Manejo de archivos y FormData
✅ Cierre automático del modal tras éxito
✅ Políticas de autorización

¡Excelente trabajo! Ahora tienes un sistema completo para gestionar fuentes de conocimiento para tus chatbots

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