Feat: Detección de Mensajes Genéricos (1 Llamada a la API)

This commit is contained in:
2025-12-09 14:05:53 -03:00
parent b2c6ea5ffc
commit 7a24538b6c
3 changed files with 127 additions and 27 deletions

View File

@@ -48,7 +48,7 @@ namespace ChatbotApi.Services
_apiUrl = $"{baseUrl}{apiKey}";
}
// Response model for structured JSON from Gemini
// Modelo de respuesta para JSON estructurado de Gemini
private class GeminiStructuredResponse
{
public string intent { get; set; } = "NOTICIAS_PORTADA";
@@ -75,7 +75,7 @@ namespace ChatbotApi.Services
try
{
// Load article if URL provided
// Cargar artículo si se proporciona URL
if (!string.IsNullOrEmpty(request.ContextUrl) && await UrlSecurity.IsSafeUrlAsync(request.ContextUrl))
{
articleTask = GetArticleContentAsync(request.ContextUrl);
@@ -83,7 +83,7 @@ namespace ChatbotApi.Services
if (articleTask != null) articleContext = await articleTask;
// Build context based on heuristics
// Construir contexto basado en heurísticas
if (!string.IsNullOrEmpty(articleContext))
{
context = articleContext;
@@ -102,7 +102,16 @@ namespace ChatbotApi.Services
if (bestMatch == null)
{
bestMatch = await FindBestMatchingArticleAIAsync(safeUserMessage, articles, request.ConversationSummary);
// Optimización: Solo llamar AI matching si el mensaje parece específico
// Evita llamadas innecesarias para saludos y mensajes genéricos
if (RequiresAIMatching(safeUserMessage))
{
bestMatch = await FindBestMatchingArticleAIAsync(safeUserMessage, articles, request.ConversationSummary);
}
else
{
_logger.LogInformation("Mensaje genérico detectado: '{Message}'. Skipping AI matching.", safeUserMessage);
}
}
if (bestMatch != null && await UrlSecurity.IsSafeUrlAsync(bestMatch.Url))
@@ -122,7 +131,7 @@ namespace ChatbotApi.Services
}
}
// Add knowledge base if available
// Agregar base de conocimiento si está disponible
var knowledgeItems = await GetKnowledgeItemsAsync();
if (knowledgeItems.Any())
{
@@ -146,7 +155,7 @@ namespace ChatbotApi.Services
yield break;
}
// ========== UNIFIED API CALL ==========
// ========== LLAMADA API UNIFICADA ==========
var httpClient = _httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(45);
@@ -158,7 +167,7 @@ namespace ChatbotApi.Services
? request.SystemPromptOverride
: await systemPromptsTask;
// Build unified meta-prompt
// Construir meta-prompt unificado
var promptBuilder = new StringBuilder();
promptBuilder.AppendLine("<instrucciones_sistema>");
@@ -197,7 +206,7 @@ namespace ChatbotApi.Services
promptBuilder.AppendLine("</instrucciones_sistema>");
promptBuilder.AppendLine();
// Conversation history
// Historial de conversación
if (!string.IsNullOrWhiteSpace(request.ConversationSummary))
{
promptBuilder.AppendLine("<historial_conversacion>");
@@ -206,13 +215,13 @@ namespace ChatbotApi.Services
promptBuilder.AppendLine();
}
// Context
// Contexto
promptBuilder.AppendLine("<contexto>");
promptBuilder.AppendLine(context);
promptBuilder.AppendLine("</contexto>");
promptBuilder.AppendLine();
// User question
// Pregunta del usuario
promptBuilder.AppendLine("<pregunta_usuario>");
promptBuilder.AppendLine(safeUserMessage);
promptBuilder.AppendLine("</pregunta_usuario>");
@@ -227,7 +236,7 @@ namespace ChatbotApi.Services
SafetySettings = GetDefaultSafetySettings()
};
// Use non-streaming endpoint
// Usar endpoint sin streaming
var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?");
var response = await httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData, cancellationToken);
@@ -259,11 +268,11 @@ namespace ChatbotApi.Services
yield break;
}
// Parse JSON response (outside try-catch to allow yield)
// Parsear respuesta JSON (fuera del try-catch para permitir yield)
GeminiStructuredResponse? apiResponse = null;
try
{
// Extract JSON from markdown code blocks if present
// Extraer JSON de bloques de código markdown si están presentes
var jsonContent = jsonText!;
if (jsonText!.Contains("```json"))
{
@@ -291,7 +300,7 @@ namespace ChatbotApi.Services
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to parse Gemini JSON. Raw response: {JsonText}", jsonText);
_logger.LogError(ex, "Error al parsear JSON de Gemini. Respuesta raw: {JsonText}", jsonText);
}
if (apiResponse == null || string.IsNullOrEmpty(apiResponse.reply))
@@ -300,10 +309,10 @@ namespace ChatbotApi.Services
yield break;
}
// Send intent metadata
// Enviar metadata de intención
yield return $"INTENT::{apiResponse.intent}";
// Simulate streaming by chunking the reply
// Simular streaming dividiendo la respuesta en fragmentos
string fullReply = apiResponse.reply;
var words = fullReply.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var chunkBuilder = new StringBuilder();
@@ -312,7 +321,7 @@ namespace ChatbotApi.Services
{
chunkBuilder.Append(word + " ");
// Send chunk every ~20 characters for smooth streaming
// Enviar fragmento cada ~20 caracteres para streaming fluido
if (chunkBuilder.Length >= 20)
{
yield return chunkBuilder.ToString();
@@ -321,13 +330,13 @@ namespace ChatbotApi.Services
}
}
// Send any remaining text
// Enviar cualquier texto restante
if (chunkBuilder.Length > 0)
{
yield return chunkBuilder.ToString();
}
// Log conversation (fire-and-forget)
// Registrar conversación (fire-and-forget)
_ = Task.Run(async () =>
{
using (var scope = _serviceProvider.CreateScope())
@@ -346,16 +355,16 @@ namespace ChatbotApi.Services
catch(Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<ChatService>>();
logger.LogError(ex, "Error in background logging");
logger.LogError(ex, "Error en registro en segundo plano");
}
}
});
// Send summary
// Enviar resumen
yield return $"SUMMARY::{apiResponse.summary}";
}
// --- PRIVATE METHODS ---
// --- MÉTODOS PRIVADOS ---
private string SanitizeInput(string? input)
{
@@ -390,8 +399,99 @@ namespace ChatbotApi.Services
};
}
// NOTE: UpdateConversationSummaryAsync and GetIntentAsync have been REMOVED
// Their functionality is now in the unified StreamMessageAsync call
/// <summary>
/// Determina si un mensaje requiere búsqueda AI de artículos.
/// Usa enfoque híbrido: heurísticas (longitud, estructura) + patrones comunes.
/// Retorna false para mensajes genéricos (saludos, respuestas cortas, confirmaciones)
/// para evitar llamadas innecesarias a la API y reducir latencia.
/// </summary>
private bool RequiresAIMatching(string userMessage)
{
// Normalizar: lowercase, trim, quitar puntuación final
var normalized = userMessage.Trim().ToLowerInvariant()
.TrimEnd('.', '!', '?', ',', ';');
// Contar palabras (excluyendo puntuación)
var wordCount = normalized
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Length;
// ========== REGLA 1: Mensajes ultra-cortos (1-2 palabras) ==========
// Probablemente sean saludos o respuestas cortas, SALVO que contengan keywords específicas
if (wordCount <= 2)
{
// Excepciones: keywords de temas que SÍ requieren búsqueda de artículos
var specificKeywords = new[] {
"economía", "economia", "inflación", "inflacion", "dólar", "dolar",
"política", "politica", "elecciones", "gobierno",
"clima", "deporte", "fútbol", "futbol", "boca", "river"
};
// Si NO contiene ningún keyword específico, skip AI
if (!specificKeywords.Any(k => normalized.Contains(k)))
{
return false; // Skip AI - probablemente saludo/respuesta corta
}
}
// ========== REGLA 2: Preguntas casuales cortas ==========
// Si tiene signos de pregunta y es corto (≤4 palabras)
if (userMessage.Contains('?') && wordCount <= 4)
{
var casualQuestions = new[] {
"qué tal", "que tal", "cómo va", "como va",
"cómo estás", "como estas", "cómo andás", "como andas",
"todo bien", "qué onda", "que onda"
};
if (casualQuestions.Any(q => normalized.Contains(q)))
{
return false; // Skip AI - pregunta casual
}
}
// ========== REGLA 3: Lista expandida de patrones comunes ==========
// Mensajes cortos (≤3 palabras) que claramente son genéricos
if (wordCount <= 3)
{
var genericPatterns = new[]
{
// Saludos (incluyendo variantes argentinas)
"hola", "buenas", "buen día", "buenos días", "buenas tardes", "buenas noches",
"buen dia", "buenos dias", "hi", "hello", "hey",
// Confirmaciones/Aceptación (argentinismos incluidos)
"ok", "perfecto", "genial", "bárbaro", "barbaro", "dale", "dale dale",
"está bien", "esta bien", "de acuerdo", "si", "sí", "vale", "listo",
"joya", "buenísimo", "buenisimo", "excelente",
// Agradecimientos
"gracias", "muchas gracias", "mil gracias", "thank you", "thanks",
// Despedidas
"chau", "chao", "adiós", "adios", "hasta luego", "nos vemos", "bye",
// Ayuda genérica
"ayuda", "help", "ayúdame", "ayudame",
// Negaciones simples
"no", "nada", "ninguna", "ninguno"
};
if (genericPatterns.Contains(normalized))
{
return false; // Skip AI - patrón genérico detectado
}
}
// ========== Por defecto: usar AI matching ==========
// Cualquier mensaje que no caiga en las reglas anteriores
// (más de 4 palabras, o contiene keywords específicas, o no está en patrones)
return true;
}
// NOTA: UpdateConversationSummaryAsync y GetIntentAsync han sido REMOVIDOS
// Su funcionalidad ahora está en la llamada unificada StreamMessageAsync
private async Task SaveConversationLogAsync(string userMessage, string botReply)
{