diff --git a/Configuration/Prompts/Domains/Financeiro.json b/Configuration/Prompts/Domains/Financeiro.json new file mode 100644 index 0000000..bf54342 --- /dev/null +++ b/Configuration/Prompts/Domains/Financeiro.json @@ -0,0 +1,11 @@ +{ + "Name": "Financeiro", + "Description": "Configurações para projetos financeiros e contábeis", + "Keywords": [ "financeiro", "contábil", "faturamento", "cobrança", "pagamento", "receita", "despesa" ], + "Concepts": [ "fluxo de caixa", "conciliação", "relatórios financeiros", "impostos", "audit trail" ], + "Prompts": { + "pt": { + "Response": "Você é um especialista em sistemas financeiros e contabilidade.\n\nSISTEMA FINANCEIRO: {0}\nPERGUNTA: \"{1}\"\nCONTEXTO FINANCEIRO: {2}\nANÁLISE REALIZADA: {3}\n\nResponda considerando:\n- Controles financeiros\n- Auditoria e compliance\n- Fluxos de aprovação\n- Relatórios gerenciais\n- Segurança de dados financeiros\n\nSeja preciso e considere aspectos regulatórios." + } + } +} \ No newline at end of file diff --git a/Configuration/Prompts/Domains/QA.json b/Configuration/Prompts/Domains/QA.json new file mode 100644 index 0000000..01901ca --- /dev/null +++ b/Configuration/Prompts/Domains/QA.json @@ -0,0 +1,11 @@ +{ + "Name": "Quality Assurance", + "Description": "Configurações para projetos de QA e testes", + "Keywords": [ "teste", "qa", "qualidade", "bug", "defeito", "validação", "verificação" ], + "Concepts": [ "test cases", "automation", "regression", "performance", "security testing" ], + "Prompts": { + "pt": { + "Response": "Você é um especialista em Quality Assurance e testes de software.\n\nPROJETO: {0}\nPERGUNTA DE QA: \"{1}\"\nCONTEXTO DE TESTES: {2}\nANÁLISE EXECUTADA: {3}\n\nResponda com foco em:\n- Estratégias de teste\n- Casos de teste específicos\n- Automação e ferramentas\n- Critérios de aceitação\n- Cobertura de testes\n\nSeja detalhado e metodológico na abordagem." + } + } +} \ No newline at end of file diff --git a/Configuration/Prompts/Domains/RH.json b/Configuration/Prompts/Domains/RH.json new file mode 100644 index 0000000..8e8f131 --- /dev/null +++ b/Configuration/Prompts/Domains/RH.json @@ -0,0 +1,11 @@ +{ + "Name": "Recursos Humanos", + "Description": "Configurações para projetos de RH e gestão de pessoas", + "Keywords": [ "funcionário", "colaborador", "cargo", "departamento", "folha", "benefícios", "treinamento" ], + "Concepts": [ "gestão de pessoas", "recrutamento", "seleção", "avaliação", "desenvolvimento" ], + "Prompts": { + "pt": { + "Response": "Você é um especialista em Recursos Humanos e gestão de pessoas.\n\nSISTEMA DE RH: {0}\nPERGUNTA: \"{1}\"\nCONTEXTO: {2}\nPROCESSOS ANALISADOS: {3}\n\nResponda considerando:\n- Políticas de RH\n- Fluxos de trabalho\n- Compliance e regulamentações\n- Melhores práticas em gestão de pessoas\n\nSeja claro e prático nas recomendações." + } + } +} \ No newline at end of file diff --git a/Configuration/Prompts/Domains/Servicos.json b/Configuration/Prompts/Domains/Servicos.json new file mode 100644 index 0000000..a1b89ef --- /dev/null +++ b/Configuration/Prompts/Domains/Servicos.json @@ -0,0 +1,74 @@ +{ + "Name": "Serviços JobMaker", + "Description": "Chatbot especializado em serviços de RAG, IA empresarial e desenvolvimento da JobMaker", + "Keywords": [ + "rag", + "retrieval augmented generation", + "semantic kernel", + "chatbot", + "ia", + "inteligencia artificial", + "desenvolvimento", + "consultoria", + "c#", + "dotnet", + ".net", + "python", + "migracao", + "sistema", + "sap", + "salesforce", + "integracao", + "web scraping", + "rpa", + "etl", + "mongodb", + "qdrant", + "workshop", + "treinamento", + "poc", + "prova conceito", + "enterprise", + "corporativo", + "automacao", + "performance", + "otimizacao", + "suporte", + "preço", + "valor", + "custo", + "orçamento", + "quanto custa", + "prazo", + "tempo", + "entrega", + "projeto", + "solução" + ], + "Concepts": [ + "retrieval augmented generation", + "microsoft semantic kernel", + "chatbot empresarial", + "inteligencia artificial conversacional", + "migracao python para c#", + "integracao sap salesforce", + "web scraping rpa", + "etl sincronizacao dados", + "arquitetura enterprise", + "sistemas escaláveis", + "poc prova conceito", + "consultoria ia empresarial", + "apresentação", + "boas vindas", + "consultoria", + "agendamento" + ], + "Prompts": { + "pt": { + "Response": "Você é um assistente virtual especializado nos serviços da JobMaker, empresa líder em RAG (Retrieval-Augmented Generation) e IA empresarial. Você atende chamadas via chat e responde com cordialidade\n\n🏢 EMPRESA: {0}\n❓ PERGUNTA DO CLIENTE: \"{1}\"\n📊 INFORMAÇÕES DISPONÍVEIS: {2}\n\nResponda de forma:\n✅ **Profissional e técnica** (mas acessível)\n✅ ***Se a pergunta for técnica ou envolver algum termo técnico***\n **Específica sobre nossos serviços**\n✅ **Highlighting nossos diferenciais**: C#/.NET, Semantic Kernel, economia 40-60%, performance 3-5x\n✅ **Incentivando contato** para demonstração ou consultoria\n\n\n- Call-to-action(não adicionar na resposta) para demo/consultoria\n\n⚠️ **Se não tiver informação suficiente:** Seja honesto, destaque que temos expertise em RAG/IA empresarial e ofereça consultoria personalizada." + }, + "en": { + "Response": "You are a virtual assistant specialized in JobMaker services, leading company in RAG (Retrieval-Augmented Generation) and enterprise AI.\n\n🏢 COMPANY: {0}\n❓ CUSTOMER QUESTION: \"{1}\"\n📊 AVAILABLE INFORMATION: {2}\n\nRespond in a:\n✅ **Professional and technical** (but accessible) manner\n✅ **Specific about our services**\n✅ **Highlighting our differentiators**: C#/.NET, Semantic Kernel, 40-60% savings, 3-5x performance\n✅ **Encouraging contact** for demonstration or consultation\n\n💡 **Always include:**\n- Concrete benefits (savings, performance)\n- Technologies used\n- Estimated timeline when relevant\n- Call-to-action(do not add to response) for demo/consultation\n\n⚠️ **If insufficient information:** Be honest, highlight our RAG/enterprise AI expertise and offer personalized consultation." + } + } +} \ No newline at end of file diff --git a/Configuration/Prompts/Domains/TI.json b/Configuration/Prompts/Domains/TI.json new file mode 100644 index 0000000..db80db0 --- /dev/null +++ b/Configuration/Prompts/Domains/TI.json @@ -0,0 +1,11 @@ +{ + "Name": "Tecnologia da Informação", + "Description": "Configurações para projetos de TI e desenvolvimento de software", + "Keywords": [ "api", "backend", "frontend", "database", "arquitetura", "código", "classe", "método", "endpoint" ], + "Concepts": [ "mvc", "rest", "microservices", "clean architecture", "design patterns", "authentication", "authorization" ], + "Prompts": { + "pt": { + "Response": "Você é um especialista em desenvolvimento de software e arquitetura de sistemas.\n\nPROJETO: {0}\nPERGUNTA TÉCNICA: \"{1}\"\nCONTEXTO TÉCNICO: {2}\nANÁLISE REALIZADA: {3}\n\nResponda com foco técnico, incluindo:\n- Implementação prática\n- Boas práticas de código\n- Considerações de arquitetura\n- Exemplos de código quando relevante\n\nSeja preciso e técnico na resposta." + } + } +} \ No newline at end of file diff --git a/Configuration/Prompts/base-prompts.json b/Configuration/Prompts/base-prompts.json new file mode 100644 index 0000000..51f6745 --- /dev/null +++ b/Configuration/Prompts/base-prompts.json @@ -0,0 +1,18 @@ +{ + "Prompts": { + "pt": { + "QueryAnalysis": "Analise esta pergunta e classifique com precisão:\nPERGUNTA: \"{0}\"\n\nResponda APENAS no formato JSON:\n{{\n \"strategy\": \"overview|specific|detailed\",\n \"complexity\": \"simple|medium|complex\",\n \"scope\": \"global|filtered|targeted\",\n \"concepts\": [\"conceito1\", \"conceito2\"],\n \"needs_hierarchy\": true|false\n}}", + + "Response": "Você é um especialista em análise de software e QA.\n\nPROJETO: {0}\nPERGUNTA: \"{1}\"\nCONTEXTO HIERÁRQUICO: {2}\nETAPAS EXECUTADAS: {3}\n\nResponda à pergunta de forma precisa e estruturada, aproveitando todo o contexto hierárquico coletado.", + + "Summary": "Resuma os pontos principais destes documentos sobre {0}:\n\n{1}\n\nResponda apenas com uma lista concisa dos pontos mais importantes:", + + "GapAnalysis": "Baseado na pergunta e contexto atual, identifique que informações ainda faltam para uma resposta completa.\n\nPERGUNTA: {0}\nCONTEXTO ATUAL: {1}\n\nResponda APENAS com palavras-chave dos conceitos/informações que ainda faltam, separados por vírgula.\nSe o contexto for suficiente, responda 'SUFICIENTE'." + }, + "en": { + "QueryAnalysis": "Analyze this question and classify precisely:\nQUESTION: \"{0}\"\n\nAnswer ONLY in JSON format:\n{{\n \"strategy\": \"overview|specific|detailed\",\n \"complexity\": \"simple|medium|complex\",\n \"scope\": \"global|filtered|targeted\",\n \"concepts\": [\"concept1\", \"concept2\"],\n \"needs_hierarchy\": true|false\n}}", + + "Response": "You are a software analysis and QA expert.\n\nPROJECT: {0}\nQUESTION: \"{1}\"\nHIERARCHICAL CONTEXT: {2}\nEXECUTED STEPS: {3}\n\nAnswer the question precisely and structured, leveraging all the hierarchical context collected." + } + } +} \ No newline at end of file diff --git a/Controllers/ChatController.cs b/Controllers/ChatController.cs index ebe38ab..1c5b718 100644 --- a/Controllers/ChatController.cs +++ b/Controllers/ChatController.cs @@ -43,7 +43,7 @@ namespace ChatApi.Controllers [HttpPost] [Route("response")] - public async Task GetResponse([FromForm] ChatRequest chatRequest) + public async Task GetResponse([FromBody] ChatRequest chatRequest) { try { diff --git a/Program.cs b/Program.cs index 70b93a1..1eeaa15 100644 --- a/Program.cs +++ b/Program.cs @@ -7,10 +7,13 @@ using ChatRAG.Contracts.VectorSearch; using ChatRAG.Data; using ChatRAG.Extensions; using ChatRAG.Services; +using ChatRAG.Services.Confidence; using ChatRAG.Services.Contracts; +using ChatRAG.Services.PromptConfiguration; using ChatRAG.Services.ResponseService; using ChatRAG.Services.SearchVectors; using ChatRAG.Services.TextServices; +using ChatRAG.Settings; using ChatRAG.Settings.ChatRAG.Configuration; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Features; @@ -76,8 +79,12 @@ builder.Services.AddSwaggerGen(c => }); }); -builder.Services.Configure( -builder.Configuration.GetSection("ChatRHSettings")); +builder.Services.Configure( +builder.Configuration.GetSection("Confidence")); + +builder.Services.Configure( +builder.Configuration.GetSection("ConfidenceAware")); + //builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -157,8 +164,11 @@ builder.Services.AddScoped(provider => { var configuration = provider.GetService(); var useHierarchical = configuration?.GetValue("Features:UseHierarchicalRAG") ?? false; + var useConfidence = configuration?.GetValue("Features:UseConfidenceAwareRAG") ?? false; - return useHierarchical + return useConfidence && useHierarchical + ? provider.GetRequiredService() + : useHierarchical ? provider.GetRequiredService() : provider.GetRequiredService(); }); @@ -167,6 +177,13 @@ builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddSingleton(); +// Registrar servios de confiana +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +// Registrar ConfidenceAwareRAGService +builder.Services.AddScoped(); + //builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); //builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); //var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin"); @@ -182,7 +199,6 @@ var model = "llama-3.1-8b-instant"; var url = "https://api.groq.com/openai/v1"; builder.Services.AddOpenAIChatCompletion(model, new Uri(url), key); - //Notebook //var model = "meta-llama/Llama-3.2-3B-Instruct"; //var url = "https://api.deepinfra.com/v1/openai"; // Adicione o /v1/openai @@ -205,9 +221,9 @@ builder.Services.AddOpenAIChatCompletion(model, new Uri(url), key); //builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://192.168.0.150:11434")); //Desktop -builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11434")); +//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11434")); //Notebook -//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435")); +builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435")); //builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); diff --git a/Services/Confidence/ConfidenceVerifier.cs b/Services/Confidence/ConfidenceVerifier.cs new file mode 100644 index 0000000..8161db8 --- /dev/null +++ b/Services/Confidence/ConfidenceVerifier.cs @@ -0,0 +1,389 @@ +using ChatRAG.Models; +using ChatRAG.Services.ResponseService; +using ChatRAG.Settings; +using Microsoft.Extensions.Options; + +namespace ChatRAG.Services.Confidence +{ + /// + /// Verifica se o RAG deve responder baseado na confiança dos resultados + /// + public class ConfidenceVerifier + { + private readonly ILogger _logger; + private readonly ConfidenceSettings _settings; + + public ConfidenceVerifier( + ILogger logger, + IOptions settings) + { + _logger = logger; + _settings = settings.Value; + } + + /// + /// Verifica se deve responder baseado na análise, resultados e contexto + /// + public ConfidenceResult VerifyConfidence( + QueryAnalysis analysis, + List results, + HierarchicalContext context, + bool strictMode = true, + string language = "pt") + { + var strategy = analysis.Strategy?.ToLower() ?? "specific"; + var thresholds = GetThresholds(strategy, strictMode); + + _logger.LogInformation("Verificando confiança - Estratégia: {Strategy}, Modo Restrito: {StrictMode}, Idioma: {Language}", + strategy, strictMode, language); + + // Calcular métricas de confiança + var metrics = CalculateConfidenceMetrics(results, context, analysis); + + // Verificar se deve responder baseado na estratégia + var shouldRespond = ShouldRespond(strategy, metrics, thresholds); + + var result = new ConfidenceResult + { + ShouldRespond = shouldRespond, + ConfidenceScore = metrics.OverallScore, + Strategy = strategy, + Metrics = metrics, + Reason = GenerateReason(shouldRespond, strategy, metrics, thresholds, language), + SuggestedResponse = shouldRespond ? null : GenerateFallbackResponse(strategy, metrics, language) + }; + + _logger.LogInformation("Resultado da confiança: {ShouldRespond} (Score: {ConfidenceScore:P1}, Estratégia: {Strategy})", + result.ShouldRespond, result.ConfidenceScore, strategy); + + return result; + } + + /// + /// Calcula métricas detalhadas de confiança + /// + private ConfidenceMetrics CalculateConfidenceMetrics( + List results, + HierarchicalContext context, + QueryAnalysis analysis) + { + var relevantResults = results.Where(r => r.Score >= 0.3).ToList(); + var highQualityResults = results.Where(r => r.Score >= 0.6).ToList(); + + var metrics = new ConfidenceMetrics + { + TotalDocuments = results.Count, + RelevantDocuments = relevantResults.Count, + HighQualityDocuments = highQualityResults.Count, + AverageScore = results.Any() ? results.Average(r => r.Score) : 0, + MaxScore = results.Any() ? results.Max(r => r.Score) : 0, + MinScore = results.Any() ? results.Min(r => r.Score) : 0, + ContextLength = context.CombinedContext?.Length ?? 0, + StepsExecuted = context.Steps.Count, + HasSpecificContent = HasSpecificContent(results, analysis), + OverallScore = CalculateOverallScore(results, context, analysis) + }; + + // Métricas adicionais + metrics.ScoreVariance = CalculateScoreVariance(results); + metrics.ContentDiversity = CalculateContentDiversity(results); + metrics.ConceptCoverage = CalculateConceptCoverage(results, analysis); + + return metrics; + } + + /// + /// Verifica se há conteúdo específico relacionado aos conceitos da query + /// + private bool HasSpecificContent(List results, QueryAnalysis analysis) + { + if (!analysis.Concepts?.Any() == true) + return results.Any(); // Se não há conceitos específicos, qualquer resultado serve + + var combinedContent = string.Join(" ", results.Select(r => $"{r.Title} {r.Content}")).ToLower(); + var conceptsFound = analysis.Concepts.Count(concept => + combinedContent.Contains(concept.ToLower())); + + var minConceptsRequired = Math.Max(1, Math.Min(2, analysis.Concepts.Length)); + return conceptsFound >= minConceptsRequired; + } + + /// + /// Calcula score geral considerando múltiplos fatores + /// + private double CalculateOverallScore(List results, HierarchicalContext context, QueryAnalysis analysis) + { + if (!results.Any()) return 0; + + // Pesos para diferentes fatores + const double scoreWeight = 0.35; // Qualidade dos scores + const double countWeight = 0.25; // Quantidade de documentos + const double contextWeight = 0.20; // Tamanho do contexto + const double diversityWeight = 0.10; // Diversidade do conteúdo + const double conceptWeight = 0.10; // Cobertura de conceitos + + // Score baseado na qualidade dos resultados + var avgScore = results.Average(r => r.Score); + var maxScore = results.Max(r => r.Score); + var qualityScore = (avgScore * 0.7) + (maxScore * 0.3); // Média ponderada + + // Score baseado na quantidade (com saturação) + var countScore = Math.Min(1.0, results.Count / 5.0); + + // Score baseado no tamanho do contexto (com saturação) + var contextScore = Math.Min(1.0, (context.CombinedContext?.Length ?? 0) / 2000.0); + + // Score baseado na diversidade do conteúdo + var diversityScore = CalculateContentDiversity(results); + + // Score baseado na cobertura de conceitos + var conceptScore = CalculateConceptCoverage(results, analysis); + + var overallScore = (qualityScore * scoreWeight) + + (countScore * countWeight) + + (contextScore * contextWeight) + + (diversityScore * diversityWeight) + + (conceptScore * conceptWeight); + + return Math.Min(1.0, overallScore); + } + + /// + /// Calcula variância dos scores para medir consistência + /// + private double CalculateScoreVariance(List results) + { + if (results.Count < 2) return 0; + + var mean = results.Average(r => r.Score); + var variance = results.Average(r => Math.Pow(r.Score - mean, 2)); + + // Normalizar para 0-1 (menor variância = melhor) + return Math.Max(0, 1 - (variance * 4)); // Multiplica por 4 para normalizar + } + + /// + /// Calcula diversidade do conteúdo (documentos diferentes) + /// + private double CalculateContentDiversity(List results) + { + if (!results.Any()) return 0; + + // Simples heurística: títulos únicos / total + var uniqueTitles = results.Select(r => r.Title?.ToLower() ?? "").Distinct().Count(); + return Math.Min(1.0, (double)uniqueTitles / results.Count); + } + + /// + /// Calcula cobertura dos conceitos da query + /// + private double CalculateConceptCoverage(List results, QueryAnalysis analysis) + { + if (!analysis.Concepts?.Any() == true) return 1.0; // Se não há conceitos, assume cobertura total + + var combinedContent = string.Join(" ", results.Select(r => $"{r.Title} {r.Content}")).ToLower(); + var conceptsFound = analysis.Concepts.Count(concept => + combinedContent.Contains(concept.ToLower())); + + return (double)conceptsFound / analysis.Concepts.Length; + } + + /// + /// Decide se deve responder baseado na estratégia e métricas + /// + private bool ShouldRespond(string strategy, ConfidenceMetrics metrics, ConfidenceThresholds thresholds) + { + return strategy switch + { + "overview" => ShouldRespondOverview(metrics, thresholds), + "specific" => ShouldRespondSpecific(metrics, thresholds), + "detailed" => ShouldRespondDetailed(metrics, thresholds), + _ => ShouldRespondSpecific(metrics, thresholds) // Default para specific + }; + } + + /// + /// Lógica específica para estratégia Overview + /// + private bool ShouldRespondOverview(ConfidenceMetrics metrics, ConfidenceThresholds thresholds) + { + // Para overview, precisamos de quantidade e contexto amplo + return metrics.TotalDocuments >= thresholds.MinDocuments && + metrics.ContextLength >= thresholds.MinContextLength && + metrics.OverallScore >= thresholds.MinOverallScore && + (metrics.RelevantDocuments >= thresholds.MinRelevantDocuments || metrics.TotalDocuments >= 10); // Flexibilidade para projetos grandes + } + + /// + /// Lógica específica para estratégia Specific + /// + private bool ShouldRespondSpecific(ConfidenceMetrics metrics, ConfidenceThresholds thresholds) + { + // Para specific, precisamos de relevância e qualidade + return metrics.RelevantDocuments >= thresholds.MinRelevantDocuments && + metrics.MaxScore >= thresholds.MinMaxScore && + metrics.OverallScore >= thresholds.MinOverallScore && + metrics.HasSpecificContent; + } + + /// + /// Lógica específica para estratégia Detailed + /// + private bool ShouldRespondDetailed(ConfidenceMetrics metrics, ConfidenceThresholds thresholds) + { + // Para detailed, precisamos de alta qualidade e cobertura + return metrics.HighQualityDocuments >= thresholds.MinHighQualityDocuments && + metrics.AverageScore >= thresholds.MinAverageScore && + metrics.HasSpecificContent && + metrics.OverallScore >= thresholds.MinOverallScore && + metrics.ConceptCoverage >= 0.5; // Pelo menos 50% dos conceitos cobertos + } + + /// + /// Obtém thresholds ajustados para modo restrito/relaxado + /// + private ConfidenceThresholds GetThresholds(string strategy, bool strictMode) + { + if (!_settings.Thresholds.ContainsKey(strategy)) + { + _logger.LogWarning("Estratégia '{Strategy}' não encontrada, usando 'specific'", strategy); + strategy = "specific"; + } + + var baseThresholds = _settings.Thresholds[strategy]; + + if (!strictMode) + { + // Modo relaxado: reduz os thresholds + return new ConfidenceThresholds + { + MinDocuments = Math.Max(1, baseThresholds.MinDocuments - 2), + MinRelevantDocuments = Math.Max(1, baseThresholds.MinRelevantDocuments - 1), + MinHighQualityDocuments = Math.Max(0, baseThresholds.MinHighQualityDocuments - 1), + MinContextLength = Math.Max(100, baseThresholds.MinContextLength - 500), + MinOverallScore = Math.Max(0.1, baseThresholds.MinOverallScore - 0.15), + MinMaxScore = Math.Max(0.2, baseThresholds.MinMaxScore - 0.15), + MinAverageScore = Math.Max(0.2, baseThresholds.MinAverageScore - 0.15) + }; + } + + return baseThresholds; + } + + /// + /// Gera explicação do motivo da decisão + /// + private string GenerateReason(bool shouldRespond, string strategy, ConfidenceMetrics metrics, ConfidenceThresholds thresholds, string language) + { + if (shouldRespond) + { + return language == "en" + ? $"Sufficient confidence for '{strategy}' strategy - Score: {metrics.OverallScore:P1}, Docs: {metrics.RelevantDocuments}/{metrics.TotalDocuments}" + : $"Confiança suficiente para estratégia '{strategy}' - Score: {metrics.OverallScore:P1}, Docs: {metrics.RelevantDocuments}/{metrics.TotalDocuments}"; + } + + var issues = new List(); + + if (metrics.TotalDocuments < thresholds.MinDocuments) + { + var msg = language == "en" + ? $"few documents ({metrics.TotalDocuments} < {thresholds.MinDocuments})" + : $"poucos documentos ({metrics.TotalDocuments} < {thresholds.MinDocuments})"; + issues.Add(msg); + } + + if (metrics.RelevantDocuments < thresholds.MinRelevantDocuments) + { + var msg = language == "en" + ? $"few relevant documents ({metrics.RelevantDocuments} < {thresholds.MinRelevantDocuments})" + : $"poucos documentos relevantes ({metrics.RelevantDocuments} < {thresholds.MinRelevantDocuments})"; + issues.Add(msg); + } + + if (metrics.OverallScore < thresholds.MinOverallScore) + { + var msg = language == "en" + ? $"low overall score ({metrics.OverallScore:P1} < {thresholds.MinOverallScore:P1})" + : $"score geral baixo ({metrics.OverallScore:P1} < {thresholds.MinOverallScore:P1})"; + issues.Add(msg); + } + + if (!metrics.HasSpecificContent) + { + var msg = language == "en" ? "no specific content found" : "conteúdo específico não encontrado"; + issues.Add(msg); + } + + var prefix = language == "en" ? "Insufficient confidence: " : "Confiança insuficiente: "; + return prefix + string.Join(", ", issues); + } + + /// + /// Gera resposta de fallback apropriada + /// + private string GenerateFallbackResponse(string strategy, ConfidenceMetrics metrics, string language) + { + if (!_settings.FallbackMessages.ContainsKey(language)) + { + language = "pt"; // Fallback para português + } + + var messages = _settings.FallbackMessages[language]; + + // Escolher mensagem baseada no problema principal + if (metrics.TotalDocuments == 0) + { + return messages.NoDocuments; + } + + if (metrics.RelevantDocuments == 0) + { + return messages.NoRelevantDocuments; + } + + return strategy switch + { + "overview" => messages.InsufficientOverview, + "specific" => messages.InsufficientSpecific, + "detailed" => messages.InsufficientDetailed, + _ => messages.Generic + }; + } + } + + /// + /// Resultado da verificação de confiança + /// + public class ConfidenceResult + { + public bool ShouldRespond { get; set; } + public double ConfidenceScore { get; set; } + public string Strategy { get; set; } = ""; + public ConfidenceMetrics Metrics { get; set; } = new(); + public string Reason { get; set; } = ""; + public string? SuggestedResponse { get; set; } + } + + /// + /// Métricas detalhadas de confiança + /// + public class ConfidenceMetrics + { + // Métricas básicas + public int TotalDocuments { get; set; } + public int RelevantDocuments { get; set; } + public int HighQualityDocuments { get; set; } + public double AverageScore { get; set; } + public double MaxScore { get; set; } + public double MinScore { get; set; } + public int ContextLength { get; set; } + public int StepsExecuted { get; set; } + public bool HasSpecificContent { get; set; } + public double OverallScore { get; set; } + + // Métricas avançadas + public double ScoreVariance { get; set; } + public double ContentDiversity { get; set; } + public double ConceptCoverage { get; set; } + } +} diff --git a/Services/PromptConfiguration/PromptConfigurationService.cs b/Services/PromptConfiguration/PromptConfigurationService.cs new file mode 100644 index 0000000..e60dedc --- /dev/null +++ b/Services/PromptConfiguration/PromptConfigurationService.cs @@ -0,0 +1,753 @@ +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Options; +using ChatRAG.Services.Confidence; +using ChatRAG.Settings; +using Microsoft.Extensions.Configuration; + +namespace ChatRAG.Services.PromptConfiguration +{ + /// + /// Serviço para configuração e carregamento de prompts por domínio e idioma + /// + public class PromptConfigurationService + { + private readonly ILogger _logger; + private readonly string _configurationPath; + private readonly LanguageSettings _languageSettings; + private readonly CacheSettings _cacheSettings; + + private Dictionary _domainConfigs = new(); + private BasePromptConfig _baseConfig = new(); + private readonly Dictionary _fileLastModified = new(); + private readonly object _lockObject = new object(); + + public PromptConfigurationService( + ILogger logger, + IConfiguration configuration, + IOptions settings) + { + _logger = logger; + _configurationPath = configuration["PromptConfiguration:Path"] ?? "Configuration/Prompts"; + _languageSettings = settings.Value.Languages; + _cacheSettings = settings.Value.Cache; + + // Carregar configurações na inicialização + _ = Task.Run(LoadConfigurations); + } + + /// + /// Obtém prompts configurados para um domínio e idioma específicos + /// + public PromptTemplates GetPrompts(string? domain = null, string language = "pt") + { + // Verificar se precisa recarregar arquivos (se habilitado) + if (_cacheSettings.AutoReloadOnFileChange) + { + CheckForFileChanges(); + } + + // Detectar idioma se auto-detecção estiver habilitada + var detectedLanguage = _languageSettings.AutoDetectLanguage + ? DetectOrValidateLanguage(language) + : language; + + var domainConfig = domain != null && _domainConfigs.ContainsKey(domain) + ? _domainConfigs[domain] + : null; + + return new PromptTemplates + { + QueryAnalysis = GetPrompt("QueryAnalysis", domainConfig, detectedLanguage), + Overview = GetPrompt("Overview", domainConfig, detectedLanguage), + Specific = GetPrompt("Specific", domainConfig, detectedLanguage), + Detailed = GetPrompt("Detailed", domainConfig, detectedLanguage), + Response = GetPrompt("Response", domainConfig, detectedLanguage), + Summary = GetPrompt("Summary", domainConfig, detectedLanguage), + GapAnalysis = GetPrompt("GapAnalysis", domainConfig, detectedLanguage) + }; + } + + /// + /// Detecta o domínio baseado na pergunta e descrição do projeto + /// + public string? DetectDomain(string question, string? projectDescription = null) + { + var content = $"{question} {projectDescription}".ToLower(); + var domainScores = new Dictionary(); + + foreach (var (domain, config) in _domainConfigs) + { + var score = 0; + + // Pontuação por palavras-chave (peso 2) + foreach (var keyword in config.Keywords) + { + if (content.Contains(keyword.ToLower())) + { + score += 2; + } + } + + // Pontuação por conceitos (peso 1) + foreach (var concept in config.Concepts) + { + if (content.Contains(concept.ToLower())) + { + score += 1; + } + } + + if (score > 0) + { + domainScores[domain] = score; + } + } + + if (domainScores.Any()) + { + var bestDomain = domainScores.OrderByDescending(x => x.Value).First(); + _logger.LogDebug("Domínio detectado: {Domain} com score {Score}", bestDomain.Key, bestDomain.Value); + return bestDomain.Key; + } + + _logger.LogDebug("Nenhum domínio detectado, usando padrão"); + return null; + } + + /// + /// Detecta o idioma da pergunta + /// + public string DetectLanguage(string question) + { + if (string.IsNullOrWhiteSpace(question)) + return _languageSettings.DefaultLanguage; + + var languageScores = new Dictionary(); + var words = Regex.Split(question.ToLower(), @"\W+") + .Where(w => w.Length > 2) + .ToList(); + + foreach (var (language, keywords) in _languageSettings.LanguageKeywords) + { + var score = words.Count(word => keywords.Contains(word)); + if (score > 0) + { + languageScores[language] = score; + } + } + + if (languageScores.Any()) + { + var detectedLanguage = languageScores.OrderByDescending(x => x.Value).First().Key; + _logger.LogDebug("Idioma detectado: {Language}", detectedLanguage); + return detectedLanguage; + } + + _logger.LogDebug("Idioma não detectado, usando padrão: {DefaultLanguage}", _languageSettings.DefaultLanguage); + return _languageSettings.DefaultLanguage; + } + + /// + /// Lista domínios disponíveis + /// + public List GetAvailableDomains() + { + return _domainConfigs.Keys.ToList(); + } + + /// + /// Lista idiomas suportados + /// + public List GetSupportedLanguages() + { + return _languageSettings.SupportedLanguages; + } + + /// + /// Força recarregamento das configurações + /// + public async Task ReloadConfigurations() + { + await LoadConfigurations(); + } + + // === MÉTODOS PRIVADOS === + + private void LoadBaseConfigurationSync() + { + var basePath = Path.Combine(_configurationPath, "base-prompts.json"); + + if (File.Exists(basePath)) + { + try + { + var json = File.ReadAllText(basePath); + _baseConfig = JsonSerializer.Deserialize(json) ?? new BasePromptConfig(); + _fileLastModified[basePath] = File.GetLastWriteTime(basePath); + _logger.LogDebug("Configuração base carregada de {Path}", basePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao carregar configuração base de {Path}", basePath); + _baseConfig = GetDefaultBaseConfig(); + } + } + else + { + _logger.LogInformation("Arquivo base não encontrado, criando padrão em {Path}", basePath); + _baseConfig = GetDefaultBaseConfig(); + SaveBaseConfigurationSync(basePath); + } + } + + private void LoadDomainConfigurationsSync() + { + var domainsPath = Path.Combine(_configurationPath, "Domains"); + + if (!Directory.Exists(domainsPath)) + { + _logger.LogInformation("Pasta de domínios não encontrada, criando em {Path}", domainsPath); + Directory.CreateDirectory(domainsPath); + CreateDefaultDomainConfigurationsSync(domainsPath); + } + + var domainFiles = Directory.GetFiles(domainsPath, "*.json"); + var loadedDomains = new Dictionary(); + + foreach (var file in domainFiles) + { + try + { + var json = File.ReadAllText(file); + var config = JsonSerializer.Deserialize(json); + if (config != null) + { + var domainName = Path.GetFileNameWithoutExtension(file); + loadedDomains[domainName] = config; + _fileLastModified[file] = File.GetLastWriteTime(file); + _logger.LogDebug("Domínio {Domain} carregado de {File}", domainName, file); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Erro ao carregar configuração do domínio: {File}", file); + } + } + + _domainConfigs = loadedDomains; + } + + private void CheckForFileChanges() + { + if (!_cacheSettings.AutoReloadOnFileChange) return; + + var needsReload = false; + var filesToCheck = new List { Path.Combine(_configurationPath, "base-prompts.json") }; + + var domainsPath = Path.Combine(_configurationPath, "Domains"); + if (Directory.Exists(domainsPath)) + { + filesToCheck.AddRange(Directory.GetFiles(domainsPath, "*.json")); + } + + foreach (var file in filesToCheck) + { + if (File.Exists(file)) + { + var lastModified = File.GetLastWriteTime(file); + if (!_fileLastModified.ContainsKey(file) || _fileLastModified[file] < lastModified) + { + _logger.LogInformation("Arquivo modificado detectado: {File}", file); + needsReload = true; + break; + } + } + } + + if (needsReload) + { + _ = Task.Run(LoadConfigurations); + } + } + + private string DetectOrValidateLanguage(string language) + { + // Se o idioma é suportado, usar ele + if (_languageSettings.SupportedLanguages.Contains(language)) + { + return language; + } + + _logger.LogWarning("Idioma não suportado: {Language}, usando padrão: {DefaultLanguage}", + language, _languageSettings.DefaultLanguage); + return _languageSettings.DefaultLanguage; + } + + private string GetPrompt(string promptType, DomainPromptConfig? domainConfig, string language) + { + // 1. Tentar buscar no domínio específico e idioma específico + if (domainConfig?.Prompts.ContainsKey(language) == true && + domainConfig.Prompts[language].ContainsKey(promptType)) + { + return domainConfig.Prompts[language][promptType]; + } + + // 2. Tentar buscar no domínio específico no idioma padrão + if (domainConfig?.Prompts.ContainsKey(_languageSettings.DefaultLanguage) == true && + domainConfig.Prompts[_languageSettings.DefaultLanguage].ContainsKey(promptType)) + { + var prompt = domainConfig.Prompts[_languageSettings.DefaultLanguage][promptType]; + return _languageSettings.AlwaysRespondInRequestedLanguage && language != _languageSettings.DefaultLanguage + ? AddLanguageInstruction(prompt, language) + : prompt; + } + + // 3. Fallback para configuração base no idioma solicitado + if (_baseConfig.Prompts.ContainsKey(language) && + _baseConfig.Prompts[language].ContainsKey(promptType)) + { + return _baseConfig.Prompts[language][promptType]; + } + + // 4. Fallback para configuração base no idioma padrão + if (_baseConfig.Prompts.ContainsKey(_languageSettings.DefaultLanguage) && + _baseConfig.Prompts[_languageSettings.DefaultLanguage].ContainsKey(promptType)) + { + var prompt = _baseConfig.Prompts[_languageSettings.DefaultLanguage][promptType]; + return _languageSettings.AlwaysRespondInRequestedLanguage && language != _languageSettings.DefaultLanguage + ? AddLanguageInstruction(prompt, language) + : prompt; + } + + // 5. Fallback final + return GetFallbackPrompt(promptType, language); + } + + private string AddLanguageInstruction(string prompt, string targetLanguage) + { + var instruction = targetLanguage switch + { + "en" => "\n\nIMPORTANT: Respond in English.", + "pt" => "\n\nIMPORTANTE: Responda em português.", + _ => $"\n\nIMPORTANT: Respond in {targetLanguage}." + }; + + return prompt + instruction; + } + + private void SaveBaseConfigurationSync(string basePath) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(basePath)!); + + var options = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + var json = JsonSerializer.Serialize(_baseConfig, options); + File.WriteAllText(basePath, json); + _logger.LogInformation("Configuração base salva em {Path}", basePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao salvar configuração base em {Path}", basePath); + } + } + + private void CreateDefaultDomainConfigurationsSync(string domainsPath) + { + var domains = new Dictionary + { + ["TI"] = GetTIDomainConfig(), + ["RH"] = GetRHDomainConfig(), + ["Financeiro"] = GetFinanceiroDomainConfig(), + ["QA"] = GetQADomainConfig() + }; + + var options = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + foreach (var (domain, config) in domains) + { + try + { + var filePath = Path.Combine(domainsPath, $"{domain}.json"); + var json = JsonSerializer.Serialize(config, options); + File.WriteAllText(filePath, json); + _logger.LogInformation("Configuração de domínio {Domain} criada em {Path}", domain, filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao criar configuração do domínio {Domain}", domain); + } + } + } + + private BasePromptConfig GetDefaultBaseConfig() + { + return new BasePromptConfig + { + Prompts = new Dictionary> + { + ["pt"] = new Dictionary + { + ["QueryAnalysis"] = @"Analise esta pergunta e classifique com precisão: + PERGUNTA: ""{0}"" + + Responda APENAS no formato JSON: + {{ + ""strategy"": ""overview|specific|detailed"", + ""complexity"": ""simple|medium|complex"", + ""scope"": ""global|filtered|targeted"", + ""concepts"": [""conceito1"", ""conceito2""], + ""needs_hierarchy"": true|false + }} + + DEFINIÇÕES PRECISAS: + STRATEGY: + - overview: Pergunta sobre o PROJETO COMO UM TODO + - specific: Pergunta sobre MÓDULO/FUNCIONALIDADE ESPECÍFICA + - detailed: Pergunta técnica específica que precisa de CONTEXTO PROFUNDO", + + ["Response"] = @"Você é um especialista em análise de software e QA. + + PROJETO: {0} + PERGUNTA: ""{1}"" + CONTEXTO HIERÁRQUICO: {2} + ETAPAS EXECUTADAS: {3} + + Responda à pergunta de forma precisa e estruturada, aproveitando todo o contexto hierárquico coletado.", + + ["Summary"] = @"Resuma os pontos principais destes documentos sobre {0}: + + {1} + + Responda apenas com uma lista concisa dos pontos mais importantes:", + + ["GapAnalysis"] = @"Baseado na pergunta e contexto atual, identifique que informações ainda faltam para uma resposta completa. + + PERGUNTA: {0} + CONTEXTO ATUAL: {1} + + Responda APENAS com palavras-chave dos conceitos/informações que ainda faltam, separados por vírgula. + Se o contexto for suficiente, responda 'SUFICIENTE'." + }, + ["en"] = new Dictionary + { + ["QueryAnalysis"] = @"Analyze this question and classify precisely: + QUESTION: ""{0}"" + + Answer ONLY in JSON format: + {{ + ""strategy"": ""overview|specific|detailed"", + ""complexity"": ""simple|medium|complex"", + ""scope"": ""global|filtered|targeted"", + ""concepts"": [""concept1"", ""concept2""], + ""needs_hierarchy"": true|false + }} + + PRECISE DEFINITIONS: + STRATEGY: + - overview: Question about the PROJECT AS A WHOLE + - specific: Question about SPECIFIC MODULE/FUNCTIONALITY + - detailed: Technical specific question needing DEEP CONTEXT", + + ["Response"] = @"You are a software analysis and QA expert. + + PROJECT: {0} + QUESTION: ""{1}"" + HIERARCHICAL CONTEXT: {2} + EXECUTED STEPS: {3} + + Answer the question precisely and structured, leveraging all the hierarchical context collected.", + + ["Summary"] = @"Summarize the main points of these documents about {0}: + + {1} + + Answer only with a concise list of the most important points:", + + ["GapAnalysis"] = @"Based on the question and current context, identify what information is still missing for a complete answer. + + QUESTION: {0} + CURRENT CONTEXT: {1} + + Answer ONLY with keywords of missing concepts/information, separated by commas. + If the context is sufficient, answer 'SUFFICIENT'." + } + } + }; + } + + private DomainPromptConfig GetTIDomainConfig() + { + return new DomainPromptConfig + { + Name = "Tecnologia da Informação", + Description = "Configurações para projetos de TI e desenvolvimento de software", + Keywords = ["api", "backend", "frontend", "database", "arquitetura", "código", "classe", "método", "endpoint", "sistema", "software"], + Concepts = ["mvc", "rest", "microservices", "clean architecture", "design patterns", "authentication", "authorization", "crud"], + Prompts = new Dictionary> + { + ["pt"] = new Dictionary + { + ["Response"] = @"Você é um especialista em desenvolvimento de software e arquitetura de sistemas. + + PROJETO TÉCNICO: {0} + PERGUNTA TÉCNICA: ""{1}"" + CONTEXTO TÉCNICO: {2} + ANÁLISE REALIZADA: {3} + + Responda com foco técnico, incluindo: + - Implementação prática + - Boas práticas de código + - Considerações de arquitetura + - Exemplos de código quando relevante + + Seja preciso e técnico na resposta." + }, + ["en"] = new Dictionary + { + ["Response"] = @"You are a software development and system architecture expert. + + TECHNICAL PROJECT: {0} + TECHNICAL QUESTION: ""{1}"" + TECHNICAL CONTEXT: {2} + ANALYSIS PERFORMED: {3} + + Answer with technical focus, including: + - Practical implementation + - Code best practices + - Architecture considerations + - Code examples when relevant + + Be precise and technical in your response." + } + } + }; + } + + private DomainPromptConfig GetRHDomainConfig() + { + return new DomainPromptConfig + { + Name = "Recursos Humanos", + Description = "Configurações para projetos de RH e gestão de pessoas", + Keywords = ["funcionário", "colaborador", "cargo", "departamento", "folha", "benefícios", "treinamento", "employee", "hr"], + Concepts = ["gestão de pessoas", "recrutamento", "seleção", "avaliação", "desenvolvimento", "human resources"], + Prompts = new Dictionary> + { + ["pt"] = new Dictionary + { + ["Response"] = @"Você é um especialista em Recursos Humanos e gestão de pessoas. + + SISTEMA DE RH: {0} + PERGUNTA: ""{1}"" + CONTEXTO: {2} + PROCESSOS ANALISADOS: {3} + + Responda considerando: + - Políticas de RH + - Fluxos de trabalho + - Compliance e regulamentações + - Melhores práticas em gestão de pessoas + + Seja claro e prático nas recomendações." + }, + ["en"] = new Dictionary + { + ["Response"] = @"You are a Human Resources and people management expert. + + HR SYSTEM: {0} + QUESTION: ""{1}"" + CONTEXT: {2} + ANALYZED PROCESSES: {3} + + Answer considering: + - HR policies + - Workflows + - Compliance and regulations + - Best practices in people management + + Be clear and practical in recommendations." + } + } + }; + } + + private DomainPromptConfig GetFinanceiroDomainConfig() + { + return new DomainPromptConfig + { + Name = "Financeiro", + Description = "Configurações para projetos financeiros e contábeis", + Keywords = ["financeiro", "contábil", "faturamento", "cobrança", "pagamento", "receita", "despesa", "financial", "accounting"], + Concepts = ["fluxo de caixa", "conciliação", "relatórios financeiros", "impostos", "audit trail", "cash flow"], + Prompts = new Dictionary> + { + ["pt"] = new Dictionary + { + ["Response"] = @"Você é um especialista em sistemas financeiros e contabilidade. + + SISTEMA FINANCEIRO: {0} + PERGUNTA: ""{1}"" + CONTEXTO FINANCEIRO: {2} + ANÁLISE REALIZADA: {3} + + Responda considerando: + - Controles financeiros + - Auditoria e compliance + - Fluxos de aprovação + - Relatórios gerenciais + - Segurança de dados financeiros + + Seja preciso e considere aspectos regulatórios." + }, + ["en"] = new Dictionary + { + ["Response"] = @"You are a financial systems and accounting expert. + + FINANCIAL SYSTEM: {0} + QUESTION: ""{1}"" + FINANCIAL CONTEXT: {2} + ANALYSIS PERFORMED: {3} + + Answer considering: + - Financial controls + - Audit and compliance + - Approval workflows + - Management reports + - Financial data security + + Be precise and consider regulatory aspects." + } + } + }; + } + + private DomainPromptConfig GetQADomainConfig() + { + return new DomainPromptConfig + { + Name = "Quality Assurance", + Description = "Configurações para projetos de QA e testes", + Keywords = ["teste", "qa", "qualidade", "bug", "defeito", "validação", "verificação", "quality", "testing"], + Concepts = ["test cases", "automation", "regression", "performance", "security testing", "casos de teste"], + Prompts = new Dictionary> + { + ["pt"] = new Dictionary + { + ["Response"] = @"Você é um especialista em Quality Assurance e testes de software. + + PROJETO: {0} + PERGUNTA DE QA: ""{1}"" + CONTEXTO DE TESTES: {2} + ANÁLISE EXECUTADA: {3} + + Responda com foco em: + - Estratégias de teste + - Casos de teste específicos + - Automação e ferramentas + - Critérios de aceitação + - Cobertura de testes + + Seja detalhado e metodológico na abordagem." + }, + ["en"] = new Dictionary + { + ["Response"] = @"You are a Quality Assurance and software testing expert. + + PROJECT: {0} + QA QUESTION: ""{1}"" + TESTING CONTEXT: {2} + ANALYSIS EXECUTED: {3} + + Answer focusing on: + - Testing strategies + - Specific test cases + - Automation and tools + - Acceptance criteria + - Test coverage + + Be detailed and methodical in your approach." + } + } + }; + } + + private string GetFallbackPrompt(string promptType, string language) + { + return language == "en" ? promptType switch + { + "QueryAnalysis" => "Analyze the question: {0}", + "Response" => "Answer based on context: {2}", + "Summary" => "Summarize: {1}", + "GapAnalysis" => "Identify gaps for: {0}", + _ => "Process the request: {0}" + } : promptType switch + { + "QueryAnalysis" => "Analise a pergunta: {0}", + "Response" => "Responda baseado no contexto: {2}", + "Summary" => "Resuma: {1}", + "GapAnalysis" => "Identifique lacunas para: {0}", + _ => "Processe a solicitação: {0}" + }; + } + + /// Carrega todas as configurações de prompts + /// + public async Task LoadConfigurations() + { + lock (_lockObject) + { + try + { + LoadBaseConfigurationSync(); + LoadDomainConfigurationsSync(); + _logger.LogInformation("Carregados {DomainCount} domínios de prompt em {ConfigPath}", + _domainConfigs.Count, _configurationPath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao carregar configurações de prompt"); + } + } + } + + /// + } + + // === MODELOS DE DADOS === + + public class PromptTemplates + { + public string QueryAnalysis { get; set; } = ""; + public string Overview { get; set; } = ""; + public string Specific { get; set; } = ""; + public string Detailed { get; set; } = ""; + public string Response { get; set; } = ""; + public string Summary { get; set; } = ""; + public string GapAnalysis { get; set; } = ""; + } + + public class BasePromptConfig + { + public Dictionary> Prompts { get; set; } = new(); + } + + public class DomainPromptConfig + { + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public List Keywords { get; set; } = new(); + public List Concepts { get; set; } = new(); + public Dictionary> Prompts { get; set; } = new(); + } +} diff --git a/Services/ResponseService/ConfidenceAwareRAGService.cs b/Services/ResponseService/ConfidenceAwareRAGService.cs new file mode 100644 index 0000000..1b79af0 --- /dev/null +++ b/Services/ResponseService/ConfidenceAwareRAGService.cs @@ -0,0 +1,385 @@ +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 \ No newline at end of file diff --git a/Services/ResponseService/HierarchicalRAGService.cs b/Services/ResponseService/HierarchicalRAGService.cs index b073233..ebe5896 100644 --- a/Services/ResponseService/HierarchicalRAGService.cs +++ b/Services/ResponseService/HierarchicalRAGService.cs @@ -78,22 +78,24 @@ namespace ChatRAG.Services.ResponseService Responda APENAS no formato JSON: {{ - ""strategy"": ""overview|specific|detailed"", + ""strategy"": ""overview|specific|detailed|out_of_scope"", ""complexity"": ""simple|medium|complex"", ""scope"": ""global|filtered|targeted"", ""concepts"": [""conceito1"", ""conceito2""], - ""needs_hierarchy"": true|false + ""needs_hierarchy"": true|false, }} DEFINIÇÕES PRECISAS: STRATEGY: - overview: Pergunta sobre o PROJETO COMO UM TODO. Palavras-chave: ""projeto"", ""sistema"", ""aplicação"", ""este projeto"", ""todo o"", ""geral"", ""inteiro"". NÃO menciona módulos, funcionalidades ou tecnologias específicas. + - out_of_scope: Pergunta/frase sem sentido relacionada à projetos. Saudações, cumprimentos, perguntas sem sentido, etc. Palavras-chave: ""oi"", ""olá"", ""bom dia"", ""boa tarde"", ""quem"", ""quando"", ""etc"". NÃO menciona projetos e contém apenas uma saudação ou o usuário se apresentando, sem falar mais nada além disso. - specific: Pergunta sobre MÓDULO/FUNCIONALIDADE ESPECÍFICA. Menciona: nome de classe, controller, entidade, CRUD específico, funcionalidade particular, tecnologia específica. - detailed: Pergunta técnica específica que precisa de CONTEXTO PROFUNDO e detalhes de implementação. + - out_of_scope: Suadação, pergunta sem relação com os textos ou fora de contexto SCOPE: - - global: Busca informações de TODO o projeto (usar com overview) + - global: Busca informações de TODO o projeto (usar com overview ou com out_of_scope) - filtered: Busca com filtros específicos (usar com specific/detailed) - targeted: Busca muito específica e direcionada @@ -103,6 +105,11 @@ namespace ChatRAG.Services.ResponseService - ""Gere casos de teste para o CRUD de usuário"" → specific/filtered - ""Como implementar autenticação JWT neste controller"" → detailed/targeted - ""Documente este sistema"" → overview/global + - ""Oi!"" → out_of_scope/global + - ""Boa tarde!"" → out_of_scope/global + - ""Meu nome é [nome de usuario]"" → out_of_scope/global + - ""Faça uma conta"" → out_of_scope/global + - ""Me passe a receita de bolo?"" → out_of_scope/global - ""Explique a classe UserService"" → specific/filtered" : @"Analyze this question and classify precisely: @@ -110,7 +117,7 @@ namespace ChatRAG.Services.ResponseService Answer ONLY in JSON format: {{ - ""strategy"": ""overview|specific|detailed"", + ""strategy"": ""overview|specific|detailed|out_of_scope"", ""complexity"": ""simple|medium|complex"", ""scope"": ""global|filtered|targeted"", ""concepts"": [""concept1"", ""concept2""], @@ -121,11 +128,12 @@ namespace ChatRAG.Services.ResponseService STRATEGY: - overview: Question about the PROJECT AS A WHOLE. Keywords: ""project"", ""system"", ""application"", ""this project"", ""entire"", ""general"", ""whole"". Does NOT mention specific modules, functionalities or technologies. + - out_of_scope: Meaningless question/phrase related to projects. Greetings, salutations, meaningless questions, etc. Keywords: ""hi"", ""hello"", ""good morning"", ""good afternoon"", ""who"", ""when"", ""etc"". Does NOT mention projects and contains only a greeting or the user introducing themselves, without saying anything else. - specific: Question about SPECIFIC MODULE/FUNCTIONALITY. Mentions: class name, controller, entity, specific CRUD, particular functionality, specific technology. - detailed: Technical specific question needing DEEP CONTEXT and implementation details. SCOPE: - - global: Search information from ENTIRE project (use with overview) + - global: Search information from ENTIRE project (use with overview or out_of_scope) - filtered: Search with specific filters (use with specific/detailed) - targeted: Very specific and directed search @@ -134,6 +142,11 @@ namespace ChatRAG.Services.ResponseService - ""Generate test cases for user CRUD"" → specific/filtered - ""How to implement JWT authentication in this controller"" → detailed/targeted - ""Document this system"" → overview/global + - ""Hi!"" → out_of_scope/global + - ""Good afternoon!"" → out_of_scope/global + - ""My name is [nome de usuario]"" → out_of_scope/global + - ""Do an operation math for me"" → out_of_scope/global + - ""Give me the recipe for a cake?"" → out_of_scope/global - ""Explain the UserService class"" → specific/filtered"; var prompt = string.Format(analysisPrompt, question); @@ -182,6 +195,9 @@ namespace ChatRAG.Services.ResponseService switch (analysis.Strategy) { + case "out_of_scope": + await ExecuteOutOfContextStrategy(context, question, projectId, embeddingService); + break; case "overview": await ExecuteOverviewStrategy(context, question, projectId, embeddingService); break; @@ -198,6 +214,16 @@ namespace ChatRAG.Services.ResponseService return context; } + private async Task ExecuteOutOfContextStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService) + { + context.AddStep("Buscando o projeto"); + var project = _projectDataRepository.GetAsync(projectId); + + context.CombinedContext = string.Join("\n\n", project); + } + + + private async Task ExecuteOverviewStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService) { // Etapa 1: Buscar TODOS os documentos do projeto @@ -467,14 +493,14 @@ namespace ChatRAG.Services.ResponseService var project = $"Nome: {projectData.Nome} \n\n Descrição:{projectData.Descricao}"; var prompt = language == "pt" ? - @"Você é um especialista em análise de software e QA. + @"Você é um especialista em análise de software e QA, mas também atende ao chat. PROJETO: {0} PERGUNTA: ""{1}"" CONTEXTO HIERÁRQUICO: {2} ETAPAS EXECUTADAS: {3} - Responda à pergunta de forma precisa e estruturada, aproveitando todo o contexto hierárquico coletado." : + Responda à pergunta de forma precisa e estruturada, aproveitando todo o contexto hierárquico coletado. Se for uma saudação ou não for uma pergunta relativa ao contexto, avise que não entendeu." : @"You are a software analysis and QA expert. diff --git a/Services/ResponseService/dxsmsw5a.kio~ b/Services/ResponseService/dxsmsw5a.kio~ new file mode 100644 index 0000000..922f2f8 --- /dev/null +++ b/Services/ResponseService/dxsmsw5a.kio~ @@ -0,0 +1,572 @@ +using ChatApi; +using ChatApi.Models; +using ChatRAG.Contracts.VectorSearch; +using ChatRAG.Data; +using ChatRAG.Models; +using ChatRAG.Services.Contracts; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Embeddings; +using System.Text.Json; + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +namespace ChatRAG.Services.ResponseService +{ + public class HierarchicalRAGService : 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; + + public HierarchicalRAGService( + ChatHistoryService chatHistoryService, + Kernel kernel, + TextFilter textFilter, + IProjectDataRepository projectDataRepository, + IChatCompletionService chatCompletionService, + IVectorSearchService vectorSearchService, + ILogger logger) + { + _chatHistoryService = chatHistoryService; + _kernel = kernel; + _textFilter = textFilter; + _projectDataRepository = projectDataRepository; + _chatCompletionService = chatCompletionService; + _vectorSearchService = vectorSearchService; + _logger = logger; + } + + public async Task GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt") + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + + try + { + // 1. Análise da query para determinar estratégia + var queryAnalysis = await AnalyzeQuery(question, language); + _logger.LogInformation("Query Analysis: {Strategy}, Complexity: {Complexity}", + queryAnalysis.Strategy, queryAnalysis.Complexity); + + // 2. Execução hierárquica baseada na análise + var context = await ExecuteHierarchicalSearch(question, projectId, queryAnalysis); + + // 3. Geração da resposta final + var response = await GenerateResponse(question, projectId, context, sessionId, language); + + stopWatch.Stop(); + return $"{response}\n\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s\nEtapas: {context.Steps.Count}"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro no RAG Hierárquico"); + stopWatch.Stop(); + return $"Erro: {ex.Message}\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s"; + } + } + + private async Task AnalyzeQuery(string question, string language) + { + var analysisPrompt = language == "pt" ? + @"Analise esta pergunta e classifique com precisão: + PERGUNTA: ""{0}"" + + Responda APENAS no formato JSON: + {{ + ""strategy"": ""overview|specific|detailed|out_of_scope"", + ""complexity"": ""simple|medium|complex"", + ""scope"": ""global|filtered|targeted"", + ""concepts"": [""conceito1"", ""conceito2""], + ""needs_hierarchy"": true|false, + }} + + DEFINIÇÕES PRECISAS: + + STRATEGY: + - overview: Pergunta sobre o PROJETO COMO UM TODO. Palavras-chave: ""projeto"", ""sistema"", ""aplicação"", ""este projeto"", ""todo o"", ""geral"", ""inteiro"". NÃO menciona módulos, funcionalidades ou tecnologias específicas. + - out_of_scope: Pergunta/frase sem sentido relacionada à projetos. Saudações, cumprimentos, perguntas sem sentido, etc. Palavras-chave: ""oi"", ""olá"", ""bom dia"", ""boa tarde"", ""quem"", ""quando"", ""etc"". NÃO menciona projetos e contém apenas uma saudação ou o usuário se apresentando, sem falar mais nada além disso. + - specific: Pergunta sobre MÓDULO/FUNCIONALIDADE ESPECÍFICA. Menciona: nome de classe, controller, entidade, CRUD específico, funcionalidade particular, tecnologia específica. + - detailed: Pergunta técnica específica que precisa de CONTEXTO PROFUNDO e detalhes de implementação. + - out_of_scope: Suadação, pergunta sem relação com os textos ou fora de contexto + + SCOPE: + - global: Busca informações de TODO o projeto (usar com overview ou com out_of_scope) + - filtered: Busca com filtros específicos (usar com specific/detailed) + - targeted: Busca muito específica e direcionada + + EXEMPLOS: + - ""Gere casos de teste para este projeto"" → overview/global + - ""Gere casos de teste do projeto"" → overview/global + - ""Gere casos de teste para o CRUD de usuário"" → specific/filtered + - ""Como implementar autenticação JWT neste controller"" → detailed/targeted + - ""Documente este sistema"" → overview/global + - ""Oi!"" → out_of_scope/global + - ""Boa tarde!"" → out_of_scope/global + - ""Meu nome é [nome de usuario]"" → out_of_scope/global + - ""Faça uma conta"" → out_of_scope/global + - ""Me passe a receita de bolo?"" → out_of_scope/global + - ""Explique a classe UserService"" → specific/filtered" : + + @"Analyze this question and classify precisely: + QUESTION: ""{0}"" + + Answer ONLY in JSON format: + {{ + ""strategy"": ""overview|specific|detailed|out_of_scope"", + ""complexity"": ""simple|medium|complex"", + ""scope"": ""global|filtered|targeted"", + ""concepts"": [""concept1"", ""concept2""], + ""needs_hierarchy"": true|false + }} + + PRECISE DEFINITIONS: + + STRATEGY: + - overview: Question about the PROJECT AS A WHOLE. Keywords: ""project"", ""system"", ""application"", ""this project"", ""entire"", ""general"", ""whole"". Does NOT mention specific modules, functionalities or technologies. + - out_of_scope: Meaningless question/phrase related to projects. Greetings, salutations, meaningless questions, etc. Keywords: ""hi"", ""hello"", ""good morning"", ""good afternoon"", ""who"", ""when"", ""etc"". Does NOT mention projects and contains only a greeting or the user introducing themselves, without saying anything else. + - specific: Question about SPECIFIC MODULE/FUNCTIONALITY. Mentions: class name, controller, entity, specific CRUD, particular functionality, specific technology. + - detailed: Technical specific question needing DEEP CONTEXT and implementation details. + + SCOPE: + - global: Search information from ENTIRE project (use with overview or out_of_scope) + - filtered: Search with specific filters (use with specific/detailed) + - targeted: Very specific and directed search + + EXAMPLES: + - ""Generate test cases for this project"" → overview/global + - ""Generate test cases for user CRUD"" → specific/filtered + - ""How to implement JWT authentication in this controller"" → detailed/targeted + - ""Document this system"" → overview/global + - ""Hi!"" → out_of_scope/global + - ""Good afternoon!"" → out_of_scope/global + - ""My name is [nome de usuario]"" → out_of_scope/global + - ""Do an operation math for me"" → out_of_scope/global + - ""Give me the recipe for a cake?"" → out_of_scope/global + - ""Explain the UserService class"" → specific/filtered"; + + var prompt = string.Format(analysisPrompt, question); + var executionSettings = new OpenAIPromptExecutionSettings + { + Temperature = 0.1, + MaxTokens = 300 // Aumentei um pouco para acomodar o prompt maior + }; + var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, executionSettings); + + try + { + var jsonResponse = response.Content?.Trim() ?? "{}"; + // Extrair JSON se vier com texto extra + var startIndex = jsonResponse.IndexOf('{'); + var endIndex = jsonResponse.LastIndexOf('}'); + if (startIndex >= 0 && endIndex >= startIndex) + { + jsonResponse = jsonResponse.Substring(startIndex, endIndex - startIndex + 1); + } + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var analysis = System.Text.Json.JsonSerializer.Deserialize(jsonResponse, options); + + // Log para debug - remover em produção + _logger.LogInformation($"Query: '{question}' → Strategy: {analysis?.Strategy}, Scope: {analysis?.Scope}"); + + 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) + { + var context = new HierarchicalContext(); + var embeddingService = _kernel.GetRequiredService(); + + switch (analysis.Strategy) + { + case "out_of_scope": + await ExecuteOutOfContextStrategy(context, question, projectId, embeddingService); + break; + case "overview": + await ExecuteOverviewStrategy(context, question, projectId, embeddingService); + break; + + case "detailed": + await ExecuteDetailedStrategy(context, question, projectId, embeddingService, analysis); + break; + + default: // specific + await ExecuteSpecificStrategy(context, question, projectId, embeddingService); + break; + } + + return context; + } + + private async Task ExecuteOutOfContextStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService) + { + context.AddStep("Buscando o projeto"); + var project = _projectDataRepository.GetAsync(projectId); + + context.CombinedContext = string.Join("\n\n", project); + } + + + + private async Task ExecuteOverviewStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService) + { + // Etapa 1: Buscar TODOS os documentos do projeto + context.AddStep("Buscando todos os documentos do projeto"); + var allProjectDocs = await _vectorSearchService.GetDocumentsByProjectAsync(projectId); + + // Etapa 2: Categorizar documentos por tipo/importância + context.AddStep("Categorizando e resumindo contexto do projeto"); + + // Etapa 2: Categorizar documentos por tipo baseado nos seus dados reais + context.AddStep("Categorizando e resumindo contexto do projeto"); + + var requirementsDocs = allProjectDocs.Where(d => + d.Title.ToLower().StartsWith("requisito") || + d.Title.ToLower().Contains("requisito") || + d.Content.ToLower().Contains("requisito") || + d.Content.ToLower().Contains("funcionalidade") || + d.Content.ToLower().Contains("aplicação deve") || + d.Content.ToLower().Contains("sistema deve")).ToList(); + + var architectureDocs = allProjectDocs.Where(d => + d.Title.ToLower().Contains("arquitetura") || + d.Title.ToLower().Contains("estrutura") || + d.Title.ToLower().Contains("documentação") || + d.Title.ToLower().Contains("readme") || + d.Content.ToLower().Contains("arquitetura") || + d.Content.ToLower().Contains("estrutura") || + d.Content.ToLower().Contains("tecnologia")).ToList(); + + // Documentos que não são requisitos nem arquitetura (códigos, outros docs) + var otherDocs = allProjectDocs + .Except(requirementsDocs) + .Except(architectureDocs) + .ToList(); + + // Etapa 3: Resumir cada categoria se tiver muitos documentos + var requirementsSummary = await SummarizeDocuments(requirementsDocs, "requisitos e funcionalidades do projeto"); + var architectureSummary = await SummarizeDocuments(architectureDocs, "arquitetura e documentação técnica"); + var otherSummary = await SummarizeDocuments(otherDocs, "outros documentos do projeto"); + + // Etapa 4: Busca específica para a pergunta (mantém precisão) + var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question)); + var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray(); + + context.AddStep("Identificando documentos específicos para a pergunta"); + var relevantDocs = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 8); + + // Etapa 5: Combinar resumos + documentos específicos + var contextParts = new List(); + + if (!string.IsNullOrEmpty(requirementsSummary)) + contextParts.Add($"RESUMO DOS REQUISITOS E FUNCIONALIDADES:\n{requirementsSummary}"); + + if (!string.IsNullOrEmpty(architectureSummary)) + contextParts.Add($"RESUMO DA ARQUITETURA E DOCUMENTAÇÃO:\n{architectureSummary}"); + + if (!string.IsNullOrEmpty(otherSummary)) + contextParts.Add($"OUTROS DOCUMENTOS DO PROJETO:\n{otherSummary}"); + + contextParts.Add($"DOCUMENTOS MAIS RELEVANTES PARA A PERGUNTA:\n{FormatResults(relevantDocs)}"); + + context.CombinedContext = string.Join("\n\n", contextParts); + } + + private async Task SummarizeDocuments(List documents, string category) + { + if (!documents.Any()) return string.Empty; + + // Se poucos documentos, usar todos sem resumir + if (documents.Count <= 3) + { + return FormatResults(documents); + } + + // Se muitos documentos, resumir em chunks + var chunks = documents.Chunk(5).ToList(); // Grupos de 5 documentos + var tasks = new List>(); + + // Semáforo para controlar concorrência (máximo 3 chamadas simultâneas) + var semaphore = new SemaphoreSlim(3, 3); + + foreach (var chunk in chunks) + { + var chunkContent = FormatResults(chunk); + + tasks.Add(Task.Run(async () => + { + await semaphore.WaitAsync(); + try + { + var summaryPrompt = $@"Resuma os pontos principais destes documentos sobre {category}: + + {chunkContent} + + Responda apenas com uma lista concisa dos pontos mais importantes:"; + + var response = await _chatCompletionService.GetChatMessageContentAsync( + summaryPrompt, + new OpenAIPromptExecutionSettings + { + Temperature = 0.1, + MaxTokens = 300 + }); + + return response.Content ?? string.Empty; + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Erro ao resumir chunk de {category}, usando conteúdo original"); + return chunkContent; + } + finally + { + semaphore.Release(); + } + })); + } + + // Aguardar todas as tasks de resumo + var summaries = await Task.WhenAll(tasks); + var validSummaries = summaries.Where(s => !string.IsNullOrEmpty(s)).ToList(); + + // Se tiver múltiplos resumos, consolidar + if (validSummaries.Count > 1) + { + var consolidationPrompt = $@"Consolide estes resumos sobre {category} em um resumo final: + + {string.Join("\n\n", validSummaries)} + + Responda com os pontos mais importantes organizados:"; + + try + { + var finalResponse = await _chatCompletionService.GetChatMessageContentAsync( + consolidationPrompt, + new OpenAIPromptExecutionSettings + { + Temperature = 0.1, + MaxTokens = 400 + }); + + return finalResponse.Content ?? string.Empty; + } + catch + { + return string.Join("\n\n", validSummaries); + } + } + + return validSummaries.FirstOrDefault() ?? string.Empty; + } + + private async Task ExecuteSpecificStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService) + { + // Etapa 1: Busca inicial por similaridade + context.AddStep("Busca inicial por similaridade"); + 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); + + if (initialResults.Any()) + { + context.AddStep("Expandindo contexto com documentos relacionados"); + + // Etapa 2: Expandir com contexto relacionado + var expandedContext = await ExpandContext(initialResults, projectId, embeddingService); + context.CombinedContext = $"CONTEXTO PRINCIPAL:\n{FormatResults(initialResults)}\n\nCONTEXTO EXPANDIDO:\n{FormatResults(expandedContext)}"; + } + else + { + context.AddStep("Fallback para busca ampla"); + var fallbackResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.2, 5); + context.CombinedContext = FormatResults(fallbackResults); + } + } + + private async Task ExecuteDetailedStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, QueryAnalysis analysis) + { + var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question)); + var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray(); + + // Etapa 1: Busca conceitual baseada nos conceitos identificados + context.AddStep("Busca conceitual inicial"); + 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); + } + } + + // Etapa 2: Busca direta pela pergunta + context.AddStep("Busca direta pela pergunta"); + var directResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 3); + + // Etapa 3: Síntese intermediária para identificar lacunas + context.AddStep("Identificando lacunas de conhecimento"); + var intermediateContext = FormatResults(conceptualResults.Concat(directResults).DistinctBy(r => r.Id)); + var gaps = await IdentifyKnowledgeGaps(question, intermediateContext); + + // Etapa 4: Busca complementar baseada nas lacunas + if (!string.IsNullOrEmpty(gaps)) + { + context.AddStep("Preenchendo lacunas de conhecimento"); + 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); + + 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> ExpandContext(List initialResults, string projectId, ITextEmbeddingGenerationService embeddingService) + { + var expandedResults = new List(); + + // Para cada resultado inicial, buscar documentos relacionados + foreach (var result in initialResults.Take(2)) // Limitar para evitar explosão de contexto + { + 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 IdentifyKnowledgeGaps(string question, string currentContext) + { + var gapPrompt = @"Baseado na pergunta e contexto atual, identifique que informações ainda faltam para uma resposta completa. + + PERGUNTA: {0} + CONTEXTO ATUAL: {1} + + Responda APENAS com palavras-chave dos conceitos/informações que ainda faltam, separados por vírgula. + Se o contexto for suficiente, responda 'SUFICIENTE'."; + + var prompt = string.Format(gapPrompt, question, currentContext.Substring(0, Math.Min(1000, currentContext.Length))); + + var executionSettings = new OpenAIPromptExecutionSettings + { + Temperature = 0.2, + MaxTokens = 100 + }; + + var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, executionSettings); + var gaps = response.Content?.Trim() ?? ""; + + return gaps.Equals("SUFICIENTE", StringComparison.OrdinalIgnoreCase) ? "" : gaps; + } + + private async Task GenerateResponse(string question, string projectId, HierarchicalContext context, string sessionId, string language) + { + var projectData = await _projectDataRepository.GetAsync(projectId); + var project = $"Nome: {projectData.Nome} \n\n Descrição:{projectData.Descricao}"; + + var prompt = language == "pt" ? + @"Você é um especialista em análise de software e QA, mas também atende ao chat. + + PROJETO: {0} + PERGUNTA: ""{1}"" + CONTEXTO HIERÁRQUICO: {2} + ETAPAS EXECUTADAS: {3} + + Responda à pergunta de forma precisa e estruturada, aproveitando todo o contexto hierárquico coletado." : + + @"You are a software analysis and QA expert. + + PROJECT: {0} + QUESTION: ""{1}"" + HIERARCHICAL CONTEXT: {2} + EXECUTED STEPS: {3} + + Answer the question precisely and structured, leveraging all the hierarchical context collected."; + + var finalPrompt = string.Format(prompt, project, question, context.CombinedContext, + string.Join(" → ", context.Steps)); + + var history = _chatHistoryService.GetSumarizer(sessionId); + history.AddUserMessage(finalPrompt); + + var executionSettings = new OpenAIPromptExecutionSettings + { + Temperature = 0.7, + TopP = 1.0, + FrequencyPenalty = 0, + PresencePenalty = 0 + }; + + var response = await _chatCompletionService.GetChatMessageContentAsync(history, executionSettings); + history.AddMessage(response.Role, response.Content ?? ""); + _chatHistoryService.UpdateHistory(sessionId, history); + + return response.Content ?? ""; + } + + private string FormatResults(IEnumerable results) + { + return string.Join("\n\n", results.Select((item, index) => + $"=== DOCUMENTO {index + 1} ===\n" + + $"Relevância: {item.Score:P1}\n" + + $"Conteúdo: {item.Content}")); + } + + public Task GetResponse(UserData userData, string projectId, string sessionId, string question) + { + return GetResponse(userData, projectId, sessionId, question, "pt"); + } + } + + // Classes de apoio para o RAG Hierárquico + public class QueryAnalysis + { + public string Strategy { get; set; } = "specific"; + public string Complexity { get; set; } = "medium"; + public string Scope { get; set; } = "filtered"; + public string[] Concepts { get; set; } = Array.Empty(); + public bool Needs_Hierarchy { get; set; } = false; + } + + public class HierarchicalContext + { + public List Steps { get; set; } = new(); + public string CombinedContext { get; set; } = ""; + public Dictionary Metadata { get; set; } = new(); + + public void AddStep(string step) + { + Steps.Add($"{DateTime.Now:HH:mm:ss} - {step}"); + } + } + +} +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/Services/SearchVectors/VectorDatabaseFactory.cs b/Services/SearchVectors/VectorDatabaseFactory.cs index 7a83e11..3751e82 100644 --- a/Services/SearchVectors/VectorDatabaseFactory.cs +++ b/Services/SearchVectors/VectorDatabaseFactory.cs @@ -68,12 +68,19 @@ namespace ChatRAG.Services.SearchVectors // Verificar se deve usar RAG Hierárquico var configuration = _serviceProvider.GetService(); var useHierarchical = configuration?.GetValue("Features:UseHierarchicalRAG") ?? false; + var useConfidenceAware = configuration?.GetValue("Features:UseConfidenceAwareRAG") ?? false; - if (useHierarchical) + if (useHierarchical && !useConfidenceAware) { _logger.LogInformation("Usando HierarchicalRAGService"); return GetService(); } + + if (useConfidenceAware) + { + _logger.LogInformation("Usando ConfidenceAwareRAGService"); + return GetService(); + } // Usar estratégia baseada no provider ou configuração var ragStrategy = configuration?.GetValue("Features:RAGStrategy"); diff --git a/Settings/ConfidenceAwareSettings.cs b/Settings/ConfidenceAwareSettings.cs new file mode 100644 index 0000000..2102103 --- /dev/null +++ b/Settings/ConfidenceAwareSettings.cs @@ -0,0 +1,120 @@ +namespace ChatRAG.Settings +{ + /// + /// Configurações específicas para o ConfidenceAwareRAG + /// + public class ConfidenceAwareSettings + { + /// + /// Habilita/desabilita verificação de confiança + /// true = só responde com confiança, false = sempre responde (como hoje) + /// + public bool EnableConfidenceCheck { get; set; } = true; + + /// + /// Modo restrito (critérios rigorosos) vs modo relaxado + /// + public bool UseStrictMode { get; set; } = true; + + /// + /// Mostra informações de debug na resposta (confiança, tempo, etc.) + /// + public bool ShowDebugInfo { get; set; } = false; + + /// + /// Domínio padrão quando não conseguir detectar automaticamente + /// + public string DefaultDomain { get; set; } = "TI"; + + /// + /// Mapeamento de palavras-chave para domínios (detecção automática) + /// + public Dictionary DomainMappings { get; set; } = new() + { + ["software"] = "TI", + ["sistema"] = "TI", + ["api"] = "TI", + ["backend"] = "TI", + ["frontend"] = "TI", + ["database"] = "TI", + ["funcionário"] = "RH", + ["colaborador"] = "RH", + ["employee"] = "RH", + ["hr"] = "RH", + ["financeiro"] = "Financeiro", + ["contábil"] = "Financeiro", + ["financial"] = "Financeiro", + ["accounting"] = "Financeiro", + ["teste"] = "QA", + ["qualidade"] = "QA", + ["quality"] = "QA", + ["testing"] = "QA" + }; + + /// + /// Configuração de idiomas suportados + /// + public LanguageSettings Languages { get; set; } = new(); + + /// + /// Configurações de cache para prompts + /// + public CacheSettings Cache { get; set; } = new(); + } + + /// + /// Configurações de idioma + /// + public class LanguageSettings + { + /// + /// Idioma padrão do sistema + /// + public string DefaultLanguage { get; set; } = "pt"; + + /// + /// Idiomas suportados + /// + public List SupportedLanguages { get; set; } = new() { "pt", "en" }; + + /// + /// Auto-detectar idioma da pergunta + /// + public bool AutoDetectLanguage { get; set; } = true; + + /// + /// Sempre responder no idioma detectado/solicitado, mesmo que prompts estejam em PT + /// + public bool AlwaysRespondInRequestedLanguage { get; set; } = true; + + /// + /// Palavras-chave para detecção automática de idioma + /// + public Dictionary> LanguageKeywords { get; set; } = new() + { + ["en"] = new() { "what", "how", "why", "where", "when", "which", "who", "can", "could", "would", "should", "will", "the", "and", "or", "but", "system", "project", "document", "explain", "generate" }, + ["pt"] = new() { "que", "como", "por", "onde", "quando", "qual", "quem", "pode", "poderia", "deveria", "será", "o", "a", "e", "ou", "mas", "sistema", "projeto", "documento", "explique", "gere" } + }; + } + + /// + /// Configurações de cache + /// + public class CacheSettings + { + /// + /// Habilitar cache de prompts carregados + /// + public bool EnablePromptCache { get; set; } = true; + + /// + /// Tempo de cache em minutos + /// + public int CacheExpirationMinutes { get; set; } = 30; + + /// + /// Recarregar arquivos automaticamente quando modificados + /// + public bool AutoReloadOnFileChange { get; set; } = true; + } +} diff --git a/Settings/ConfidenceSettings.cs b/Settings/ConfidenceSettings.cs new file mode 100644 index 0000000..242e88d --- /dev/null +++ b/Settings/ConfidenceSettings.cs @@ -0,0 +1,126 @@ +using System.ComponentModel.DataAnnotations; + +namespace ChatRAG.Settings +{ + /// + /// Configurações para verificação de confiança por estratégia + /// + public class ConfidenceSettings + { + public bool StrictModeByDefault { get; set; } = true; + + [Required] + public Dictionary Thresholds { get; set; } = new() + { + ["overview"] = new ConfidenceThresholds + { + MinDocuments = 5, + MinRelevantDocuments = 3, + MinHighQualityDocuments = 1, + MinContextLength = 1000, + MinOverallScore = 0.3, + MinMaxScore = 0.4, + MinAverageScore = 0.3 + }, + ["specific"] = new ConfidenceThresholds + { + MinDocuments = 2, + MinRelevantDocuments = 2, + MinHighQualityDocuments = 1, + MinContextLength = 500, + MinOverallScore = 0.4, + MinMaxScore = 0.5, + MinAverageScore = 0.4 + }, + ["detailed"] = new ConfidenceThresholds + { + MinDocuments = 3, + MinRelevantDocuments = 2, + MinHighQualityDocuments = 2, + MinContextLength = 800, + MinOverallScore = 0.5, + MinMaxScore = 0.6, + MinAverageScore = 0.5 + } + }; + + /// + /// Mensagens de fallback por idioma + /// + public Dictionary FallbackMessages { get; set; } = new() + { + ["pt"] = new ConfidenceFallbackMessages + { + NoDocuments = "Não encontrei informações sobre isso no projeto atual. Você poderia reformular a pergunta ou ser mais específico?", + NoRelevantDocuments = "Encontrei alguns documentos, mas nenhum parece diretamente relacionado à sua pergunta. Pode tentar ser mais específico ou usar outras palavras-chave?", + InsufficientOverview = "Não tenho informações suficientes para fornecer uma visão geral completa do projeto. Talvez você possa fazer uma pergunta mais específica?", + InsufficientSpecific = "Não encontrei documentação suficiente sobre esse tópico específico. Você pode tentar reformular a pergunta?", + InsufficientDetailed = "Preciso de mais contexto técnico para responder adequadamente. Você pode ser mais específico sobre o que está procurando?", + Generic = "Não tenho informações suficientes para responder com segurança. Pode tentar reformular a pergunta?" + }, + ["en"] = new ConfidenceFallbackMessages + { + NoDocuments = "I couldn't find information about this in the current project. Could you rephrase the question or be more specific?", + NoRelevantDocuments = "I found some documents, but none seem directly related to your question. Could you try being more specific or use different keywords?", + InsufficientOverview = "I don't have enough information to provide a complete project overview. Perhaps you could ask a more specific question?", + InsufficientSpecific = "I didn't find sufficient documentation about this specific topic. Could you try rephrasing the question?", + InsufficientDetailed = "I need more technical context to respond adequately. Could you be more specific about what you're looking for?", + Generic = "I don't have enough information to respond safely. Could you try rephrasing the question?" + } + }; + } + + /// + /// Thresholds de confiança para uma estratégia específica + /// + public class ConfidenceThresholds + { + /// + /// Número mínimo de documentos encontrados + /// + public int MinDocuments { get; set; } + + /// + /// Número mínimo de documentos relevantes (score >= 0.3) + /// + public int MinRelevantDocuments { get; set; } + + /// + /// Número mínimo de documentos de alta qualidade (score >= 0.6) + /// + public int MinHighQualityDocuments { get; set; } + + /// + /// Tamanho mínimo do contexto combinado (caracteres) + /// + public int MinContextLength { get; set; } + + /// + /// Score geral mínimo (0.0 a 1.0) + /// + public double MinOverallScore { get; set; } + + /// + /// Score máximo mínimo entre os documentos encontrados + /// + public double MinMaxScore { get; set; } + + /// + /// Score médio mínimo entre os documentos encontrados + /// + public double MinAverageScore { get; set; } + } + + /// + /// Mensagens de fallback quando não há confiança suficiente + /// + public class ConfidenceFallbackMessages + { + public string NoDocuments { get; set; } = ""; + public string NoRelevantDocuments { get; set; } = ""; + public string InsufficientOverview { get; set; } = ""; + public string InsufficientSpecific { get; set; } = ""; + public string InsufficientDetailed { get; set; } = ""; + public string Generic { get; set; } = ""; + } +} diff --git a/appsettings.Production.json b/appsettings.Production.json index 9e4b091..34aa977 100644 --- a/appsettings.Production.json +++ b/appsettings.Production.json @@ -1,15 +1,35 @@ { - "DomvsDatabase": { - //"ConnectionString": "mongodb://192.168.0.82:30017/?directConnection=true", - "ConnectionString": "mongodb://localhost:27017/?directConnection=true", - "DatabaseName": "DomvsSites", - "SharepointCollectionName": "SharepointSite", - "ChatBotRHCollectionName": "ChatBotRHData", - "ClassifierCollectionName": "ClassifierData" + "VectorDatabase": { + "Provider": "Qdrant", + "MongoDB": { + "ConnectionString": "mongodb://admin:c4rn31r0@k3sw2:27017,k3ss1:27017/?authSource=admin", + "DatabaseName": "RAGProjects-dev-pt", + "TextCollectionName": "Texts", + "ProjectCollectionName": "Groups", + "UserDataName": "UserData" + }, + "Qdrant": { + "Host": "192.168.0.100", + "Port": 6334, + "CollectionName": "texts-whats", + "GroupsCollectionName": "projects-whats", + "VectorSize": 384, + "Distance": "Cosine", + "HnswM": 16, + "HnswEfConstruct": 200, + "OnDisk": false + }, + "Chroma": { + "Host": "localhost", + "Port": 8000, + "CollectionName": "rag_documents" + } }, - "ChatRHSettings": { - "Url": "http://localhost:8070/", - "Create": "/CallRH" + "Features": { + "UseQdrant": true, + "UseHierarchicalRAG": true, + "UseConfidenceAwareRAG": true, + "EnableConfidenceCheck": false }, "Logging": { "LogLevel": { diff --git a/appsettings.json b/appsettings.json index 540a24e..01df49b 100644 --- a/appsettings.json +++ b/appsettings.json @@ -16,10 +16,10 @@ "UserDataName": "UserData" }, "Qdrant": { - "Host": "localhost", + "Host": "192.168.0.100", "Port": 6334, - "CollectionName": "texts", - "GroupsCollectionName": "projects", + "CollectionName": "texts-whats", + "GroupsCollectionName": "projects-whats", "VectorSize": 384, "Distance": "Cosine", "HnswM": 16, @@ -34,7 +34,60 @@ }, "Features": { "UseQdrant": true, - "UseHierarchicalRAG": true + "UseHierarchicalRAG": true, + "UseConfidenceAwareRAG": true, + "EnableConfidenceCheck": false + }, + + "ConfidenceAware": { + "EnableConfidenceCheck": true, + "UseStrictMode": true, + "ShowDebugInfo": false, + "DefaultDomain": "Servicos", + "Languages": { + "DefaultLanguage": "pt", + "AutoDetectLanguage": true + }, + "DomainMappings": { + "software": "TI", + "sistema": "TI", + "funcionário": "RH", + "colaborador": "RH", + "financeiro": "Financeiro", + "contábil": "Financeiro", + "teste": "QA", + "qualidade": "QA" + } + }, + + "Confidence": { + "Thresholds": { + "overview": { + "MinDocuments": 2, // reduzido para chatbot + "MinRelevantDocuments": 1, + "MinOverallScore": 0.25 // mais flexível + }, + "specific": { + "MinDocuments": 1, // bem flexível + "MinRelevantDocuments": 1, + "MinOverallScore": 0.3 + }, + "detailed": { + "MinDocuments": 1, // bem flexível + "MinMaxScore": 0.5, + "MinRelevantDocuments": 1, + "MinOverallScore": 0.3 + } + }, + "FallbackMessages": { + "pt": { + "NoDocuments": "Desculpe, não encontrei informações sobre esse serviço. Você pode falar com nosso atendente para mais detalhes.", + "Generic": "Não tenho informações suficientes sobre isso. Posso te conectar com um especialista?" + } + } + }, + "PromptConfiguration": { + "Path": "Configuration/Prompts" }, "AllowedHosts": "*", "AppTenantId": "20190830-5fd4-4a72-b8fd-1c1cb35b25bc", diff --git a/vvijr2kk.3vs~ b/vvijr2kk.3vs~ new file mode 100644 index 0000000..1eeaa15 --- /dev/null +++ b/vvijr2kk.3vs~ @@ -0,0 +1,343 @@ +using ChatApi; +using ChatApi.Data; +using ChatApi.Middlewares; +using ChatApi.Services.Crypt; +using ChatApi.Settings; +using ChatRAG.Contracts.VectorSearch; +using ChatRAG.Data; +using ChatRAG.Extensions; +using ChatRAG.Services; +using ChatRAG.Services.Confidence; +using ChatRAG.Services.Contracts; +using ChatRAG.Services.PromptConfiguration; +using ChatRAG.Services.ResponseService; +using ChatRAG.Services.SearchVectors; +using ChatRAG.Services.TextServices; +using ChatRAG.Settings; +using ChatRAG.Settings.ChatRAG.Configuration; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Microsoft.SemanticKernel; +using System.Text; +using static OllamaSharp.OllamaApiClient; +using static System.Net.Mime.MediaTypeNames; +using static System.Net.WebRequestMethods; + +#pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +// Adicionar servio CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowSpecificOrigin", + builder => + { + builder + .WithOrigins("http://localhost:5094") // Sua origem especfica + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "apichat", Version = "v1" }); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"", + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); + +builder.Services.Configure( +builder.Configuration.GetSection("Confidence")); + +builder.Services.Configure( +builder.Configuration.GetSection("ConfidenceAware")); + + +//builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddVectorDatabase(builder.Configuration); + +builder.Services.AddScoped(provider => +{ + var useQdrant = builder.Configuration["Features:UseQdrant"] == "true"; + var factory = provider.GetRequiredService(); + return factory.CreateVectorSearchService(); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(provider => +{ + var database = builder.Configuration["VectorDatabase:Provider"]; + if (string.IsNullOrEmpty(database)) + { + throw new InvalidOperationException("VectorDatabase:Provider is not configured."); + } + else if (database.Equals("Qdrant", StringComparison.OrdinalIgnoreCase)) + { + return provider.GetRequiredService(); + } + else if (database.Equals("MongoDB", StringComparison.OrdinalIgnoreCase)) + { + return provider.GetRequiredService(); + } + else if (database.Equals("Chroma", StringComparison.OrdinalIgnoreCase)) + { + return provider.GetRequiredService(); + } + return provider.GetRequiredService(); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(provider => +{ + var database = builder.Configuration["VectorDatabase:Provider"]; + if (string.IsNullOrEmpty(database)) + { + throw new InvalidOperationException("VectorDatabase:Provider is not configured."); + } + else if (database.Equals("Qdrant", StringComparison.OrdinalIgnoreCase)) + { + return provider.GetRequiredService(); + } + else if (database.Equals("MongoDB", StringComparison.OrdinalIgnoreCase)) + { + return provider.GetRequiredService(); + } + else if (database.Equals("Chroma", StringComparison.OrdinalIgnoreCase)) + { + return provider.GetRequiredService(); + } + return provider.GetRequiredService(); +}); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +//builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(provider => +{ + var configuration = provider.GetService(); + var useHierarchical = configuration?.GetValue("Features:UseHierarchicalRAG") ?? false; + var useConfidence = configuration?.GetValue("Features:UseConfidenceAwareRAG") ?? false; + + return useConfidence && useHierarchical + ? provider.GetRequiredService() + : useHierarchical + ? provider.GetRequiredService() + : provider.GetRequiredService(); +}); + +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddSingleton(); + +// Registrar servios de confiana +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +// Registrar ConfidenceAwareRAGService +builder.Services.AddScoped(); + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin"); + +//Olllama +//Desktop +var key = "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"; +//uilder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:11434")); +//var model = "llama-3.3-70b-versatile"; +var model = "llama-3.1-8b-instant"; +//var model = "meta-llama/llama-guard-4-12b"; +//var url = "https://api.groq.com/openai/v1/chat/completions"; // Adicione o /v1/openai +var url = "https://api.groq.com/openai/v1"; +builder.Services.AddOpenAIChatCompletion(model, new Uri(url), key); + +//Notebook +//var model = "meta-llama/Llama-3.2-3B-Instruct"; +//var url = "https://api.deepinfra.com/v1/openai"; // Adicione o /v1/openai +//builder.Services.AddOpenAIChatCompletion(model, new Uri(url), "HedaR4yPrp9N2XSHfwdZjpZvPIxejPFK"); + +//builder.Services.AddOllamaChatCompletion("llama3.2:3b", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("llama3.2:1b", new Uri("http://localhost:11435")); + + +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("starling-lm", new Uri("http://localhost:11435")); + +//ServerSpace - GPT Service +//builder.Services.AddOpenAIChatCompletion("openchat-3.5-0106", new Uri("https://gpt.serverspace.com.br/v1/chat/completions"), "tIAXVf3AkCkkpSX+PjFvktfEeSPyA1ZYam50UO3ye/qmxVZX6PIXstmJsLZXkQ39C33onFD/81mdxvhbGHm7tQ=="); + +//Ollama local server (scorpion) +//builder.Services.AddOllamaChatCompletion("llama3.1:latest", new Uri("http://192.168.0.150:11434")); + +//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://192.168.0.150:11434")); + +//Desktop +//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11434")); +//Notebook +builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435")); + + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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"); +//builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8"); + +//Anthropic / Claude +//builder.Services.AddAnthropicChatCompletion( +// modelId: "claude-3-5-sonnet-latest", // ou outro modelo Claude desejado +// apiKey: "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" +//); + + +builder.Services.AddKernel(); + +//builder.Services.AddKernel() +// .AddOllamaChatCompletion("phi3", new Uri("http://localhost:11435")) +// .AddOllamaTextEmbeddingGeneration() +// .Build(); + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://192.168.0.150:11436")); + +builder.Services.AddHttpClient(); + +var tenantId = builder.Configuration.GetSection("AppTenantId"); +var clientId = builder.Configuration.GetSection("AppClientID"); + +//builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +// .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddControllers(); + +//builder.Services.AddAuthentication(options => +// { +// options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +// }) +// .AddJwtBearer(options => +// { +// // Configuraes anteriores... + +// // Eventos para log e tratamento de erros +// options.Events = new JwtBearerEvents +// { +// OnAuthenticationFailed = context => +// { +// // Log de erros de autenticao +// Console.WriteLine($"Erro de autenticao: {context.Exception.Message}"); +// return Task.CompletedTask; +// }, +// OnTokenValidated = context => +// { +// // Validaes adicionais se necessrio +// return Task.CompletedTask; +// } +// }; +// }); + +builder.Services.AddSingleton(builder.Configuration); + +builder.Services.Configure(options => +{ + options.MaxRequestBodySize = int.MaxValue; +}); + +builder.Services.Configure(options => +{ + options.Limits.MaxRequestBodySize = int.MaxValue; +}); + +builder.Services.Configure(options => +{ + options.ValueLengthLimit = int.MaxValue; + options.MultipartBodyLengthLimit = int.MaxValue; + options.MultipartHeadersLengthLimit = int.MaxValue; +}); + +var app = builder.Build(); + + + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseDeveloperExceptionPage(); // Isso mostra erros detalhados +} + +//app.UseHttpsRedirection(); + +app.MapControllers(); + +app.Use(async (context, next) => +{ + var cookieOpt = new CookieOptions() + { + Path = "/", + Expires = DateTimeOffset.UtcNow.AddDays(1), + IsEssential = true, + HttpOnly = false, + Secure = false, + }; + + await next(); +}); + +app.UseMiddleware(); + +app.UseCors("AllowSpecificOrigin"); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.Run(); + +#pragma warning restore SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.