ChatRAG/Services/ResponseService/ConfidenceAwareRAGService.cs
2025-06-22 19:58:43 -03:00

385 lines
20 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ConfidenceAwareRAGService> _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<ConfidenceAwareRAGService> logger,
ConfidenceVerifier confidenceVerifier,
PromptConfigurationService promptService,
IOptions<ConfidenceAwareSettings> 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<string> 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<QueryAnalysis> 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<QueryAnalysis>(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<HierarchicalContext> ExecuteHierarchicalSearch(string question, string projectId, QueryAnalysis analysis, PromptTemplates prompts, string language)
{
var context = new HierarchicalContext();
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
context.Metadata["DetectedLanguage"] = language;
context.Metadata["SearchResults"] = new List<VectorSearchResult>();
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<ConfidenceResult> 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<VectorSearchResult> ExtractResultsFromContext(HierarchicalContext context, string projectId)
{
if (context.Metadata.ContainsKey("SearchResults") && context.Metadata["SearchResults"] is List<VectorSearchResult> storedResults)
return storedResults;
var results = new List<VectorSearchResult>();
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<string>();
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<VectorSearchResult>();
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<string> 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<VectorSearchResult> 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<VectorSearchResult> results)
{
if (!context.Metadata.ContainsKey("SearchResults")) context.Metadata["SearchResults"] = new List<VectorSearchResult>();
((List<VectorSearchResult>)context.Metadata["SearchResults"]).AddRange(results);
}
private void AddToSearchResults(HierarchicalContext context, List<VectorSearchResult> additionalResults)
{
if (context.Metadata.ContainsKey("SearchResults"))
{
var storedResults = (List<VectorSearchResult>)context.Metadata["SearchResults"];
storedResults.AddRange(additionalResults.Where(r => !storedResults.Any(sr => sr.Id == r.Id)));
}
else StoreSearchResults(context, additionalResults);
}
private async Task<List<VectorSearchResult>> ExpandContext(List<VectorSearchResult> initialResults, string projectId, ITextEmbeddingGenerationService embeddingService)
{
var expandedResults = new List<VectorSearchResult>();
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<string> SummarizeDocuments(List<VectorSearchResult> 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<string> 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<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
{
return GetResponse(userData, projectId, sessionId, question, "pt");
}
}
}
#pragma warning restore SKEXP0001