ChatRAG/Services/PromptConfiguration/PromptConfigurationService.cs
2025-06-22 19:58:43 -03:00

754 lines
33 KiB
C#

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
{
/// <summary>
/// Serviço para configuração e carregamento de prompts por domínio e idioma
/// </summary>
public class PromptConfigurationService
{
private readonly ILogger<PromptConfigurationService> _logger;
private readonly string _configurationPath;
private readonly LanguageSettings _languageSettings;
private readonly CacheSettings _cacheSettings;
private Dictionary<string, DomainPromptConfig> _domainConfigs = new();
private BasePromptConfig _baseConfig = new();
private readonly Dictionary<string, DateTime> _fileLastModified = new();
private readonly object _lockObject = new object();
public PromptConfigurationService(
ILogger<PromptConfigurationService> logger,
IConfiguration configuration,
IOptions<ConfidenceAwareSettings> 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);
}
/// <summary>
/// Obtém prompts configurados para um domínio e idioma específicos
/// </summary>
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)
};
}
/// <summary>
/// Detecta o domínio baseado na pergunta e descrição do projeto
/// </summary>
public string? DetectDomain(string question, string? projectDescription = null)
{
var content = $"{question} {projectDescription}".ToLower();
var domainScores = new Dictionary<string, int>();
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;
}
/// <summary>
/// Detecta o idioma da pergunta
/// </summary>
public string DetectLanguage(string question)
{
if (string.IsNullOrWhiteSpace(question))
return _languageSettings.DefaultLanguage;
var languageScores = new Dictionary<string, int>();
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;
}
/// <summary>
/// Lista domínios disponíveis
/// </summary>
public List<string> GetAvailableDomains()
{
return _domainConfigs.Keys.ToList();
}
/// <summary>
/// Lista idiomas suportados
/// </summary>
public List<string> GetSupportedLanguages()
{
return _languageSettings.SupportedLanguages;
}
/// <summary>
/// Força recarregamento das configurações
/// </summary>
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<BasePromptConfig>(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<string, DomainPromptConfig>();
foreach (var file in domainFiles)
{
try
{
var json = File.ReadAllText(file);
var config = JsonSerializer.Deserialize<DomainPromptConfig>(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<string> { 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<string, DomainPromptConfig>
{
["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<string, Dictionary<string, string>>
{
["pt"] = new Dictionary<string, string>
{
["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<string, string>
{
["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<string, Dictionary<string, string>>
{
["pt"] = new Dictionary<string, string>
{
["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<string, string>
{
["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<string, Dictionary<string, string>>
{
["pt"] = new Dictionary<string, string>
{
["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<string, string>
{
["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<string, Dictionary<string, string>>
{
["pt"] = new Dictionary<string, string>
{
["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<string, string>
{
["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<string, Dictionary<string, string>>
{
["pt"] = new Dictionary<string, string>
{
["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<string, string>
{
["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
/// </summary>
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");
}
}
}
/// <summary>
}
// === 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<string, Dictionary<string, string>> Prompts { get; set; } = new();
}
public class DomainPromptConfig
{
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public List<string> Keywords { get; set; } = new();
public List<string> Concepts { get; set; } = new();
public Dictionary<string, Dictionary<string, string>> Prompts { get; set; } = new();
}
}