diff --git a/ChatbotApi/Services/ChatService.cs b/ChatbotApi/Services/ChatService.cs index 4ddb055..7563815 100644 --- a/ChatbotApi/Services/ChatService.cs +++ b/ChatbotApi/Services/ChatService.cs @@ -48,6 +48,14 @@ namespace ChatbotApi.Services _apiUrl = $"{baseUrl}{apiKey}"; } + // Response model for structured JSON from Gemini + private class GeminiStructuredResponse + { + public string intent { get; set; } = "NOTICIAS_PORTADA"; + public string reply { get; set; } = ""; + public string summary { get; set; } = ""; + } + public async IAsyncEnumerable StreamMessageAsync(ChatRequest request, [EnumeratorCancellation] CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(request?.Message)) @@ -58,17 +66,16 @@ namespace ChatbotApi.Services string safeUserMessage = SanitizeInput(request.Message); string context = ""; - string promptInstructions = ""; string? articleContext = null; string? errorMessage = null; - IntentType intent = IntentType.Homepage; - // [OPTIMIZACIÓN] Pre-carga de prompts del sistema en paralelo + // Pre-carga de prompts del sistema en paralelo var systemPromptsTask = GetActiveSystemPromptsAsync(); Task? articleTask = null; try { + // Load article if URL provided if (!string.IsNullOrEmpty(request.ContextUrl) && await UrlSecurity.IsSafeUrlAsync(request.ContextUrl)) { articleTask = GetArticleContentAsync(request.ContextUrl); @@ -76,151 +83,142 @@ namespace ChatbotApi.Services if (articleTask != null) articleContext = await articleTask; - intent = await GetIntentAsync(safeUserMessage, articleContext, request.ConversationSummary); - - // [FIX] Si la intención es 'Artículo' pero no hay contexto (el usuario pregunta sobre un tema específico sin abrir una nota), - // asumimos que quiere BUSCAR en la portada. - if (intent == IntentType.Article && string.IsNullOrEmpty(articleContext)) + // Build context based on heuristics + if (!string.IsNullOrEmpty(articleContext)) { - intent = IntentType.Homepage; + context = articleContext; + } + else + { + var articles = await GetWebsiteNewsAsync(_siteUrl, 50); + + if (request.ShownArticles != null && request.ShownArticles.Any()) + { + articles = articles.Where(a => !request.ShownArticles.Contains(a.Url)).ToList(); + } + + // Búsqueda Híbrida: local + AI fallback + var bestMatch = FindBestMatchingArticleLocal(safeUserMessage, articles); + + if (bestMatch == null) + { + bestMatch = await FindBestMatchingArticleAIAsync(safeUserMessage, articles, request.ConversationSummary); + } + + if (bestMatch != null && await UrlSecurity.IsSafeUrlAsync(bestMatch.Url)) + { + string rawContent = await GetArticleContentAsync(bestMatch.Url) ?? ""; + context = $"ARTÍCULO ENCONTRADO: {bestMatch.Title}\nURL: {bestMatch.Url}\n\nCONTENIDO:\n{SanitizeInput(rawContent)}"; + } + else + { + var sb = new StringBuilder(); + sb.AppendLine("NOTICIAS DISPONIBLES:"); + foreach (var article in articles.Take(15)) + { + sb.AppendLine($"- {article.Title} ({article.Url})"); + } + context = sb.ToString(); + } } - switch (intent) + // Add knowledge base if available + var knowledgeItems = await GetKnowledgeItemsAsync(); + if (knowledgeItems.Any()) { - case IntentType.Article: - context = articleContext ?? "No se pudo cargar el artículo."; - promptInstructions = "Responde la pregunta dentro de basándote ESTRICTA Y ÚNICAMENTE en la información dentro de ."; - break; - - case IntentType.KnowledgeBase: - var contextBuilder = new StringBuilder(); - - // [OPTIMIZACIÓN] Recolección de conocimiento en paralelo - var knowledgeTask = GetKnowledgeItemsAsync(); - var fuentesTask = GetFuentesDeContextoAsync(); - await Task.WhenAll(knowledgeTask, fuentesTask); - - foreach (var item in knowledgeTask.Result.Values) - { - contextBuilder.AppendLine($"- TEMA: {item.Descripcion}\n INFORMACIÓN: {item.Valor}"); - } - - foreach (var fuente in fuentesTask.Result) - { - if (await UrlSecurity.IsSafeUrlAsync(fuente.Url)) - { - contextBuilder.AppendLine($"\n--- {fuente.Nombre} ---"); - string scrapedContent = await ScrapeUrlContentAsync(fuente); - contextBuilder.AppendLine(SanitizeInput(scrapedContent)); - } - } - context = contextBuilder.ToString(); - promptInstructions = "Responde basándote ESTRICTA Y ÚNICAMENTE en la información proporcionada en ."; - break; - - default: - // No es necesario hacer scraping si solo vinculamos a la portada, - // pero la lógica mantiene el scraping de 50 items aquí. - // Podría optimizarse más, pero el scraping es rápido comparado con el LLM. - var articles = await GetWebsiteNewsAsync(_siteUrl, 50); - - if (request.ShownArticles != null && request.ShownArticles.Any()) - { - articles = articles - .Where(a => !request.ShownArticles.Contains(a.Url)) - .ToList(); - } - - // [OPTIMIZACIÓN] Búsqueda Híbrida: Intentamos localmente (rápido), si falla usamos IA (inteligente) - var bestMatch = FindBestMatchingArticleLocal(safeUserMessage, articles); - - if (bestMatch == null) - { - bestMatch = await FindBestMatchingArticleAIAsync(safeUserMessage, articles, request.ConversationSummary); - } - - if (bestMatch != null) - { - if (await UrlSecurity.IsSafeUrlAsync(bestMatch.Url)) - { - string rawContent = await GetArticleContentAsync(bestMatch.Url) ?? ""; - context = SanitizeInput(rawContent); - promptInstructions = $"La pregunta es sobre el artículo '{bestMatch.Title}'. Responde con un resumen conciso y ofrece el enlace: [{bestMatch.Title}]({bestMatch.Url})."; - } - } - else - { - // [OPTIMIZACIÓN] Limitamos a las 15 primeras para no saturar el contexto - var sb = new StringBuilder(); - foreach (var article in articles.Take(15)) sb.AppendLine($"- {article.Title} ({article.Url})"); - context = sb.ToString(); - promptInstructions = "Responde la pregunta del usuario. Si la pregunta es sobre algo mencionado en el , responde basándote en eso (por ejemplo, si pregunta 'dónde puedo leerla', proporciona el enlace mencionado anteriormente). Si la pregunta es sobre noticias actuales, selecciona las 3 más relevantes del , escribe una frase breve para cada una e INCLUYE el enlace con formato [Título](URL)."; - } - break; + var kbBuilder = new StringBuilder("\n\nBASE DE CONOCIMIENTO:"); + foreach (var item in knowledgeItems.Values) + { + kbBuilder.AppendLine($"\n- {item.Descripcion}: {item.Valor}"); + } + context += kbBuilder.ToString(); } } catch (Exception ex) { - _logger.LogError(ex, "Error procesando intención."); + _logger.LogError(ex, "Error construyendo contexto."); errorMessage = "Lo siento, hubo un problema técnico procesando tu solicitud."; } - yield return $"INTENT::{intent}"; - if (!string.IsNullOrEmpty(errorMessage)) { yield return errorMessage; yield break; } - Stream? responseStream = null; - var fullBotReply = new StringBuilder(); + // ========== UNIFIED API CALL ========== var httpClient = _httpClientFactory.CreateClient(); - httpClient.Timeout = TimeSpan.FromSeconds(30); + httpClient.Timeout = TimeSpan.FromSeconds(45); + + string? jsonText = null; try { - var promptBuilder = new StringBuilder(); var systemInstructions = !string.IsNullOrWhiteSpace(request.SystemPromptOverride) ? request.SystemPromptOverride - : await systemPromptsTask; // Esperar tarea precargada + : await systemPromptsTask; + // Build unified meta-prompt + var promptBuilder = new StringBuilder(); + promptBuilder.AppendLine(""); promptBuilder.AppendLine("Eres DiaBot, asistente virtual de El Día (La Plata, Argentina)."); + promptBuilder.AppendLine(); + promptBuilder.AppendLine("FORMATO DE RESPUESTA:"); + promptBuilder.AppendLine("Debes responder en formato JSON con esta estructura EXACTA:"); + promptBuilder.AppendLine("{\"intent\": \"...\", \"reply\": \"...\", \"summary\": \"...\"}"); + promptBuilder.AppendLine(); + + promptBuilder.AppendLine("INSTRUCCIONES GENERALES:"); promptBuilder.AppendLine(systemInstructions); - promptBuilder.AppendLine("IMPORTANTE:"); - promptBuilder.AppendLine("- NO uses formatos de email/carta ('Estimado/a', 'Atentamente')."); - promptBuilder.AppendLine("- NO saludes de nuevo si ya saludaste o si la pregunta es directa, ve al grano."); - promptBuilder.AppendLine("- Sé conciso, directo y natural."); - promptBuilder.AppendLine("- Si el usuario pregunta '¿algo más?' o '¿qué más?', asume que pide más noticias de la portada y no saludes."); - promptBuilder.AppendLine(promptInstructions); - - try - { - var timeInfo = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("America/Argentina/Buenos_Aires")); - promptBuilder.AppendLine($"Fecha y hora actual: {timeInfo:dd/MM/yyyy HH:mm}"); - } - catch { } - + promptBuilder.AppendLine("- NO uses formatos de email/carta ('Estimado/a', 'Atentamente')"); + promptBuilder.AppendLine("- NO saludes de nuevo si ya saludaste o si la pregunta es directa"); + promptBuilder.AppendLine("- Sé conciso, directo y natural"); + promptBuilder.AppendLine(); + + promptBuilder.AppendLine("--- REGLAS PARA CADA CAMPO JSON ---"); + promptBuilder.AppendLine(); + promptBuilder.AppendLine("1. 'intent': Clasifica la intención usando SOLO uno de estos valores:"); + promptBuilder.AppendLine(" - \"ARTICULO_ACTUAL\": Si la pregunta es sobre el tema del artículo en "); + promptBuilder.AppendLine(" - \"BASE_DE_CONOCIMIENTO\": Para preguntas sobre 'El Día' como empresa/organización"); + promptBuilder.AppendLine(" - \"NOTICIAS_PORTADA\": Para todo lo demás (este es el default si dudas)"); + promptBuilder.AppendLine(); + + promptBuilder.AppendLine("2. 'reply': Tu respuesta en texto Markdown para el usuario."); + promptBuilder.AppendLine(" - Si es un artículo específico: Resume brevemente e INCLUYE el enlace [Título](URL)"); + promptBuilder.AppendLine(" - Si son noticias generales: Selecciona las 3 más relevantes, breve frase c/u + enlace"); + promptBuilder.AppendLine(" - Si la pregunta refiere a algo del , úsalo (ej: 'dónde leerla' → dale el link)"); + promptBuilder.AppendLine(); + + promptBuilder.AppendLine("3. 'summary': Actualiza el historial de conversación."); + promptBuilder.AppendLine(" - Resume el intercambio actual (pregunta + respuesta) en 1-2 líneas"); + promptBuilder.AppendLine(" - Integra con el previo si existe"); + promptBuilder.AppendLine(" - Máximo 200 palabras para el resumen completo"); promptBuilder.AppendLine(""); + promptBuilder.AppendLine(); - // Incluir historial de conversación para referencias contextuales + // Conversation history if (!string.IsNullOrWhiteSpace(request.ConversationSummary)) { promptBuilder.AppendLine(""); promptBuilder.AppendLine(SanitizeInput(request.ConversationSummary)); promptBuilder.AppendLine(""); + promptBuilder.AppendLine(); } + // Context promptBuilder.AppendLine(""); promptBuilder.AppendLine(context); promptBuilder.AppendLine(""); + promptBuilder.AppendLine(); + // User question promptBuilder.AppendLine(""); promptBuilder.AppendLine(safeUserMessage); promptBuilder.AppendLine(""); - - promptBuilder.AppendLine("RESPUESTA:"); + promptBuilder.AppendLine(); + + promptBuilder.AppendLine("RESPUESTA (SOLO el JSON, sin comentarios adicionales):"); var requestData = new GeminiRequest { @@ -229,24 +227,30 @@ namespace ChatbotApi.Services SafetySettings = GetDefaultSafetySettings() }; - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _apiUrl) - { - Content = JsonContent.Create(requestData) - }; - - var response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + // Use non-streaming endpoint + var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?"); + + var response = await httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData, cancellationToken); + if (!response.IsSuccessStatusCode) { _logger.LogWarning("Error API Gemini: {StatusCode}", response.StatusCode); - throw new HttpRequestException("Error en proveedor de IA."); + throw new HttpRequestException($"Error en proveedor de IA: {response.StatusCode}"); } - responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + var geminiResponse = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + jsonText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim(); + + if (string.IsNullOrEmpty(jsonText)) + { + _logger.LogWarning("Respuesta vacía de Gemini"); + errorMessage = "Lo siento, hubo un problema al procesar la respuesta."; + } } catch (Exception ex) { - _logger.LogError(ex, "Error en stream."); - errorMessage = "Lo siento, servicio temporalmente no disponible."; + _logger.LogError(ex, "Error en llamada unificada a Gemini."); + errorMessage = "Lo siento, el servicio está temporalmente no disponible. Por favor, intenta de nuevo."; } if (!string.IsNullOrEmpty(errorMessage)) @@ -255,64 +259,100 @@ namespace ChatbotApi.Services yield break; } - if (responseStream != null) + // Parse JSON response (outside try-catch to allow yield) + GeminiStructuredResponse? apiResponse = null; + try { - await using (responseStream) - using (var reader = new StreamReader(responseStream)) + // Extract JSON from markdown code blocks if present + var jsonContent = jsonText!; + if (jsonText!.Contains("```json")) { - string? line; - while ((line = await reader.ReadLineAsync(cancellationToken)) != null) + var startIndex = jsonText.IndexOf("```json") + 7; + var endIndex = jsonText.IndexOf("```", startIndex); + if (endIndex > startIndex) { - if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue; - var jsonString = line.Substring(6); - - string? chunk = null; - try - { - var geminiResponse = JsonSerializer.Deserialize(jsonString); - chunk = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text; - } - catch (JsonException) { continue; } - - if (chunk != null) - { - fullBotReply.Append(chunk); - yield return chunk; - } + jsonContent = jsonText.Substring(startIndex, endIndex - startIndex).Trim(); } } + else if (jsonText.Contains("```")) + { + var startIndex = jsonText.IndexOf("```") + 3; + var endIndex = jsonText.IndexOf("```", startIndex); + if (endIndex > startIndex) + { + jsonContent = jsonText.Substring(startIndex, endIndex - startIndex).Trim(); + } + } + + apiResponse = JsonSerializer.Deserialize(jsonContent, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to parse Gemini JSON. Raw response: {JsonText}", jsonText); + } + + if (apiResponse == null || string.IsNullOrEmpty(apiResponse.reply)) + { + yield return "Lo siento, tuve un problema al procesar la respuesta. Por favor, intenta de nuevo."; + yield break; + } + + // Send intent metadata + yield return $"INTENT::{apiResponse.intent}"; + + // Simulate streaming by chunking the reply + string fullReply = apiResponse.reply; + var words = fullReply.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var chunkBuilder = new StringBuilder(); + + foreach (var word in words) + { + chunkBuilder.Append(word + " "); + + // Send chunk every ~20 characters for smooth streaming + if (chunkBuilder.Length >= 20) + { + yield return chunkBuilder.ToString(); + chunkBuilder.Clear(); + await Task.Delay(30, cancellationToken); + } } - if (fullBotReply.Length > 0) + // Send any remaining text + if (chunkBuilder.Length > 0) { - // [OPTIMIZACIÓN] Logging "fire-and-forget" (BD) - _ = Task.Run(async () => - { - using (var scope = _serviceProvider.CreateScope()) - { - var db = scope.ServiceProvider.GetRequiredService(); - try - { - db.ConversacionLogs.Add(new ConversacionLog - { - UsuarioMensaje = safeUserMessage, - BotRespuesta = fullBotReply.ToString(), - Fecha = DateTime.UtcNow - }); - await db.SaveChangesAsync(); - } - catch(Exception ex) - { - var logger = scope.ServiceProvider.GetRequiredService>(); - logger.LogError(ex, "Error in background logging"); - } - } - }); - - // [IMPORTANTE] El resumen del contexto debe permanecer en primer plano para informar al cliente - var newSummary = await UpdateConversationSummaryAsync(request.ConversationSummary, safeUserMessage, fullBotReply.ToString()); - yield return $"SUMMARY::{newSummary}"; + yield return chunkBuilder.ToString(); } + + // Log conversation (fire-and-forget) + _ = Task.Run(async () => + { + using (var scope = _serviceProvider.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + try + { + db.ConversacionLogs.Add(new ConversacionLog + { + UsuarioMensaje = safeUserMessage, + BotRespuesta = fullReply, + Fecha = DateTime.UtcNow + }); + await db.SaveChangesAsync(); + } + catch(Exception ex) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "Error in background logging"); + } + } + }); + + // Send summary + yield return $"SUMMARY::{apiResponse.summary}"; } // --- PRIVATE METHODS --- @@ -350,107 +390,13 @@ namespace ChatbotApi.Services }; } - private async Task UpdateConversationSummaryAsync(string? oldSummary, string userMessage, string botResponse) - { - string safeOldSummary = SanitizeInput(oldSummary ?? "Esta es una nueva conversación."); - string safeUserMsg = SanitizeInput(userMessage); - string safeBotMsg = SanitizeInput(new string(botResponse.Take(300).ToArray())); - - var promptBuilder = new StringBuilder(); - promptBuilder.AppendLine("Tu tarea es actualizar un resumen de conversación. Basado en el y el , crea un nuevo resumen conciso."); - promptBuilder.AppendLine($"{safeOldSummary}"); - promptBuilder.AppendLine(""); - promptBuilder.AppendLine($"Usuario: {safeUserMsg}"); - promptBuilder.AppendLine($"Bot: {safeBotMsg}..."); - promptBuilder.AppendLine(""); - promptBuilder.AppendLine("\nResponde SOLO con el nuevo resumen."); - - var requestData = new GeminiRequest - { - Contents = new[] { new Content { Parts = new[] { new Part { Text = promptBuilder.ToString() } } } }, - SafetySettings = GetDefaultSafetySettings() - }; - - var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?"); - var httpClient = _httpClientFactory.CreateClient(); - - try - { - var response = await httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData); - if (!response.IsSuccessStatusCode) return safeOldSummary; - var geminiResponse = await response.Content.ReadFromJsonAsync(); - var newSummary = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim(); - return newSummary ?? safeOldSummary; - } - catch (Exception ex) - { - _logger.LogError(ex, "Excepción en UpdateConversationSummaryAsync."); - return safeOldSummary; - } - } - - private async Task GetIntentAsync(string userMessage, string? activeArticleContent, string? conversationSummary) - { - string safeUserMsg = SanitizeInput(userMessage); - string safeSummary = SanitizeInput(conversationSummary); - string safeArticle = SanitizeInput(new string((activeArticleContent ?? "").Take(1000).ToArray())); - - var promptBuilder = new StringBuilder(); - promptBuilder.AppendLine("Actúa como un router de intenciones. Analiza la y decide qué fuente de información usar."); - promptBuilder.AppendLine("Categorías posibles: [ARTICULO_ACTUAL], [BASE_DE_CONOCIMIENTO], [NOTICIAS_PORTADA]."); - - if (!string.IsNullOrWhiteSpace(safeSummary)) - promptBuilder.AppendLine($"{safeSummary}"); - - if (!string.IsNullOrEmpty(safeArticle)) - promptBuilder.AppendLine($"{safeArticle}..."); - - promptBuilder.AppendLine("\n--- CRITERIOS DE DECISIÓN ESTRICTOS ---"); - promptBuilder.AppendLine("1. [ARTICULO_ACTUAL]: SOLO si la pregunta es sobre el MISMO TEMA del ."); - promptBuilder.AppendLine(" Ejemplos: '¿qué más dice?', 'cuándo pasó?', 'quién es?', 'dame detalles'."); - promptBuilder.AppendLine(" IMPORTANTE: Si la pregunta menciona un tema DIFERENTE al artículo, NO uses esta categoría."); - promptBuilder.AppendLine(""); - promptBuilder.AppendLine("2. [NOTICIAS_PORTADA]: Si la pregunta es sobre:"); - promptBuilder.AppendLine(" - Noticias generales ('¿qué hay?', '¿algo más?', 'novedades')"); - promptBuilder.AppendLine(" - Un tema DIFERENTE al del artículo actual"); - promptBuilder.AppendLine(" - Cualquier tema que NO esté en el "); - promptBuilder.AppendLine(""); - promptBuilder.AppendLine("3. [BASE_DE_CONOCIMIENTO]: Solo para preguntas sobre el diario 'El Día' como empresa/organización."); - promptBuilder.AppendLine($"\n{safeUserMsg}"); - promptBuilder.AppendLine("\nResponde ÚNICAMENTE con el nombre de la categoría entre corchetes. Si hay duda, usa [NOTICIAS_PORTADA]."); - - var requestData = new GeminiRequest - { - Contents = new[] { new Content { Parts = new[] { new Part { Text = promptBuilder.ToString() } } } }, - SafetySettings = GetDefaultSafetySettings() - }; - - var nonStreamingApiUrl = _apiUrl.Replace(":streamGenerateContent?alt=sse&", ":generateContent?"); - var httpClient = _httpClientFactory.CreateClient(); - - try - { - var response = await httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData); - if (!response.IsSuccessStatusCode) return IntentType.Homepage; - var geminiResponse = await response.Content.ReadFromJsonAsync(); - var responseText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim() ?? ""; - - if (responseText.Contains("ARTICULO_ACTUAL")) return IntentType.Article; - if (responseText.Contains("BASE_DE_CONOCIMIENTO")) return IntentType.KnowledgeBase; - return IntentType.Homepage; - } - catch (Exception ex) - { - _logger.LogError(ex, "Excepción en GetIntentAsync."); - return IntentType.Homepage; - } - } + // NOTE: UpdateConversationSummaryAsync and GetIntentAsync have been REMOVED + // Their functionality is now in the unified StreamMessageAsync call private async Task SaveConversationLogAsync(string userMessage, string botReply) { try { - // usamos dbContext injectado (Scoped) directamente _dbContext.ConversacionLogs.Add(new ConversacionLog { UsuarioMensaje = userMessage, @@ -517,13 +463,11 @@ namespace ChatbotApi.Services var titleTerms = Tokenize(article.Title); double score = CalculateJaccardSimilarity(userTerms, titleTerms); - // Boost: Palabras clave compartidas (longitud > 3) if (userTerms.Intersect(titleTerms).Any(t => t.Length > 3)) { score += 0.2; } - // Aumentar puntaje si los términos son consecutivos en el título (coincidencia de frase) if (article.Title.IndexOf(userMessage, StringComparison.OrdinalIgnoreCase) >= 0) { score += 0.5; @@ -536,7 +480,6 @@ namespace ChatbotApi.Services } } - // Umbral mínimo de relevancia: Reducido a 0.05 para capturar coincidencias de una sola palabra en títulos largos return maxScore >= 0.05 ? bestMatch : null; } @@ -590,7 +533,7 @@ namespace ChatbotApi.Services return normalizedText .Split() .Select(x => x.Trim(punctuation)) - .Where(x => x.Length > 2) // ignorar palabras muy cortas + .Where(x => x.Length > 2) .ToHashSet(); } diff --git a/ChatbotApi/appsettings.json b/ChatbotApi/appsettings.json index b8f791d..3c009ae 100644 --- a/ChatbotApi/appsettings.json +++ b/ChatbotApi/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "Gemini": { - "GeminiApiUrl": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse&key=", + "GeminiApiUrl": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent?alt=sse&key=", "GeminiApiKey": "" }, "ConnectionStrings": {},