22. Fuentes de conocimiento para los chatbots
22: Fuentes de Conocimiento para los Chatbots
En este tutorial, implementaremos el sistema de fuentes de conocimiento para los chatbots. Estas fuentes permitirán que los chatbots tengan acceso a información específica de PDFs o websites, mejorando así su capacidad de respuesta y precisión.
1. Generar el Modelo y la Migración
Primero, crearemos el modelo KnowledgeSource con todos los archivos necesarios (excepto el seeder).
1.1. Comando Artisan
php artisan make:model KnowledgeSource -m -c -r -f
Opciones:
-m: Crea la migración-c: Crea el controlador-r: Crea el controlador con recursos RESTful-f: Crea el factory
1.2. Configurar el Modelo
app/Models/KnowledgeSource.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class KnowledgeSource extends Model { use HasFactory, HasUuids; /** * La llave primaria es de tipo UUID */ protected $keyType = 'string'; public $incrementing = false; /** * Los atributos que son asignables masivamente. * * @var array<int, string> */ protected $fillable = [ 'chatbot_id', 'name', 'type', 'path', 'extracted_content', 'status', 'metadata', ]; /** * Los atributos que deben ser casteados. * * @var array<string, string> */ protected $casts = [ 'metadata' => 'array', 'extracted_content' => 'string', ]; /** * Obtiene el chatbot al que pertenece esta fuente de conocimiento. */ public function chatbot(): BelongsTo { return $this->belongsTo(Chatbot::class); } /** * Verifica si la fuente está procesada. */ public function isProcessed(): bool { return $this->status === 'processed'; } /** * Verifica si la fuente está en proceso. */ public function isProcessing(): bool { return $this->status === 'processing'; } /** * Verifica si la fuente está pendiente. */ public function isPending(): bool { return $this->status === 'pending'; } /** * Verifica si la fuente ha fallado. */ public function hasFailed(): bool { return $this->status === 'failed'; } }
1.3. Agregar Relación en el Modelo Chatbot
app/Models/Chatbot.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Chatbot extends Model { use HasFactory, HasUuids; protected $keyType = 'string'; public $incrementing = false; protected $fillable = [ 'name', 'description', 'model', 'system_prompt', 'user_id', ]; protected $casts = [ 'config' => 'array', ]; /** * Obtiene el usuario propietario del chatbot. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Obtiene todas las fuentes de conocimiento del chatbot. */ public function knowledgeSources(): HasMany { return $this->hasMany(KnowledgeSource::class); } /** * Obtiene solo las fuentes de conocimiento procesadas. */ public function processedKnowledgeSources(): HasMany { return $this->knowledgeSources()->where('status', 'processed'); } /** * Obtiene solo las fuentes de conocimiento pendientes. */ public function pendingKnowledgeSources(): HasMany { return $this->knowledgeSources()->where('status', 'pending'); } /** * Obtiene solo las fuentes de conocimiento que han fallado. */ public function failedKnowledgeSources(): HasMany { return $this->knowledgeSources()->where('status', 'failed'); } }
2. Configurar la Migración
database/migrations/xxxx_xx_xx_create_knowledge_sources_table.php
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('knowledge_sources', function (Blueprint $table) { // ID como UUID $table->uuid('id')->primary(); // Relación con el chatbot $table->uuid('chatbot_id'); $table->foreign('chatbot_id') ->references('id') ->on('chatbots') ->onDelete('cascade'); // Campos principales $table->string('name')->comment('Nombre descriptivo de la fuente'); $table->enum('type', ['pdf', 'website', 'text', 'csv', 'excel', 'json']) ->default('text') ->comment('Tipo de fuente de conocimiento'); // Ruta o URL del archivo $table->string('path')->nullable()->comment('URL o ruta del archivo'); // Contenido extraído (texto plano) $table->longText('extracted_content')->nullable() ->comment('Contenido extraído y procesado en texto plano'); // Estado del procesamiento $table->enum('status', ['pending', 'processing', 'processed', 'failed']) ->default('pending') ->comment('Estado del procesamiento de la fuente'); // Metadatos adicionales (tamaño, páginas, etc.) $table->json('metadata')->nullable() ->comment('Metadatos adicionales de la fuente'); // Timestamps $table->timestamps(); // Índices para mejor rendimiento $table->index(['chatbot_id', 'status']); $table->index('type'); }); } public function down(): void { Schema::dropIfExists('knowledge_sources'); } };
3. Configurar el Factory
database/factories/KnowledgeSourceFactory.php
<?php namespace Database\Factories; use App\Models\Chatbot; use Illuminate\Database\Eloquent\Factories\Factory; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\KnowledgeSource> */ class KnowledgeSourceFactory extends Factory { /** * Define el estado por defecto del modelo. */ public function definition(): array { $types = ['pdf', 'website', 'text', 'csv', 'excel', 'json']; $type = $this->faker->randomElement($types); return [ 'id' => $this->faker->uuid(), 'chatbot_id' => Chatbot::factory(), 'name' => $this->faker->sentence(3), 'type' => $type, 'path' => $this->getPathByType($type), 'extracted_content' => $this->faker->paragraphs(3, true), 'status' => $this->faker->randomElement(['pending', 'processing', 'processed', 'failed']), 'metadata' => $this->getMetadataByType($type), 'created_at' => now(), 'updated_at' => now(), ]; } /** * Obtiene la ruta según el tipo de fuente. */ private function getPathByType(string $type): ?string { return match ($type) { 'pdf' => $this->faker->url() . '/document.pdf', 'website' => $this->faker->url(), 'text' => null, 'csv' => $this->faker->url() . '/data.csv', 'excel' => $this->faker->url() . '/spreadsheet.xlsx', 'json' => $this->faker->url() . '/data.json', default => null, }; } /** * Obtiene los metadatos según el tipo de fuente. */ private function getMetadataByType(string $type): array { $metadata = [ 'size' => $this->faker->numberBetween(1000, 10000000), ]; return match ($type) { 'pdf' => array_merge($metadata, [ 'pages' => $this->faker->numberBetween(1, 100), 'author' => $this->faker->name(), 'title' => $this->faker->sentence(), ]), 'website' => array_merge($metadata, [ 'title' => $this->faker->sentence(), 'description' => $this->faker->paragraph(), 'last_scraped' => $this->faker->dateTime()->format('Y-m-d H:i:s'), ]), 'csv' => array_merge($metadata, [ 'rows' => $this->faker->numberBetween(10, 10000), 'columns' => $this->faker->numberBetween(3, 20), 'delimiter' => $this->faker->randomElement([',', ';', '\t']), ]), 'excel' => array_merge($metadata, [ 'sheets' => $this->faker->numberBetween(1, 5), 'rows' => $this->faker->numberBetween(10, 10000), 'columns' => $this->faker->numberBetween(3, 20), ]), 'json' => array_merge($metadata, [ 'records' => $this->faker->numberBetween(10, 1000), 'keys' => $this->faker->words(5), ]), default => $metadata, ]; } /** * Estado: Fuente pendiente. */ public function pending(): static { return $this->state(fn (array $attributes) => [ 'status' => 'pending', 'extracted_content' => null, ]); } /** * Estado: Fuente procesada. */ public function processed(): static { return $this->state(fn (array $attributes) => [ 'status' => 'processed', 'extracted_content' => $this->faker->paragraphs(5, true), ]); } /** * Estado: Fuente con error. */ public function failed(): static { return $this->state(fn (array $attributes) => [ 'status' => 'failed', 'extracted_content' => null, 'metadata' => array_merge($attributes['metadata'] ?? [], [ 'error_message' => $this->faker->sentence(), 'error_code' => $this->faker->numberBetween(100, 500), ]), ]); } /** * Estado: Fuente en proceso. */ public function processing(): static { return $this->state(fn (array $attributes) => [ 'status' => 'processing', 'extracted_content' => null, ]); } /** * Estado: Fuente de tipo PDF. */ public function pdf(): static { return $this->state(fn (array $attributes) => [ 'type' => 'pdf', 'path' => $this->faker->url() . '/document.pdf', 'metadata' => [ 'pages' => $this->faker->numberBetween(1, 100), 'size' => $this->faker->numberBetween(1000, 5000000), ], ]); } /** * Estado: Fuente de tipo Website. */ public function website(): static { return $this->state(fn (array $attributes) => [ 'type' => 'website', 'path' => $this->faker->url(), 'metadata' => [ 'title' => $this->faker->sentence(), 'description' => $this->faker->paragraph(), ], ]); } /** * Estado: Fuente de tipo Texto. */ public function text(): static { return $this->state(fn (array $attributes) => [ 'type' => 'text', 'path' => null, 'extracted_content' => $this->faker->paragraphs(10, true), 'status' => 'processed', ]); } }
4. Crear el Controlador
app/Http/Controllers/KnowledgeSourceController.php
<?php namespace App\Http\Controllers; use App\Models\Chatbot; use App\Models\KnowledgeSource; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use Inertia\Inertia; class KnowledgeSourceController extends Controller { /** * Muestra la lista de fuentes de conocimiento de un chatbot. */ 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 de conocimiento. */ 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(Request $request, Chatbot $chatbot) { $this->authorize('update', $chatbot); $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'type' => 'required|in:pdf,website,text,csv,excel,json', 'path' => 'nullable|url|max:2048', 'content' => 'nullable|string', // Para el tipo 'text' ]); if ($validator->fails()) { return back()->withErrors($validator)->withInput(); } $validated = $validator->validated(); // Crear la fuente de conocimiento $knowledgeSource = $chatbot->knowledgeSources()->create([ 'name' => $validated['name'], 'type' => $validated['type'], 'path' => $validated['path'] ?? null, 'extracted_content' => $validated['content'] ?? null, 'status' => $validated['type'] === 'text' ? 'processed' : 'pending', 'metadata' => [], ]); // Si es de tipo texto, ya está procesado // Si es de otro tipo, disparar proceso de extracción (job) if ($knowledgeSource->isPending()) { // dispatch(new ProcessKnowledgeSource($knowledgeSource)); } return redirect() ->route('knowledge-sources.index', $chatbot) ->with('success', 'Fuente de conocimiento creada exitosamente.'); } /** * Muestra una fuente de conocimiento específica. */ public function show(KnowledgeSource $knowledgeSource) { $this->authorize('view', $knowledgeSource->chatbot); return Inertia::render('KnowledgeSources/Show', [ 'knowledgeSource' => $knowledgeSource, ]); } /** * Muestra el formulario para editar una fuente de conocimiento. */ public function edit(KnowledgeSource $knowledgeSource) { $this->authorize('update', $knowledgeSource->chatbot); return Inertia::render('KnowledgeSources/Edit', [ 'knowledgeSource' => $knowledgeSource, ]); } /** * Actualiza una fuente de conocimiento. */ public function update(Request $request, KnowledgeSource $knowledgeSource) { $this->authorize('update', $knowledgeSource->chatbot); $validator = Validator::make($request->all(), [ 'name' => 'sometimes|string|max:255', 'path' => 'nullable|url|max:2048', ]); if ($validator->fails()) { return back()->withErrors($validator)->withInput(); } $knowledgeSource->update($validator->validated()); return redirect() ->route('knowledge-sources.index', $knowledgeSource->chatbot) ->with('success', 'Fuente de conocimiento actualizada exitosamente.'); } /** * Elimina una fuente de conocimiento. */ public function destroy(KnowledgeSource $knowledgeSource) { $this->authorize('delete', $knowledgeSource->chatbot); // Eliminar archivo físico si existe // Storage::delete($knowledgeSource->path); $knowledgeSource->delete(); return redirect() ->route('knowledge-sources.index', $knowledgeSource->chatbot) ->with('success', 'Fuente de conocimiento eliminada exitosamente.'); } /** * Reprocesa una fuente de conocimiento. */ public function reprocess(KnowledgeSource $knowledgeSource) { $this->authorize('update', $knowledgeSource->chatbot); $knowledgeSource->update([ 'status' => 'pending', 'extracted_content' => null, ]); // dispatch(new ProcessKnowledgeSource($knowledgeSource)); return back()->with('success', 'La fuente de conocimiento está siendo reprocesada.'); } }
5. Configurar las Rutas
routes/web.php
<?php use App\Http\Controllers\KnowledgeSourceController; use App\Http\Controllers\ChatbotController; use Illuminate\Support\Facades\Route; Route::middleware(['auth'])->group(function () { // Rutas de Chatbots Route::resource('chatbots', ChatbotController::class); // Rutas de Fuentes de Conocimiento Route::resource('knowledge-sources', KnowledgeSourceController::class) ->except(['index', 'create', 'store']); // Rutas anidadas para fuentes de conocimiento (dentro de un chatbot) Route::prefix('chatbots/{chatbot}')->group(function () { Route::get('/knowledge-sources', [KnowledgeSourceController::class, 'index']) ->name('knowledge-sources.index'); Route::get('/knowledge-sources/create', [KnowledgeSourceController::class, 'create']) ->name('knowledge-sources.create'); Route::post('/knowledge-sources', [KnowledgeSourceController::class, 'store']) ->name('knowledge-sources.store'); Route::post('/knowledge-sources/reprocess', [KnowledgeSourceController::class, 'reprocess']) ->name('knowledge-sources.reprocess'); }); });
6. Crear las Vistas (Inertia/Vue)
6.1. Vista de Listado
resources/js/Pages/KnowledgeSources/Index.vue
<template> <AppLayout> <template #header> <div class="flex items-center justify-between"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"> Fuentes de Conocimiento - {{ chatbot.name }} </h2> <Link :href="route('knowledge-sources.create', chatbot.id)" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition" > <PlusIcon class="w-5 h-5 mr-2" /> Agregar Fuente </Link> </div> </template> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900 dark:text-gray-100"> <!-- Tabla de fuentes de conocimiento --> <div class="overflow-x-auto"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <thead class="bg-gray-50 dark:bg-gray-700"> <tr> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> Nombre </th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> Tipo </th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> Estado </th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> Creado </th> <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> Acciones </th> </tr> </thead> <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> <tr v-for="source in knowledgeSources.data" :key="source.id"> <td class="px-6 py-4 whitespace-nowrap"> <div class="text-sm font-medium text-gray-900 dark:text-gray-100"> {{ source.name }} </div> </td> <td class="px-6 py-4 whitespace-nowrap"> <span :class="getTypeBadgeClass(source.type)" class="px-2 py-1 text-xs rounded-full"> {{ source.type.toUpperCase() }} </span> </td> <td class="px-6 py-4 whitespace-nowrap"> <span :class="getStatusBadgeClass(source.status)" class="px-2 py-1 text-xs rounded-full"> {{ getStatusLabel(source.status) }} </span> </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> {{ new Date(source.created_at).toLocaleDateString() }} </td> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <Link :href="route('knowledge-sources.show', source.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 mr-3" > Ver </Link> <Link :href="route('knowledge-sources.edit', source.id)" class="text-yellow-600 hover:text-yellow-900 dark:text-yellow-400 dark:hover:text-yellow-300 mr-3" > Editar </Link> <button @click="reprocessSource(source)" v-if="source.status !== 'processed'" class="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300 mr-3" > Reprocesar </button> <button @click="deleteSource(source)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300" > Eliminar </button> </td> </tr> </tbody> </table> </div> <!-- Paginación --> <Pagination :links="knowledgeSources.links" class="mt-4" /> </div> </div> </div> </div> </AppLayout> </template> <script setup> import AppLayout from '@/Layouts/AppLayout.vue'; import { Link } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3'; import Pagination from '@/Components/Pagination.vue'; import PlusIcon from '@/Components/Icons/PlusIcon.vue'; const props = defineProps({ chatbot: Object, knowledgeSources: Object, }); const getTypeBadgeClass = (type) => { const classes = { pdf: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300', website: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300', text: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', csv: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', excel: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300', json: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300', }; return classes[type] || 'bg-gray-100 text-gray-800'; }; const getStatusBadgeClass = (status) => { const classes = { pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300', processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300', processed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300', failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300', }; return classes[status] || 'bg-gray-100 text-gray-800'; }; const getStatusLabel = (status) => { const labels = { pending: 'Pendiente', processing: 'Procesando', processed: 'Procesado', failed: 'Falló', }; return labels[status] || status; }; const deleteSource = (source) => { if (confirm(`¿Estás seguro de que quieres eliminar "${source.name}"?`)) { router.delete(route('knowledge-sources.destroy', source.id)); } }; const reprocessSource = (source) => { if (confirm(`¿Quieres reprocesar "${source.name}"?`)) { router.post(route('knowledge-sources.reprocess', source.id)); } }; </script>
7. Tipos de Fuentes de Conocimiento
| Tipo | Descripción | Ejemplo de Uso |
|---|---|---|
| Documentos PDF | Manuales, reportes, documentación técnica | |
| Website | URLs de páginas web | Blogs, páginas de productos, documentación online |
| Text | Texto plano ingresado manualmente | Notas, instrucciones personalizadas |
| CSV | Archivos CSV | Datos estructurados en tabla |
| Excel | Archivos de Excel | Hojas de cálculo con datos |
| JSON | Archivos JSON | Datos estructurados en formato JSON |
8. Estructura de la Tabla
knowledge_sources ├── id (UUID, PK) ├── chatbot_id (UUID, FK) ├── name (string) ├── type (enum: pdf, website, text, csv, excel, json) ├── path (string, nullable) ├── extracted_content (longText, nullable) ├── status (enum: pending, processing, processed, failed) ├── metadata (json, nullable) ├── created_at (timestamp) └── updated_at (timestamp)
9. Mejores Prácticas
Procesamiento asíncrono: El procesamiento de las fuentes debe hacerse en background usando Jobs
Validación robusta: Validar los archivos y URLs antes de procesarlos
Almacenamiento seguro: Guardar los archivos en storage y no directamente en la BD
Cacheo: Cachear el contenido extraído para mejorar el rendimiento
Actualización incremental: Solo procesar cambios cuando sea necesario
10. Resumen
Lo que hemos logrado:
✅ Modelo KnowledgeSource con UUID y relaciones
✅ Migración con todos los campos necesarios
✅ Factory con datos de prueba para todos los tipos
✅ Controlador RESTful completo
✅ Vistas básicas en Inertia.js
✅ Soporte para múltiples tipos de fuentes
✅ Sistema de estados para seguimiento del procesamiento
Próximos Pasos:
Implementar el procesamiento real de archivos (PDF, Websites, etc.)
Crear Jobs para procesamiento en background
Integrar el contenido extraído con el sistema de embeddings
Implementar búsqueda semántica en el contenido
Conectar con la API de OpenAI para generar respuestas basadas en el conocimiento
¡Excelente trabajo! Ahora tenemos una base sólida para gestionar las fuentes de conocimiento de los chatbots
Comentarios
Publicar un comentario