From 119fea13a58e6333c097f97e783ded4955bc13d0 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Tue, 25 Nov 2025 11:46:52 -0300 Subject: [PATCH] =?UTF-8?q?Fix-Feat:=20Invalida=20la=20cach=C3=A9=20al=20m?= =?UTF-8?q?odificar=20fuentes=20de=20contexto.=20A=C3=B1ade=20selector=20d?= =?UTF-8?q?e=20contenido=20de=20contexto.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se soluciona un bug donde el chatbot no reconocía las nuevas fuentes de conocimiento o los items de contexto añadidos desde el panel de administración hasta que la API se reiniciaba. Ahora, el AdminController borra la caché correspondiente después de cada operación de Crear, Actualizar o Eliminar. Esto fuerza al chatbot a recargar la información desde la base de datos en la siguiente petición, haciendo que los cambios se reflejen de forma inmediata. Se añade un nuevo campo para determinar el selector de contenido dentro de la web al momento de realizar el scrap. --- ChatbotApi/Constrollers/AdminController.cs | 26 +- ChatbotApi/Constrollers/ChatController.cs | 63 +-- ChatbotApi/Data/Models/FuenteContexto.cs | 5 +- ..._AddSelectorContenidoToFuentes.Designer.cs | 368 ++++++++++++++++++ ...125135810_AddSelectorContenidoToFuentes.cs | 29 ++ .../Migrations/AppContextoModelSnapshot.cs | 4 + ChatbotApi/Services/CacheKeys.cs | 9 + .../src/components/SourceManager.tsx | 9 + chatbot-widget/src/components/Chatbot.tsx | 2 +- 9 files changed, 489 insertions(+), 26 deletions(-) create mode 100644 ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.Designer.cs create mode 100644 ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.cs create mode 100644 ChatbotApi/Services/CacheKeys.cs diff --git a/ChatbotApi/Constrollers/AdminController.cs b/ChatbotApi/Constrollers/AdminController.cs index 8104c31..1298ead 100644 --- a/ChatbotApi/Constrollers/AdminController.cs +++ b/ChatbotApi/Constrollers/AdminController.cs @@ -2,6 +2,8 @@ using ChatbotApi.Data.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Caching.Memory; +using ChatbotApi.Services; namespace ChatbotApi.Controllers { @@ -11,10 +13,12 @@ namespace ChatbotApi.Controllers public class AdminController : ControllerBase { private readonly AppContexto _context; + private readonly IMemoryCache _cache; - public AdminController(AppContexto context) + public AdminController(AppContexto context, IMemoryCache cache) { _context = context; + _cache = cache; } // GET: api/admin/contexto @@ -36,6 +40,10 @@ namespace ChatbotApi.Controllers item.FechaActualizacion = DateTime.UtcNow; _context.ContextoItems.Add(item); await _context.SaveChangesAsync(); + + // Invalida la caché de KnowledgeItems + _cache.Remove(CacheKeys.KnowledgeItems); + return CreatedAtAction(nameof(GetAllContextoItems), new { id = item.Id }, item); } @@ -58,6 +66,9 @@ namespace ChatbotApi.Controllers existingItem.FechaActualizacion = DateTime.UtcNow; await _context.SaveChangesAsync(); + // Invalida la caché de KnowledgeItems + _cache.Remove(CacheKeys.KnowledgeItems); + return NoContent(); } @@ -72,6 +83,9 @@ namespace ChatbotApi.Controllers } _context.ContextoItems.Remove(item); await _context.SaveChangesAsync(); + // Invalida la caché de KnowledgeItems + _cache.Remove(CacheKeys.KnowledgeItems); + return NoContent(); } @@ -100,6 +114,10 @@ namespace ChatbotApi.Controllers { _context.FuentesDeContexto.Add(fuente); await _context.SaveChangesAsync(); + + // Invalida la caché de FuentesDeContexto + _cache.Remove(CacheKeys.FuentesDeContexto); + return CreatedAtAction(nameof(GetAllFuentes), new { id = fuente.Id }, fuente); } @@ -129,6 +147,9 @@ namespace ChatbotApi.Controllers } } + // Invalida la caché de FuentesDeContexto + _cache.Remove(CacheKeys.FuentesDeContexto); + return NoContent(); } @@ -144,6 +165,9 @@ namespace ChatbotApi.Controllers _context.FuentesDeContexto.Remove(fuente); await _context.SaveChangesAsync(); + // Invalida la caché de FuentesDeContexto + _cache.Remove(CacheKeys.FuentesDeContexto); + return NoContent(); } } diff --git a/ChatbotApi/Constrollers/ChatController.cs b/ChatbotApi/Constrollers/ChatController.cs index 3aa5599..10cfb1d 100644 --- a/ChatbotApi/Constrollers/ChatController.cs +++ b/ChatbotApi/Constrollers/ChatController.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Caching.Memory; using System.Runtime.CompilerServices; using System.Text.Json; using System.Globalization; +using ChatbotApi.Services; // Clases de Request/Response public class GenerationConfig @@ -51,9 +52,6 @@ namespace ChatbotApi.Controllers private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private static readonly HttpClient _httpClient = new HttpClient(); - private static readonly string _knowledgeCacheKey = "KnowledgeBase"; - private static readonly string _fuentesCacheKey = "FuentesDeContexto"; - private static readonly string _siteUrl = "https://www.eldia.com/"; private static readonly string[] PrefijosAQuitar = { "VIDEO.- ", "VIDEO. ", "FOTOS.- ", "FOTOS. " }; const int OutTokens = 8192; @@ -207,7 +205,9 @@ namespace ChatbotApi.Controllers foreach (var fuente in fuentesExternas) { contextBuilder.AppendLine($"\n--- Información de la página '{fuente.Nombre}' ---"); - string scrapedContent = await ScrapeUrlContentAsync(fuente.Url); + + string scrapedContent = await ScrapeUrlContentAsync(fuente); + contextBuilder.AppendLine(scrapedContent); } @@ -272,7 +272,7 @@ namespace ChatbotApi.Controllers { var promptBuilder = new StringBuilder(); promptBuilder.AppendLine("INSTRUCCIONES:"); - promptBuilder.AppendLine("Eres DiaBot, el asistente virtual del periódico El Día. Tu personalidad es profesional, servicial y concisa. Responde siempre en español Rioplatense."); + promptBuilder.AppendLine("Eres DiaBot, el asistente virtual del periódico El Día. Tu personalidad es profesional, servicial y concisa. Responde siempre en español Rioplatense. El usuario se encuentra navegando en la web de eldia.com"); // CONTEXTO FIJO try { @@ -287,7 +287,7 @@ namespace ChatbotApi.Controllers promptBuilder.AppendLine("Usa esta información para dar contexto a las noticias y responder preguntas sobre el día o la ubicación."); promptBuilder.AppendLine("--------------------------------------------------"); } - catch(Exception ex) + catch (Exception ex) { _logger.LogWarning(ex, "No se pudo determinar la zona horaria de Argentina. El contexto de tiempo será omitido."); } @@ -501,7 +501,7 @@ namespace ChatbotApi.Controllers private async Task> GetKnowledgeItemsAsync() { - return await _cache.GetOrCreateAsync(_knowledgeCacheKey, async entry => + return await _cache.GetOrCreateAsync(CacheKeys.KnowledgeItems, async entry => { _logger.LogInformation("Cargando ContextoItems desde la base de datos a la caché..."); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); @@ -515,7 +515,7 @@ namespace ChatbotApi.Controllers private async Task> GetFuentesDeContextoAsync() { - return await _cache.GetOrCreateAsync(_fuentesCacheKey, async entry => + return await _cache.GetOrCreateAsync(CacheKeys.FuentesDeContexto, async entry => { _logger.LogInformation("Cargando FuentesDeContexto desde la base de datos a la caché..."); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); @@ -562,30 +562,47 @@ namespace ChatbotApi.Controllers } } - private async Task ScrapeUrlContentAsync(string url) + private async Task ScrapeUrlContentAsync(FuenteContexto fuente) { - return await _cache.GetOrCreateAsync($"scrape_{url}", async entry => + // La clave de caché sigue siendo la misma. + var result = await _cache.GetOrCreateAsync($"scrape_{fuente.Url}_{fuente.SelectorContenido}", async entry => { - _logger.LogInformation("Contenido de {Url} no encontrado en caché. Scrapeando...", url); + _logger.LogInformation("Contenido de {Url} no encontrado en caché. Scrapeando...", fuente.Url); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); var web = new HtmlWeb(); - var doc = await web.LoadFromWebAsync(url); - var mainContentNode = doc.DocumentNode.SelectSingleNode("//main") ?? doc.DocumentNode.SelectSingleNode("//body"); + var doc = await web.LoadFromWebAsync(fuente.Url); - if (mainContentNode == null) return string.Empty; + HtmlNode? contentNode; + string selectorUsado; - // Extraer texto de etiquetas comunes de contenido - var textNodes = mainContentNode.SelectNodes(".//p | .//h1 | .//h2 | .//h3 | .//li"); - if (textNodes == null) return WebUtility.HtmlDecode(mainContentNode.InnerText); - - var sb = new StringBuilder(); - foreach (var node in textNodes) + // Si se especificó un selector en la base de datos, lo usamos. + if (!string.IsNullOrWhiteSpace(fuente.SelectorContenido)) { - sb.AppendLine(WebUtility.HtmlDecode(node.InnerText).Trim()); + selectorUsado = fuente.SelectorContenido; + contentNode = doc.DocumentNode.SelectSingleNode(selectorUsado); } - return sb.ToString(); - }) ?? string.Empty; + else + { + // Si no, usamos nuestro fallback genérico a
o . + selectorUsado = "//main | //body"; + contentNode = doc.DocumentNode.SelectSingleNode("//main") ?? doc.DocumentNode.SelectSingleNode("//body"); + } + + if (contentNode == null) + { + _logger.LogWarning("No se encontró contenido en {Url} con el selector '{Selector}'", fuente.Url, selectorUsado); + return string.Empty; + } + + _logger.LogInformation("Extrayendo texto de {Url} usando el selector '{Selector}'", fuente.Url, selectorUsado); + + // --- LA LÓGICA CLAVE Y SIMPLIFICADA --- + // Extraemos TODO el texto visible dentro del nodo seleccionado, sin importar las etiquetas. + // InnerText es recursivo y obtiene el texto de todos los nodos hijos. + return WebUtility.HtmlDecode(contentNode.InnerText) ?? string.Empty; + }); + return result ?? string.Empty; } } } \ No newline at end of file diff --git a/ChatbotApi/Data/Models/FuenteContexto.cs b/ChatbotApi/Data/Models/FuenteContexto.cs index b8b6f8a..aec1705 100644 --- a/ChatbotApi/Data/Models/FuenteContexto.cs +++ b/ChatbotApi/Data/Models/FuenteContexto.cs @@ -16,7 +16,10 @@ public class FuenteContexto [Required] [MaxLength(500)] - public string DescripcionParaIA { get; set; } = null!; // ¡La parte más importante! + public string DescripcionParaIA { get; set; } = null!; public bool Activo { get; set; } = true; + + [MaxLength(200)] + public string? SelectorContenido { get; set; } // Ej: "div.contenedor-planes" } \ No newline at end of file diff --git a/ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.Designer.cs b/ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.Designer.cs new file mode 100644 index 0000000..f4cf37e --- /dev/null +++ b/ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.Designer.cs @@ -0,0 +1,368 @@ +// +using System; +using ChatbotApi.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ChatbotApi.Migrations +{ + [DbContext(typeof(AppContexto))] + [Migration("20251125135810_AddSelectorContenidoToFuentes")] + partial class AddSelectorContenidoToFuentes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ChatbotApi.Data.Models.ContextoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Clave") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Descripcion") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FechaActualizacion") + .HasColumnType("datetime2"); + + b.Property("Valor") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("Id"); + + b.ToTable("ContextoItems"); + }); + + modelBuilder.Entity("ChatbotApi.Data.Models.ConversacionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BotRespuesta") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Fecha") + .HasColumnType("datetime2"); + + b.Property("UsuarioMensaje") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("ConversacionLogs"); + }); + + modelBuilder.Entity("FuenteContexto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Activo") + .HasColumnType("bit"); + + b.Property("DescripcionParaIA") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Nombre") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SelectorContenido") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("Id"); + + b.ToTable("FuentesDeContexto"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.cs b/ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.cs new file mode 100644 index 0000000..b6235f8 --- /dev/null +++ b/ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ChatbotApi.Migrations +{ + /// + public partial class AddSelectorContenidoToFuentes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SelectorContenido", + table: "FuentesDeContexto", + type: "nvarchar(200)", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SelectorContenido", + table: "FuentesDeContexto"); + } + } +} diff --git a/ChatbotApi/Migrations/AppContextoModelSnapshot.cs b/ChatbotApi/Migrations/AppContextoModelSnapshot.cs index d48f446..ac4ab2e 100644 --- a/ChatbotApi/Migrations/AppContextoModelSnapshot.cs +++ b/ChatbotApi/Migrations/AppContextoModelSnapshot.cs @@ -97,6 +97,10 @@ namespace ChatbotApi.Migrations .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("SelectorContenido") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + b.Property("Url") .IsRequired() .HasMaxLength(1000) diff --git a/ChatbotApi/Services/CacheKeys.cs b/ChatbotApi/Services/CacheKeys.cs new file mode 100644 index 0000000..4c3ceda --- /dev/null +++ b/ChatbotApi/Services/CacheKeys.cs @@ -0,0 +1,9 @@ +// ChatbotApi/Services/CacheKeys.cs +namespace ChatbotApi.Services +{ + public static class CacheKeys + { + public const string KnowledgeItems = "KnowledgeBase"; + public const string FuentesDeContexto = "FuentesDeContexto"; + } +} \ No newline at end of file diff --git a/chatbot-admin/src/components/SourceManager.tsx b/chatbot-admin/src/components/SourceManager.tsx index b868724..de183b8 100644 --- a/chatbot-admin/src/components/SourceManager.tsx +++ b/chatbot-admin/src/components/SourceManager.tsx @@ -15,6 +15,7 @@ interface FuenteContexto { url: string; descripcionParaIA: string; activo: boolean; + selectorContenido?: string; } interface SourceManagerProps { @@ -169,6 +170,14 @@ const SourceManager: React.FC = ({ onAuthError }) => { onChange={(e) => setCurrentRow({ ...currentRow, descripcionParaIA: e.target.value })} helperText="¡Crucial! Describe en una frase para qué sirve esta fuente. Ej: 'Usar para responder preguntas sobre cómo registrarse, iniciar sesión o sobre el registro'." /> + setCurrentRow({ ...currentRow, selectorContenido: e.target.value })} + helperText="Selector CSS/XPath para apuntar al 'div' específico que contiene la información. Ej: //div[@id='precios']" + /> { {activeArticle && (
- Hablando sobre: + Enlace relacionado: {activeArticle.title}