Feat: Principio Memoria de Hilo Conversacional
This commit is contained in:
@@ -64,7 +64,45 @@ namespace ChatbotApi.Controllers
|
||||
_apiUrl = $"{baseUrl}{apiKey}";
|
||||
}
|
||||
|
||||
private async Task<IntentType> GetIntentAsync(string userMessage, string? activeArticleContent)
|
||||
private async Task<string> UpdateConversationSummaryAsync(string? oldSummary, string userMessage, string botResponse)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(oldSummary))
|
||||
{
|
||||
oldSummary = "Esta es una nueva conversación.";
|
||||
}
|
||||
|
||||
var promptBuilder = new StringBuilder();
|
||||
promptBuilder.AppendLine("Tu tarea es actualizar un resumen de conversación. Basado en el RESUMEN ANTERIOR y el ÚLTIMO INTERCAMBIO, crea un nuevo resumen conciso. Mantén solo los puntos clave y el tema principal de la conversación.");
|
||||
promptBuilder.AppendLine("\n--- RESUMEN ANTERIOR ---");
|
||||
promptBuilder.AppendLine(oldSummary);
|
||||
promptBuilder.AppendLine("\n--- ÚLTIMO INTERCAMBIO ---");
|
||||
promptBuilder.AppendLine($"Usuario: \"{userMessage}\"");
|
||||
promptBuilder.AppendLine($"Bot: \"{new string(botResponse.Take(300).ToArray())}...\"");
|
||||
promptBuilder.AppendLine("\n--- NUEVO RESUMEN CONCISO ---");
|
||||
|
||||
var finalPrompt = promptBuilder.ToString();
|
||||
var requestData = new GeminiRequest { Contents = new[] { new Content { Parts = new[] { new Part { Text = finalPrompt } } } } };
|
||||
var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData);
|
||||
if (!response.IsSuccessStatusCode) return oldSummary ?? "";
|
||||
|
||||
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>();
|
||||
var newSummary = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim();
|
||||
|
||||
_logger.LogInformation("Resumen de conversación actualizado: '{NewSummary}'", newSummary);
|
||||
return newSummary ?? oldSummary ?? "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Excepción en UpdateConversationSummaryAsync. Se mantendrá el resumen anterior.");
|
||||
return oldSummary ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IntentType> GetIntentAsync(string userMessage, string? activeArticleContent, string? conversationSummary)
|
||||
{
|
||||
var promptBuilder = new StringBuilder();
|
||||
promptBuilder.AppendLine("Tu tarea es actuar como un router de intenciones. Basado en la PREGUNTA DEL USUARIO, decide qué herramienta es la más apropiada para encontrar la respuesta. Responde única y exclusivamente con una de las siguientes tres opciones: [ARTICULO_ACTUAL], [BASE_DE_DATOS], [NOTICIAS_PORTADA].");
|
||||
@@ -73,6 +111,12 @@ namespace ChatbotApi.Controllers
|
||||
promptBuilder.AppendLine("[BASE_DE_DATOS]: Úsala si la pregunta es sobre información específica y general del diario, como datos de contacto (teléfono, dirección), publicidad o suscripciones.");
|
||||
promptBuilder.AppendLine("[NOTICIAS_PORTADA]: Úsala para preguntas generales sobre noticias actuales, eventos, o si ninguna de las otras herramientas parece adecuada.");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(conversationSummary))
|
||||
{
|
||||
promptBuilder.AppendLine("\n--- RESUMEN DE LA CONVERSACIÓN ACTUAL ---");
|
||||
promptBuilder.AppendLine(conversationSummary);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(activeArticleContent))
|
||||
{
|
||||
promptBuilder.AppendLine("\n--- CONVERSACIÓN ACTUAL (Contexto del artículo) ---");
|
||||
@@ -134,7 +178,8 @@ namespace ChatbotApi.Controllers
|
||||
articleContext = await GetArticleContentAsync(request.ContextUrl);
|
||||
}
|
||||
|
||||
intent = await GetIntentAsync(userMessage, articleContext);
|
||||
// Le pasamos el resumen al router de intenciones
|
||||
intent = await GetIntentAsync(userMessage, articleContext, request.ConversationSummary);
|
||||
|
||||
switch (intent)
|
||||
{
|
||||
@@ -147,7 +192,7 @@ namespace ChatbotApi.Controllers
|
||||
case IntentType.Database:
|
||||
_logger.LogInformation("Ejecutando intención: Base de Datos.");
|
||||
var knowledgeBase = await GetKnowledgeAsync();
|
||||
context = await FindBestDbItemAsync(userMessage, knowledgeBase) ?? "No se encontró información relevante en la base de datos.";
|
||||
context = await FindBestDbItemAsync(userMessage, request.ConversationSummary, knowledgeBase) ?? "No se encontró información relevante en la base de datos.";
|
||||
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene información específica (contacto, suscripciones, etc.).";
|
||||
break;
|
||||
|
||||
@@ -174,6 +219,8 @@ namespace ChatbotApi.Controllers
|
||||
}
|
||||
|
||||
Stream? responseStream = null;
|
||||
var fullBotReply = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
var promptBuilder = new StringBuilder();
|
||||
@@ -222,7 +269,6 @@ namespace ChatbotApi.Controllers
|
||||
yield break;
|
||||
}
|
||||
|
||||
var fullBotReply = new StringBuilder();
|
||||
if (responseStream != null)
|
||||
{
|
||||
await using (responseStream)
|
||||
@@ -258,7 +304,14 @@ namespace ChatbotApi.Controllers
|
||||
|
||||
if (fullBotReply.Length > 0)
|
||||
{
|
||||
// Guardamos el log de la conversación como antes
|
||||
await SaveConversationLogAsync(userMessage, fullBotReply.ToString());
|
||||
|
||||
// Creamos el nuevo resumen
|
||||
var newSummary = await UpdateConversationSummaryAsync(request.ConversationSummary, userMessage, fullBotReply.ToString());
|
||||
|
||||
// Enviamos el nuevo resumen al frontend como el último mensaje del stream
|
||||
yield return $"SUMMARY::{newSummary}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,14 +338,22 @@ namespace ChatbotApi.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> FindBestDbItemAsync(string userMessage, Dictionary<string, string> knowledgeBase)
|
||||
private async Task<string?> FindBestDbItemAsync(string userMessage, string? conversationSummary, Dictionary<string, string> knowledgeBase)
|
||||
{
|
||||
if (knowledgeBase == null || !knowledgeBase.Any()) return null;
|
||||
|
||||
var availableKeys = string.Join(", ", knowledgeBase.Keys);
|
||||
|
||||
var promptBuilder = new StringBuilder();
|
||||
promptBuilder.AppendLine("Tu tarea es actuar como un buscador semántico. Basado en la PREGUNTA DEL USUARIO, elige la CLAVE más relevante de la LISTA DE CLAVES DISPONIBLES. Responde única y exclusivamente con la clave que elijas.");
|
||||
promptBuilder.AppendLine("Tu tarea es actuar como un buscador semántico. Usa el RESUMEN para entender el contexto de la conversación. Basado en la PREGUNTA DEL USUARIO, elige la CLAVE más relevante de la lista. Responde única y exclusivamente con la clave que elijas.");
|
||||
|
||||
// Añadimos el resumen al prompt del buscador
|
||||
if (!string.IsNullOrWhiteSpace(conversationSummary))
|
||||
{
|
||||
promptBuilder.AppendLine("\n--- RESUMEN DE LA CONVERSACIÓN ---");
|
||||
promptBuilder.AppendLine(conversationSummary);
|
||||
}
|
||||
|
||||
promptBuilder.AppendLine("\n--- LISTA DE CLAVES DISPONIBLES ---");
|
||||
promptBuilder.AppendLine(availableKeys);
|
||||
promptBuilder.AppendLine("\n--- PREGUNTA DEL USUARIO ---");
|
||||
|
||||
@@ -6,4 +6,5 @@ public class ChatRequest
|
||||
[MaxLength(200)]
|
||||
public required string Message { get; set; }
|
||||
public string? ContextUrl { get; set; }
|
||||
public string? ConversationSummary { get; set; }
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const MAX_CHARS = 200;
|
||||
// Constantes para la clave del localStorage
|
||||
const CHAT_HISTORY_KEY = 'chatbot-history';
|
||||
const CHAT_CONTEXT_KEY = 'chatbot-active-article';
|
||||
const CHAT_SUMMARY_KEY = 'chatbot-summary';
|
||||
|
||||
const Chatbot: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -51,6 +52,23 @@ const Chatbot: React.FC = () => {
|
||||
return null;
|
||||
});
|
||||
|
||||
const [conversationSummary, setConversationSummary] = useState<string>(() => {
|
||||
try {
|
||||
return localStorage.getItem(CHAT_SUMMARY_KEY) || "";
|
||||
} catch (error) {
|
||||
console.error("No se pudo cargar el resumen de la conversación:", error);
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(CHAT_SUMMARY_KEY, conversationSummary);
|
||||
} catch (error) {
|
||||
console.error("No se pudo guardar el resumen de la conversación:", error);
|
||||
}
|
||||
}, [conversationSummary]);
|
||||
|
||||
// Añadimos un useEffect para guardar los mensajes.
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -102,14 +120,14 @@ const Chatbot: React.FC = () => {
|
||||
const messageToSend = inputValue;
|
||||
setInputValue('');
|
||||
|
||||
// Inicia el estado de carga, pero aún no el de streaming
|
||||
setIsLoading(true);
|
||||
setIsStreaming(false);
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
message: messageToSend,
|
||||
contextUrl: activeArticle ? activeArticle.url : null
|
||||
contextUrl: activeArticle ? activeArticle.url : null,
|
||||
conversationSummary: conversationSummary
|
||||
};
|
||||
|
||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/chat/stream-message`, {
|
||||
@@ -133,22 +151,41 @@ const Chatbot: React.FC = () => {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
// ... (La lógica de `if (done)` no cambia)
|
||||
try {
|
||||
const responseArray = JSON.parse(fullReplyRaw);
|
||||
let intent = 'Homepage';
|
||||
let messageChunks = [];
|
||||
if (Array.isArray(responseArray) && responseArray.length > 0) {
|
||||
if (typeof responseArray[0] === 'string' && responseArray[0].startsWith('INTENT::')) {
|
||||
intent = responseArray[0].split('::')[1];
|
||||
messageChunks = responseArray.slice(1);
|
||||
const messageChunks: string[] = [];
|
||||
let finalSummary = conversationSummary;
|
||||
|
||||
if (Array.isArray(responseArray)) {
|
||||
responseArray.forEach((item: string) => {
|
||||
if (typeof item === 'string') {
|
||||
if (item.startsWith('INTENT::')) {
|
||||
intent = item.split('::')[1];
|
||||
} else if (item.startsWith('SUMMARY::')) {
|
||||
finalSummary = item.split('::')[1];
|
||||
} else {
|
||||
messageChunks = responseArray;
|
||||
messageChunks.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConversationSummary(finalSummary);
|
||||
const finalCleanText = messageChunks.join('');
|
||||
|
||||
// 1. Aseguramos que el último mensaje tenga el texto final y 100% limpio.
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
if (updatedMessages.length > 0) {
|
||||
updatedMessages[updatedMessages.length - 1].text = finalCleanText;
|
||||
}
|
||||
return updatedMessages;
|
||||
});
|
||||
|
||||
const linkRegex = /\[(.*?)\]\((https?:\/\/[^\s]+)\)/;
|
||||
const match = finalCleanText.match(linkRegex);
|
||||
|
||||
if (match && match[1] && match[2]) {
|
||||
setActiveArticle({ title: match[1], url: match[2] });
|
||||
} else if (intent === 'Database' || intent === 'Homepage') {
|
||||
@@ -164,26 +201,37 @@ const Chatbot: React.FC = () => {
|
||||
const chunk = decoder.decode(value);
|
||||
fullReplyRaw += chunk;
|
||||
|
||||
// --- LÓGICA DE VISUALIZACIÓN EN TIEMPO REAL ---
|
||||
let cleanTextForDisplay = '';
|
||||
try {
|
||||
const parsedArray = JSON.parse(fullReplyRaw.replace(/,$/, '') + ']');
|
||||
const displayChunks = parsedArray[0] && parsedArray[0].startsWith('INTENT::')
|
||||
? parsedArray.slice(1)
|
||||
: parsedArray;
|
||||
|
||||
// Filtramos CUALQUIER item que sea una pista interna.
|
||||
const displayChunks = Array.isArray(parsedArray)
|
||||
? parsedArray.filter((item: string) =>
|
||||
typeof item === 'string' && !item.startsWith('INTENT::') && !item.startsWith('SUMMARY::')
|
||||
)
|
||||
: [];
|
||||
|
||||
cleanTextForDisplay = displayChunks.join('');
|
||||
} catch (e) {
|
||||
cleanTextForDisplay = fullReplyRaw.replace(/^\["INTENT::.*?","|\["|"]$|","/g, '');
|
||||
// El fallback también debe filtrar ambas pistas.
|
||||
cleanTextForDisplay = fullReplyRaw
|
||||
.replace(/\"INTENT::.*?\",?/g, '')
|
||||
.replace(/\"SUMMARY::.*?\",?/g, '')
|
||||
.replace(/^\["|"]$|","/g, '');
|
||||
}
|
||||
|
||||
if (isFirstChunk) {
|
||||
// En el primer chunk, activamos el flag de streaming
|
||||
setIsStreaming(true);
|
||||
setMessages(prev => [...prev, { text: cleanTextForDisplay, sender: 'bot' }]);
|
||||
isFirstChunk = false;
|
||||
} else {
|
||||
setMessages(prev => {
|
||||
const updatedMessages = [...prev];
|
||||
if (updatedMessages.length > 0) {
|
||||
updatedMessages[updatedMessages.length - 1].text = cleanTextForDisplay;
|
||||
}
|
||||
return updatedMessages;
|
||||
});
|
||||
}
|
||||
@@ -197,7 +245,6 @@ const Chatbot: React.FC = () => {
|
||||
const errorText = error instanceof Error ? error.message : 'Lo siento, no pude conectarme.';
|
||||
setMessages(prev => [...prev, { text: errorText, sender: 'bot' }]);
|
||||
} finally {
|
||||
// Al final, reseteamos ambos estados
|
||||
setIsLoading(false);
|
||||
setIsStreaming(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user