Feat: Detección de Mensajes Genéricos (1 Llamada a la API)
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// EN: src/components/SourceManager.tsx
|
||||
// ChatBot\chatbot-admin\src\components\SourceManager.tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
|
||||
@@ -143,7 +143,7 @@ const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
|
||||
icon={<DeleteIcon />}
|
||||
label="Eliminar"
|
||||
onClick={() => handleDeleteClick(params.id as number)}
|
||||
color="inherit" // Keeping it generic to avoid type errors
|
||||
color="inherit" // Mantener genérico para evitar errores de tipo
|
||||
/>,
|
||||
],
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; // Iconos para "Test"
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; // Íconos para "Test"
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import apiClient from '../api/apiClient';
|
||||
|
||||
Reference in New Issue
Block a user