using ChatApi; using ChatApi.Models; using ChatRAG.Contracts.VectorSearch; using ChatRAG.Data; using ChatRAG.Models; using ChatRAG.Services.Contracts; using ChatRAG.Services.Confidence; using ChatRAG.Services.PromptConfiguration; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using System.Text.Json; using ChatRAG.Settings; using Microsoft.Extensions.Options; #pragma warning disable SKEXP0001 namespace ChatRAG.Services.ResponseService { public class ConfidenceAwareRAGService : IResponseService { private readonly ChatHistoryService _chatHistoryService; private readonly Kernel _kernel; private readonly TextFilter _textFilter; private readonly IProjectDataRepository _projectDataRepository; private readonly IChatCompletionService _chatCompletionService; private readonly IVectorSearchService _vectorSearchService; private readonly ILogger _logger; private readonly ConfidenceVerifier _confidenceVerifier; private readonly PromptConfigurationService _promptService; private readonly ConfidenceAwareSettings _settings; public ConfidenceAwareRAGService( ChatHistoryService chatHistoryService, Kernel kernel, TextFilter textFilter, IProjectDataRepository projectDataRepository, IChatCompletionService chatCompletionService, IVectorSearchService vectorSearchService, ILogger logger, ConfidenceVerifier confidenceVerifier, PromptConfigurationService promptService, IOptions settings) { _chatHistoryService = chatHistoryService; _kernel = kernel; _textFilter = textFilter; _projectDataRepository = projectDataRepository; _chatCompletionService = chatCompletionService; _vectorSearchService = vectorSearchService; _logger = logger; _confidenceVerifier = confidenceVerifier; _promptService = promptService; _settings = settings.Value; } public async Task GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt") { var stopWatch = new System.Diagnostics.Stopwatch(); stopWatch.Start(); string detectedLanguage = language; try { detectedLanguage = _settings.Languages.AutoDetectLanguage ? _promptService.DetectLanguage(question) : language; var projectData = await _projectDataRepository.GetAsync(projectId); var detectedDomain = _promptService.DetectDomain(question, projectData?.Descricao); var prompts = _promptService.GetPrompts(detectedDomain, detectedLanguage); var queryAnalysis = await AnalyzeQuery(question, detectedLanguage, prompts.QueryAnalysis); var context = await ExecuteHierarchicalSearch(question, projectId, queryAnalysis, prompts, detectedLanguage); var confidenceResult = await VerifyConfidenceIfEnabled(queryAnalysis, context, projectId, detectedLanguage); if (!confidenceResult.ShouldRespond) { stopWatch.Stop(); var fallbackResponse = confidenceResult.SuggestedResponse ?? GetGenericFallbackMessage(detectedLanguage); return FormatFinalResponse(fallbackResponse, stopWatch.ElapsedMilliseconds, context.Steps.Count, confidenceResult); } var response = await GenerateResponse(question, projectId, context, sessionId, detectedLanguage, prompts.Response, detectedDomain); stopWatch.Stop(); return FormatFinalResponse(response, stopWatch.ElapsedMilliseconds, context.Steps.Count, confidenceResult); } catch (Exception ex) { _logger.LogError(ex, "Erro no ConfidenceAwareRAG"); stopWatch.Stop(); var errorMessage = detectedLanguage == "en" ? $"Error: {ex.Message}" : $"Erro: {ex.Message}"; return $"{errorMessage}\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s"; } } private string GetGenericFallbackMessage(string language) { return language == "en" ? "I don't have enough information to respond safely. Could you try rephrasing the question?" : "Não tenho informações suficientes para responder com segurança. Pode tentar reformular a pergunta?"; } private async Task AnalyzeQuery(string question, string language, string promptTemplate) { var prompt = string.Format(promptTemplate, question); var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, new OpenAIPromptExecutionSettings { Temperature = 0.1, MaxTokens = 300 }); try { var jsonResponse = response.Content?.Trim() ?? "{}"; var startIndex = jsonResponse.IndexOf('{'); var endIndex = jsonResponse.LastIndexOf('}'); if (startIndex >= 0 && endIndex >= startIndex) jsonResponse = jsonResponse.Substring(startIndex, endIndex - startIndex + 1); var analysis = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); return analysis ?? new QueryAnalysis { Strategy = "specific", Complexity = "medium" }; } catch (Exception ex) { _logger.LogWarning(ex, "Erro ao parsear análise da query, usando padrão"); return new QueryAnalysis { Strategy = "specific", Complexity = "medium" }; } } private async Task ExecuteHierarchicalSearch(string question, string projectId, QueryAnalysis analysis, PromptTemplates prompts, string language) { var context = new HierarchicalContext(); var embeddingService = _kernel.GetRequiredService(); context.Metadata["DetectedLanguage"] = language; context.Metadata["SearchResults"] = new List(); switch (analysis.Strategy) { case "overview": await ExecuteOverviewStrategy(context, question, projectId, embeddingService, prompts); break; case "detailed": await ExecuteDetailedStrategy(context, question, projectId, embeddingService, analysis, prompts); break; default: await ExecuteSpecificStrategy(context, question, projectId, embeddingService, prompts); break; } return context; } private async Task VerifyConfidenceIfEnabled(QueryAnalysis analysis, HierarchicalContext context, string projectId, string language) { if (!_settings.EnableConfidenceCheck) { return new ConfidenceResult { ShouldRespond = true, ConfidenceScore = 1.0, Reason = language == "en" ? "Confidence check disabled" : "Verificação de confiança desabilitada" }; } var results = ExtractResultsFromContext(context, projectId); return _confidenceVerifier.VerifyConfidence(analysis, results, context, _settings.UseStrictMode, language); } private List ExtractResultsFromContext(HierarchicalContext context, string projectId) { if (context.Metadata.ContainsKey("SearchResults") && context.Metadata["SearchResults"] is List storedResults) return storedResults; var results = new List(); var contextLength = context.CombinedContext?.Length ?? 0; if (contextLength > 0) { var estimatedDocuments = Math.Max(1, Math.Min(10, contextLength / 500)); for (int i = 0; i < estimatedDocuments; i++) { results.Add(new VectorSearchResult { Id = $"estimated_doc_{i}", Score = Math.Max(0.1, 0.8 - (i * 0.1)), Content = "Conteúdo estimado do contexto", Title = $"Documento estimado {i + 1}", ProjectId = projectId ?? "unknown" }); } } return results; } private async Task ExecuteOverviewStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, PromptTemplates prompts) { context.AddStep("Buscando todos os documentos do projeto"); var allProjectDocs = await _vectorSearchService.GetDocumentsByProjectAsync(projectId); StoreSearchResults(context, allProjectDocs); var requirementsDocs = allProjectDocs.Where(d => d.Title.ToLower().Contains("requisito") || d.Content.ToLower().Contains("requisito")).ToList(); var architectureDocs = allProjectDocs.Where(d => d.Title.ToLower().Contains("arquitetura") || d.Content.ToLower().Contains("arquitetura")).ToList(); var otherDocs = allProjectDocs.Except(requirementsDocs).Except(architectureDocs).ToList(); context.AddStep("Resumindo documentos por categoria"); var requirementsSummary = await SummarizeDocuments(requirementsDocs, "requisitos", prompts.Summary); var architectureSummary = await SummarizeDocuments(architectureDocs, "arquitetura", prompts.Summary); var otherSummary = await SummarizeDocuments(otherDocs, "outros documentos", prompts.Summary); var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question)); var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray(); var relevantDocs = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 8); AddToSearchResults(context, relevantDocs); var contextParts = new List(); if (!string.IsNullOrEmpty(requirementsSummary)) contextParts.Add($"RESUMO DOS REQUISITOS:\n{requirementsSummary}"); if (!string.IsNullOrEmpty(architectureSummary)) contextParts.Add($"RESUMO DA ARQUITETURA:\n{architectureSummary}"); if (!string.IsNullOrEmpty(otherSummary)) contextParts.Add($"OUTROS DOCUMENTOS:\n{otherSummary}"); contextParts.Add($"DOCUMENTOS RELEVANTES:\n{FormatResults(relevantDocs)}"); context.CombinedContext = string.Join("\n\n", contextParts); } private async Task ExecuteSpecificStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, PromptTemplates prompts) { var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question)); var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray(); var initialResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.4, 3); StoreSearchResults(context, initialResults); if (initialResults.Any()) { var expandedContext = await ExpandContext(initialResults, projectId, embeddingService); AddToSearchResults(context, expandedContext); context.CombinedContext = $"CONTEXTO PRINCIPAL:\n{FormatResults(initialResults)}\n\nCONTEXTO EXPANDIDO:\n{FormatResults(expandedContext)}"; } else { var fallbackResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.2, 5); StoreSearchResults(context, fallbackResults); context.CombinedContext = FormatResults(fallbackResults); } } private async Task ExecuteDetailedStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, QueryAnalysis analysis, PromptTemplates prompts) { var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question)); var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray(); var conceptualResults = new List(); if (analysis.Concepts?.Any() == true) { foreach (var concept in analysis.Concepts) { var conceptEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(concept)); var conceptArray = conceptEmbedding.ToArray().Select(e => (double)e).ToArray(); var conceptResults = await _vectorSearchService.SearchSimilarAsync(conceptArray, projectId, 0.3, 2); conceptualResults.AddRange(conceptResults); } } var directResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 3); var allResults = conceptualResults.Concat(directResults).DistinctBy(r => r.Id).ToList(); StoreSearchResults(context, allResults); var intermediateContext = FormatResults(allResults); var gaps = await IdentifyKnowledgeGaps(question, intermediateContext, prompts.GapAnalysis); if (!string.IsNullOrEmpty(gaps)) { var gapEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(gaps)); var gapArray = gapEmbedding.ToArray().Select(e => (double)e).ToArray(); var gapResults = await _vectorSearchService.SearchSimilarAsync(gapArray, projectId, 0.25, 2); AddToSearchResults(context, gapResults); context.CombinedContext = $"CONTEXTO CONCEITUAL:\n{FormatResults(conceptualResults)}\n\nCONTEXTO DIRETO:\n{FormatResults(directResults)}\n\nCONTEXTO COMPLEMENTAR:\n{FormatResults(gapResults)}"; } else { context.CombinedContext = $"CONTEXTO CONCEITUAL:\n{FormatResults(conceptualResults)}\n\nCONTEXTO DIRETO:\n{FormatResults(directResults)}"; } } private async Task GenerateResponse(string question, string projectId, HierarchicalContext context, string sessionId, string language, string promptTemplate, string? domain) { var projectData = await _projectDataRepository.GetAsync(projectId); var project = $"Nome: {projectData.Nome}\nDescrição: {projectData.Descricao}"; if (!string.IsNullOrEmpty(domain)) project += $"\nDomínio: {domain}"; var finalPrompt = string.Format(promptTemplate, project, question, context.CombinedContext, string.Join(" → ", context.Steps)); var history = _chatHistoryService.GetSumarizer(sessionId); history.AddUserMessage(finalPrompt); var response = await _chatCompletionService.GetChatMessageContentAsync(history, new OpenAIPromptExecutionSettings { Temperature = 0.6 }); history.AddMessage(response.Role, response.Content ?? ""); _chatHistoryService.UpdateHistory(sessionId, history); return response.Content ?? ""; } private string FormatFinalResponse(string response, long milliseconds, int steps, ConfidenceResult? confidence = null) { var result = response; if (_settings.ShowDebugInfo) { result += $"\n\n📊 **Debug Info:**\n⏱️ Tempo: {milliseconds / 1000}s\n🔍 Etapas: {steps}"; if (confidence != null) { result += $"\n🎯 Confiança: {confidence.ConfidenceScore:P1}\n📋 Estratégia: {confidence.Strategy}"; result += $"\n✅ Deve responder: {(confidence.ShouldRespond ? "Sim" : "Não")}"; if (!string.IsNullOrEmpty(confidence.Reason)) result += $"\n💭 Motivo: {confidence.Reason}"; } } return result; } private string FormatResults(IEnumerable results) { return string.Join("\n\n", results.Select((item, index) => $"=== DOCUMENTO {index + 1} ===\nRelevância: {item.Score:P1}\nConteúdo: {item.Content}")); } private void StoreSearchResults(HierarchicalContext context, List results) { if (!context.Metadata.ContainsKey("SearchResults")) context.Metadata["SearchResults"] = new List(); ((List)context.Metadata["SearchResults"]).AddRange(results); } private void AddToSearchResults(HierarchicalContext context, List additionalResults) { if (context.Metadata.ContainsKey("SearchResults")) { var storedResults = (List)context.Metadata["SearchResults"]; storedResults.AddRange(additionalResults.Where(r => !storedResults.Any(sr => sr.Id == r.Id))); } else StoreSearchResults(context, additionalResults); } private async Task> ExpandContext(List initialResults, string projectId, ITextEmbeddingGenerationService embeddingService) { var expandedResults = new List(); foreach (var result in initialResults.Take(2)) { var resultEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(result.Content)); var embeddingArray = resultEmbedding.ToArray().Select(e => (double)e).ToArray(); var relatedDocs = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.4, 2); expandedResults.AddRange(relatedDocs.Where(r => !initialResults.Any(ir => ir.Id == r.Id))); } return expandedResults.DistinctBy(r => r.Id).ToList(); } private async Task SummarizeDocuments(List documents, string category, string promptTemplate) { if (!documents.Any()) return string.Empty; if (documents.Count <= 3) return FormatResults(documents); var chunks = documents.Chunk(5).ToList(); var tasks = chunks.Select(async chunk => { try { var prompt = string.Format(promptTemplate, category, FormatResults(chunk)); var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, new OpenAIPromptExecutionSettings { Temperature = 0.1, MaxTokens = 300 }); return response.Content ?? string.Empty; } catch { return FormatResults(chunk); } }); var summaries = await Task.WhenAll(tasks); var validSummaries = summaries.Where(s => !string.IsNullOrEmpty(s)).ToList(); return validSummaries.Count > 1 ? string.Join("\n\n", validSummaries) : validSummaries.FirstOrDefault() ?? string.Empty; } private async Task IdentifyKnowledgeGaps(string question, string currentContext, string promptTemplate) { var prompt = string.Format(promptTemplate, question, currentContext.Substring(0, Math.Min(1000, currentContext.Length))); var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, new OpenAIPromptExecutionSettings { Temperature = 0.2, MaxTokens = 100 }); var gaps = response.Content?.Trim() ?? ""; return gaps.Equals("SUFICIENTE", StringComparison.OrdinalIgnoreCase) ? "" : gaps; } public Task GetResponse(UserData userData, string projectId, string sessionId, string question) { return GetResponse(userData, projectId, sessionId, question, "pt"); } } } #pragma warning restore SKEXP0001