diff --git a/ChatbotApi/Services/ChatService.cs b/ChatbotApi/Services/ChatService.cs index 7563815..c371a96 100644 --- a/ChatbotApi/Services/ChatService.cs +++ b/ChatbotApi/Services/ChatService.cs @@ -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(""); @@ -197,7 +206,7 @@ namespace ChatbotApi.Services promptBuilder.AppendLine(""); promptBuilder.AppendLine(); - // Conversation history + // Historial de conversación if (!string.IsNullOrWhiteSpace(request.ConversationSummary)) { promptBuilder.AppendLine(""); @@ -206,13 +215,13 @@ namespace ChatbotApi.Services promptBuilder.AppendLine(); } - // Context + // Contexto promptBuilder.AppendLine(""); promptBuilder.AppendLine(context); promptBuilder.AppendLine(""); promptBuilder.AppendLine(); - // User question + // Pregunta del usuario promptBuilder.AppendLine(""); promptBuilder.AppendLine(safeUserMessage); promptBuilder.AppendLine(""); @@ -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>(); - 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 + /// + /// 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. + /// + 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) { diff --git a/chatbot-admin/src/components/SourceManager.tsx b/chatbot-admin/src/components/SourceManager.tsx index 8aecb51..bf87a4d 100644 --- a/chatbot-admin/src/components/SourceManager.tsx +++ b/chatbot-admin/src/components/SourceManager.tsx @@ -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 = ({ onAuthError }) => { icon={} 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 />, ], }, diff --git a/chatbot-admin/src/components/SystemPromptManager.tsx b/chatbot-admin/src/components/SystemPromptManager.tsx index cfff351..35ab36e 100644 --- a/chatbot-admin/src/components/SystemPromptManager.tsx +++ b/chatbot-admin/src/components/SystemPromptManager.tsx @@ -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';