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}";
|
_apiUrl = $"{baseUrl}{apiKey}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response model for structured JSON from Gemini
|
// Modelo de respuesta para JSON estructurado de Gemini
|
||||||
private class GeminiStructuredResponse
|
private class GeminiStructuredResponse
|
||||||
{
|
{
|
||||||
public string intent { get; set; } = "NOTICIAS_PORTADA";
|
public string intent { get; set; } = "NOTICIAS_PORTADA";
|
||||||
@@ -75,7 +75,7 @@ namespace ChatbotApi.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Load article if URL provided
|
// Cargar artículo si se proporciona URL
|
||||||
if (!string.IsNullOrEmpty(request.ContextUrl) && await UrlSecurity.IsSafeUrlAsync(request.ContextUrl))
|
if (!string.IsNullOrEmpty(request.ContextUrl) && await UrlSecurity.IsSafeUrlAsync(request.ContextUrl))
|
||||||
{
|
{
|
||||||
articleTask = GetArticleContentAsync(request.ContextUrl);
|
articleTask = GetArticleContentAsync(request.ContextUrl);
|
||||||
@@ -83,7 +83,7 @@ namespace ChatbotApi.Services
|
|||||||
|
|
||||||
if (articleTask != null) articleContext = await articleTask;
|
if (articleTask != null) articleContext = await articleTask;
|
||||||
|
|
||||||
// Build context based on heuristics
|
// Construir contexto basado en heurísticas
|
||||||
if (!string.IsNullOrEmpty(articleContext))
|
if (!string.IsNullOrEmpty(articleContext))
|
||||||
{
|
{
|
||||||
context = articleContext;
|
context = articleContext;
|
||||||
@@ -101,9 +101,18 @@ namespace ChatbotApi.Services
|
|||||||
var bestMatch = FindBestMatchingArticleLocal(safeUserMessage, articles);
|
var bestMatch = FindBestMatchingArticleLocal(safeUserMessage, articles);
|
||||||
|
|
||||||
if (bestMatch == null)
|
if (bestMatch == null)
|
||||||
|
{
|
||||||
|
// 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);
|
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))
|
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();
|
var knowledgeItems = await GetKnowledgeItemsAsync();
|
||||||
if (knowledgeItems.Any())
|
if (knowledgeItems.Any())
|
||||||
{
|
{
|
||||||
@@ -146,7 +155,7 @@ namespace ChatbotApi.Services
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== UNIFIED API CALL ==========
|
// ========== LLAMADA API UNIFICADA ==========
|
||||||
var httpClient = _httpClientFactory.CreateClient();
|
var httpClient = _httpClientFactory.CreateClient();
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(45);
|
httpClient.Timeout = TimeSpan.FromSeconds(45);
|
||||||
|
|
||||||
@@ -158,7 +167,7 @@ namespace ChatbotApi.Services
|
|||||||
? request.SystemPromptOverride
|
? request.SystemPromptOverride
|
||||||
: await systemPromptsTask;
|
: await systemPromptsTask;
|
||||||
|
|
||||||
// Build unified meta-prompt
|
// Construir meta-prompt unificado
|
||||||
var promptBuilder = new StringBuilder();
|
var promptBuilder = new StringBuilder();
|
||||||
|
|
||||||
promptBuilder.AppendLine("<instrucciones_sistema>");
|
promptBuilder.AppendLine("<instrucciones_sistema>");
|
||||||
@@ -197,7 +206,7 @@ namespace ChatbotApi.Services
|
|||||||
promptBuilder.AppendLine("</instrucciones_sistema>");
|
promptBuilder.AppendLine("</instrucciones_sistema>");
|
||||||
promptBuilder.AppendLine();
|
promptBuilder.AppendLine();
|
||||||
|
|
||||||
// Conversation history
|
// Historial de conversación
|
||||||
if (!string.IsNullOrWhiteSpace(request.ConversationSummary))
|
if (!string.IsNullOrWhiteSpace(request.ConversationSummary))
|
||||||
{
|
{
|
||||||
promptBuilder.AppendLine("<historial_conversacion>");
|
promptBuilder.AppendLine("<historial_conversacion>");
|
||||||
@@ -206,13 +215,13 @@ namespace ChatbotApi.Services
|
|||||||
promptBuilder.AppendLine();
|
promptBuilder.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context
|
// Contexto
|
||||||
promptBuilder.AppendLine("<contexto>");
|
promptBuilder.AppendLine("<contexto>");
|
||||||
promptBuilder.AppendLine(context);
|
promptBuilder.AppendLine(context);
|
||||||
promptBuilder.AppendLine("</contexto>");
|
promptBuilder.AppendLine("</contexto>");
|
||||||
promptBuilder.AppendLine();
|
promptBuilder.AppendLine();
|
||||||
|
|
||||||
// User question
|
// Pregunta del usuario
|
||||||
promptBuilder.AppendLine("<pregunta_usuario>");
|
promptBuilder.AppendLine("<pregunta_usuario>");
|
||||||
promptBuilder.AppendLine(safeUserMessage);
|
promptBuilder.AppendLine(safeUserMessage);
|
||||||
promptBuilder.AppendLine("</pregunta_usuario>");
|
promptBuilder.AppendLine("</pregunta_usuario>");
|
||||||
@@ -227,7 +236,7 @@ namespace ChatbotApi.Services
|
|||||||
SafetySettings = GetDefaultSafetySettings()
|
SafetySettings = GetDefaultSafetySettings()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use non-streaming endpoint
|
// Usar endpoint sin streaming
|
||||||
var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?");
|
var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?");
|
||||||
|
|
||||||
var response = await httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData, cancellationToken);
|
var response = await httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData, cancellationToken);
|
||||||
@@ -259,11 +268,11 @@ namespace ChatbotApi.Services
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON response (outside try-catch to allow yield)
|
// Parsear respuesta JSON (fuera del try-catch para permitir yield)
|
||||||
GeminiStructuredResponse? apiResponse = null;
|
GeminiStructuredResponse? apiResponse = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Extract JSON from markdown code blocks if present
|
// Extraer JSON de bloques de código markdown si están presentes
|
||||||
var jsonContent = jsonText!;
|
var jsonContent = jsonText!;
|
||||||
if (jsonText!.Contains("```json"))
|
if (jsonText!.Contains("```json"))
|
||||||
{
|
{
|
||||||
@@ -291,7 +300,7 @@ namespace ChatbotApi.Services
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
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))
|
if (apiResponse == null || string.IsNullOrEmpty(apiResponse.reply))
|
||||||
@@ -300,10 +309,10 @@ namespace ChatbotApi.Services
|
|||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send intent metadata
|
// Enviar metadata de intención
|
||||||
yield return $"INTENT::{apiResponse.intent}";
|
yield return $"INTENT::{apiResponse.intent}";
|
||||||
|
|
||||||
// Simulate streaming by chunking the reply
|
// Simular streaming dividiendo la respuesta en fragmentos
|
||||||
string fullReply = apiResponse.reply;
|
string fullReply = apiResponse.reply;
|
||||||
var words = fullReply.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
var words = fullReply.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
var chunkBuilder = new StringBuilder();
|
var chunkBuilder = new StringBuilder();
|
||||||
@@ -312,7 +321,7 @@ namespace ChatbotApi.Services
|
|||||||
{
|
{
|
||||||
chunkBuilder.Append(word + " ");
|
chunkBuilder.Append(word + " ");
|
||||||
|
|
||||||
// Send chunk every ~20 characters for smooth streaming
|
// Enviar fragmento cada ~20 caracteres para streaming fluido
|
||||||
if (chunkBuilder.Length >= 20)
|
if (chunkBuilder.Length >= 20)
|
||||||
{
|
{
|
||||||
yield return chunkBuilder.ToString();
|
yield return chunkBuilder.ToString();
|
||||||
@@ -321,13 +330,13 @@ namespace ChatbotApi.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send any remaining text
|
// Enviar cualquier texto restante
|
||||||
if (chunkBuilder.Length > 0)
|
if (chunkBuilder.Length > 0)
|
||||||
{
|
{
|
||||||
yield return chunkBuilder.ToString();
|
yield return chunkBuilder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log conversation (fire-and-forget)
|
// Registrar conversación (fire-and-forget)
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
using (var scope = _serviceProvider.CreateScope())
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
@@ -346,16 +355,16 @@ namespace ChatbotApi.Services
|
|||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<ChatService>>();
|
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}";
|
yield return $"SUMMARY::{apiResponse.summary}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PRIVATE METHODS ---
|
// --- MÉTODOS PRIVADOS ---
|
||||||
|
|
||||||
private string SanitizeInput(string? input)
|
private string SanitizeInput(string? input)
|
||||||
{
|
{
|
||||||
@@ -390,8 +399,99 @@ namespace ChatbotApi.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: UpdateConversationSummaryAsync and GetIntentAsync have been REMOVED
|
/// <summary>
|
||||||
// Their functionality is now in the unified StreamMessageAsync call
|
/// 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)
|
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 React, { useState, useEffect, useCallback } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
|
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
|
||||||
@@ -143,7 +143,7 @@ const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
|
|||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
label="Eliminar"
|
label="Eliminar"
|
||||||
onClick={() => handleDeleteClick(params.id as number)}
|
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';
|
} from '@mui/material';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
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 AddIcon from '@mui/icons-material/Add';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
import apiClient from '../api/apiClient';
|
import apiClient from '../api/apiClient';
|
||||||
|
|||||||
Reference in New Issue
Block a user