Fix-Feat: Invalida la caché al modificar fuentes de contexto. Añade selector de contenido de contexto.
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.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ChatController> _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
|
||||
{
|
||||
@@ -501,7 +501,7 @@ namespace ChatbotApi.Controllers
|
||||
|
||||
private async Task<Dictionary<string, ContextoItem>> 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<List<FuenteContexto>> 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<string> ScrapeUrlContentAsync(string url)
|
||||
private async Task<string> 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 <main> o <body>.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
368
ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.Designer.cs
generated
Normal file
368
ChatbotApi/Migrations/20251125135810_AddSelectorContenidoToFuentes.Designer.cs
generated
Normal file
@@ -0,0 +1,368 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Clave")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Descripcion")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<DateTime>("FechaActualizacion")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Valor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ContextoItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ChatbotApi.Data.Models.ConversacionLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("BotRespuesta")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("Fecha")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("UsuarioMensaje")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ConversacionLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FuenteContexto", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("Activo")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("DescripcionParaIA")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Nombre")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("SelectorContenido")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("FuentesDeContexto");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("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<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("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<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("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<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ChatbotApi.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSelectorContenidoToFuentes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SelectorContenido",
|
||||
table: "FuentesDeContexto",
|
||||
type: "nvarchar(200)",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SelectorContenido",
|
||||
table: "FuentesDeContexto");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,10 @@ namespace ChatbotApi.Migrations
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("SelectorContenido")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
|
||||
9
ChatbotApi/Services/CacheKeys.cs
Normal file
9
ChatbotApi/Services/CacheKeys.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ interface FuenteContexto {
|
||||
url: string;
|
||||
descripcionParaIA: string;
|
||||
activo: boolean;
|
||||
selectorContenido?: string;
|
||||
}
|
||||
|
||||
interface SourceManagerProps {
|
||||
@@ -169,6 +170,14 @@ const SourceManager: React.FC<SourceManagerProps> = ({ 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'."
|
||||
/>
|
||||
<TextField
|
||||
margin="dense"
|
||||
label="Selector de Contenido (Opcional)"
|
||||
fullWidth
|
||||
value={currentRow.selectorContenido || ''}
|
||||
onChange={(e) => 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']"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
|
||||
@@ -302,7 +302,7 @@ const Chatbot: React.FC = () => {
|
||||
</div>
|
||||
{activeArticle && (
|
||||
<div className="context-indicator">
|
||||
Hablando sobre:
|
||||
Enlace relacionado:
|
||||
<a href={activeArticle.url} target="_blank" rel="noopener noreferrer">
|
||||
{activeArticle.title}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user