385 lines
20 KiB
C#
385 lines
20 KiB
C#
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 |