394 lines
17 KiB
C#
394 lines
17 KiB
C#
|
|
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;
|
|
|
|
#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 ResponseRAGService : IResponseService
|
|
{
|
|
private readonly ChatHistoryService _chatHistoryService;
|
|
private readonly Kernel _kernel;
|
|
private readonly TextFilter _textFilter;
|
|
private readonly TextDataRepository _textDataRepository;
|
|
private readonly IProjectDataRepository _projectDataRepository;
|
|
private readonly IChatCompletionService _chatCompletionService;
|
|
private readonly IVectorSearchService _vectorSearchService;
|
|
|
|
public ResponseRAGService(
|
|
ChatHistoryService chatHistoryService,
|
|
Kernel kernel,
|
|
TextFilter textFilter,
|
|
TextDataRepository textDataRepository,
|
|
IProjectDataRepository projectDataRepository,
|
|
IChatCompletionService chatCompletionService,
|
|
IVectorSearchService vectorSearchService,
|
|
ITextDataService textDataService)
|
|
{
|
|
this._chatHistoryService = chatHistoryService;
|
|
this._kernel = kernel;
|
|
this._textFilter = textFilter;
|
|
this._textDataRepository = textDataRepository;
|
|
this._projectDataRepository = projectDataRepository;
|
|
this._chatCompletionService = chatCompletionService;
|
|
this._vectorSearchService = vectorSearchService;
|
|
}
|
|
|
|
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt")
|
|
{
|
|
var stopWatch = new System.Diagnostics.Stopwatch();
|
|
stopWatch.Start();
|
|
|
|
var searchStrategy = await ClassificarEstrategiaDeBusca(question, language);
|
|
|
|
string resposta;
|
|
switch (searchStrategy)
|
|
{
|
|
case SearchStrategy.TodosProjeto:
|
|
resposta = await BuscarTodosRequisitosDoProjeto(question, projectId);
|
|
break;
|
|
case SearchStrategy.SimilaridadeComFiltro:
|
|
resposta = await BuscarTopTextosRelacionadosComInterface(question, projectId);
|
|
break;
|
|
case SearchStrategy.SimilaridadeGlobal:
|
|
resposta = await BuscarTopTextosRelacionados(question, projectId);
|
|
break;
|
|
default:
|
|
resposta = await BuscarTopTextosRelacionadosComInterface(question, projectId);
|
|
break;
|
|
}
|
|
|
|
var projectData = await _projectDataRepository.GetAsync(projectId);
|
|
|
|
var project = $"Nome: {projectData.Nome} \n\n Descrição:{projectData.Descricao}";
|
|
|
|
//question = $"Para responder à solicitação/pergunta: \"{question}\" por favor, considere o projeto: \"{project}\" e os requisitos: \"{resposta}\"";
|
|
// Base prompt template
|
|
string basePrompt = @"You are a QA professional. Generate ONLY what the user requests.
|
|
Project Context: {0}
|
|
Requirements: {1}
|
|
User Request: ""{2}""
|
|
|
|
Focus exclusively on the user's request. Do not add summaries, explanations, or additional content unless specifically asked.";
|
|
|
|
if (language == "pt")
|
|
{
|
|
basePrompt = @"Você é um profissional de QA. Gere APENAS o que o usuário solicitar.
|
|
Contexto do Projeto: {0}
|
|
Requisitos: {1}
|
|
Solicitação do Usuário: ""{2}""
|
|
|
|
Foque exclusivamente na solicitação do usuário. Não adicione resumos, explicações ou conteúdo adicional, a menos que especificamente solicitado.";
|
|
}
|
|
// Usage
|
|
question = string.Format(basePrompt, project, resposta, question);
|
|
|
|
ChatHistory history = _chatHistoryService.GetSumarizer(sessionId);
|
|
|
|
history.AddUserMessage(question);
|
|
|
|
var executionSettings = new OpenAIPromptExecutionSettings
|
|
{
|
|
Temperature = 0.8,
|
|
TopP = 1.0,
|
|
FrequencyPenalty = 0,
|
|
PresencePenalty = 0
|
|
};
|
|
|
|
var response = await _chatCompletionService.GetChatMessageContentAsync(history, executionSettings);
|
|
history.AddMessage(response.Role, response.Content ?? "");
|
|
|
|
_chatHistoryService.UpdateHistory(sessionId, history);
|
|
|
|
stopWatch.Stop();
|
|
return $"{response.Content ?? ""}\n\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s";
|
|
|
|
}
|
|
|
|
private async Task<SearchStrategy> ClassificarEstrategiaDeBusca(string question, string language)
|
|
{
|
|
string prompt = language == "pt" ?
|
|
@"TAREFA: Classificar estratégia de busca
|
|
ENTRADA: ""{0}""
|
|
|
|
REGRAS OBRIGATÓRIAS:
|
|
- Se menciona ""projeto"" sem especificar módulos/aspectos → TODOS_PROJETO
|
|
- Se menciona ""todo"", ""todos"", ""completo"", ""geral"" → TODOS_PROJETO
|
|
- Se menciona aspectos específicos como ""usuário"", ""login"", ""pagamento"" → SIMILARIDADE_FILTRADA
|
|
- Se pergunta ""como funciona"" algo específico → SIMILARIDADE_GLOBAL
|
|
|
|
EXEMPLOS OBRIGATÓRIOS:
|
|
""gere casos de teste para o projeto"" → TODOS_PROJETO
|
|
""gere resumo do projeto"" → TODOS_PROJETO
|
|
""gere lista de tarefas para este projeto"" → TODOS_PROJETO
|
|
""casos de teste para usuários"" → SIMILARIDADE_FILTRADA
|
|
""como funciona validação CPF"" → SIMILARIDADE_GLOBAL
|
|
|
|
RESPOSTA OBRIGATÓRIA (copie exatamente): TODOS_PROJETO, SIMILARIDADE_FILTRADA ou SIMILARIDADE_GLOBAL" :
|
|
|
|
@"TASK: Classify search strategy
|
|
INPUT: ""{0}""
|
|
|
|
MANDATORY RULES:
|
|
- If mentions ""project"" without specifying modules/aspects → ALL_PROJECT
|
|
- If mentions ""all"", ""entire"", ""complete"", ""overview"" → ALL_PROJECT
|
|
- If mentions specific aspects like ""user"", ""login"", ""payment"" → FILTERED_SIMILARITY
|
|
- If asks ""how does"" something specific work → GLOBAL_SIMILARITY
|
|
|
|
MANDATORY EXAMPLES:
|
|
""generate test cases for the project"" → ALL_PROJECT
|
|
""generate project summary"" → ALL_PROJECT
|
|
""generate task list for this project"" → ALL_PROJECT
|
|
""test cases for users"" → FILTERED_SIMILARITY
|
|
""how does CPF validation work"" → GLOBAL_SIMILARITY
|
|
|
|
MANDATORY RESPONSE (copy exactly): ALL_PROJECT, FILTERED_SIMILARITY or GLOBAL_SIMILARITY";
|
|
|
|
var classificationPrompt = string.Format(prompt, question);
|
|
|
|
var executionSettings = new OpenAIPromptExecutionSettings
|
|
{
|
|
Temperature = 0.0, // Mais determinístico
|
|
MaxTokens = 50, // Resposta curta
|
|
TopP = 1.0,
|
|
FrequencyPenalty = 0,
|
|
PresencePenalty = 0
|
|
};
|
|
|
|
// Aqui você faria a chamada para o Ollama
|
|
var resp = await _chatCompletionService.GetChatMessageContentAsync(classificationPrompt, executionSettings);
|
|
//var classification = await _ollamaService.GetResponse(classificationPrompt);
|
|
var classification = resp.Content ?? "";
|
|
|
|
return classification.ToUpper().Contains("TODOS") || classification.ToUpper().Contains("ALL") ?
|
|
SearchStrategy.TodosProjeto :
|
|
classification.ToUpper().Contains("FILTRADA") || classification.ToUpper().Contains("FILTERED") ?
|
|
SearchStrategy.SimilaridadeComFiltro :
|
|
SearchStrategy.SimilaridadeGlobal;
|
|
}
|
|
|
|
async Task<string> BuscarTextoRelacionado(string pergunta)
|
|
{
|
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
|
var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(pergunta));
|
|
var embeddingArrayPergunta = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
var textos = await _textDataRepository.GetAsync();
|
|
|
|
TextoComEmbedding melhorTexto = null;
|
|
double melhorSimilaridade = -1.0;
|
|
|
|
foreach (var texto in textos)
|
|
{
|
|
double similaridade = CalcularSimilaridadeCoseno(embeddingArrayPergunta, texto.Embedding);
|
|
if (similaridade > melhorSimilaridade)
|
|
{
|
|
melhorSimilaridade = similaridade;
|
|
melhorTexto = texto;
|
|
}
|
|
}
|
|
|
|
return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada.";
|
|
}
|
|
|
|
private async Task<string> BuscarTopTextosRelacionadosComInterface(string pergunta, string projectId)
|
|
{
|
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
|
var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(
|
|
_textFilter.ToLowerAndWithoutAccents(pergunta));
|
|
var embeddingArray = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
var resultados = await _vectorSearchService.SearchSimilarDynamicAsync(embeddingArray, projectId, 0.5, 3);
|
|
|
|
if (!resultados.Any())
|
|
return "Não encontrei respostas adequadas para a pergunta fornecida.";
|
|
|
|
var cabecalho = $"Contexto encontrado para: '{pergunta}' ({resultados.Count} resultado(s)):\n\n";
|
|
|
|
var resultadosFormatados = resultados
|
|
.Select((item, index) =>
|
|
$"=== CONTEXTO {index + 1} ===\n" +
|
|
$"Relevância: {item.Score:P1}\n" +
|
|
$"Conteúdo:\n{item.Content}")
|
|
.ToList();
|
|
|
|
return cabecalho + string.Join("\n\n", resultadosFormatados);
|
|
}
|
|
|
|
private async Task<string> BuscarTodosRequisitosDoProjeto(string pergunta, string projectId)
|
|
{
|
|
var resultados = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
|
|
|
if (!resultados.Any())
|
|
return "Não encontrei respostas adequadas para a pergunta fornecida.";
|
|
|
|
var cabecalho = $"Contexto encontrado para: '{pergunta}' ({resultados.Count} resultado(s)):\n\n";
|
|
|
|
var resultadosFormatados = resultados
|
|
.Select((item, index) =>
|
|
$"=== CONTEXTO {index + 1} ===\n" +
|
|
$"Relevância: {item.Score:P1}\n" +
|
|
$"Conteúdo:\n{item.Content}")
|
|
.ToList();
|
|
|
|
return cabecalho + string.Join("\n\n", resultadosFormatados);
|
|
}
|
|
|
|
|
|
async Task<string> BuscarTopTextosRelacionadosDinamico(string pergunta, string projectId, int size = 3)
|
|
{
|
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
|
var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(pergunta));
|
|
var embeddingArrayPergunta = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
// Cria instância da classe de busca otimizada
|
|
var searchTextData = new SearchTextData(_textDataRepository.GetCollection()); // Você precisará expor a collection
|
|
|
|
// Busca dinâmica com threshold adaptativo
|
|
var resultados = await BuscarSimilaridadeHibridaAdaptativa(
|
|
searchTextData,
|
|
embeddingArrayPergunta,
|
|
projectId,
|
|
size
|
|
);
|
|
|
|
if (!resultados.Any())
|
|
return "Não encontrei respostas adequadas para a pergunta fornecida.";
|
|
|
|
var cabecalho = $"Contexto encontrado para: '{pergunta}' ({resultados.Count} resultado(s)):\n\n";
|
|
|
|
var resultadosFormatados = resultados
|
|
.Select((item, index) =>
|
|
$"=== CONTEXTO {index + 1} ===\n" +
|
|
$"Relevância: {item.Similaridade:P1}\n" +
|
|
$"Conteúdo:\n{item.Documento.Conteudo}")
|
|
.ToList();
|
|
|
|
return cabecalho + string.Join("\n\n", resultadosFormatados);
|
|
}
|
|
|
|
// Método auxiliar para busca híbrida adaptativa
|
|
private async Task<List<ResultadoSimilaridade>> BuscarSimilaridadeHibridaAdaptativa(
|
|
SearchTextData searchTextData,
|
|
double[] embeddingArray,
|
|
string projectId,
|
|
int quantidadeDesejada)
|
|
{
|
|
// Estratégia 1: Busca com threshold alto (0.5) limitado à quantidade desejada
|
|
var resultados = await searchTextData.BuscarSimilaridadeOtimizada(
|
|
embeddingArray,
|
|
projectId,
|
|
similaridadeMinima: 0.5,
|
|
limite: quantidadeDesejada
|
|
);
|
|
|
|
// Se conseguiu a quantidade desejada com qualidade alta, retorna
|
|
if (resultados.Count >= quantidadeDesejada)
|
|
{
|
|
return resultados.Take(quantidadeDesejada).ToList();
|
|
}
|
|
|
|
// Estratégia 2: Se não conseguiu o suficiente, busca com threshold médio
|
|
if (resultados.Count < quantidadeDesejada)
|
|
{
|
|
resultados = await searchTextData.BuscarSimilaridadeOtimizada(
|
|
embeddingArray,
|
|
projectId,
|
|
similaridadeMinima: 0.35,
|
|
limite: quantidadeDesejada * 2 // Busca mais para ter opções
|
|
);
|
|
|
|
if (resultados.Count >= quantidadeDesejada)
|
|
{
|
|
return resultados.Take(quantidadeDesejada).ToList();
|
|
}
|
|
}
|
|
|
|
// Estratégia 3: Se ainda não conseguiu, busca com threshold baixo
|
|
if (resultados.Count < quantidadeDesejada)
|
|
{
|
|
resultados = await searchTextData.BuscarSimilaridadeOtimizada(
|
|
embeddingArray,
|
|
projectId,
|
|
similaridadeMinima: 0.2,
|
|
limite: quantidadeDesejada * 3
|
|
);
|
|
}
|
|
|
|
// Retorna o que conseguiu encontrar, no máximo a quantidade desejada
|
|
return resultados.Take(quantidadeDesejada).ToList();
|
|
}
|
|
|
|
async Task<string> BuscarTopTextosRelacionados(string pergunta, string projectId, int size = 3)
|
|
{
|
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
|
var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(pergunta));
|
|
var embeddingArrayPergunta = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
|
|
|
|
var textos = await _textDataRepository.GetByProjectIdAsync(projectId);
|
|
|
|
var melhoresTextos = textos
|
|
.Select(texto => new
|
|
{
|
|
Conteudo = texto.Conteudo,
|
|
Similaridade = CalcularSimilaridadeCoseno(embeddingArrayPergunta, texto.Embedding)
|
|
})
|
|
.Where(x => x.Similaridade > 0.3)
|
|
.OrderByDescending(x => x.Similaridade)
|
|
.Take(3)
|
|
.ToList();
|
|
|
|
if (!melhoresTextos.Any())
|
|
return "Não encontrei respostas adequadas para a pergunta fornecida.";
|
|
|
|
var cabecalho = $"Contexto encontrado para: '{pergunta}' ({melhoresTextos.Count} resultado(s)):\n\n";
|
|
|
|
var resultadosFormatados = melhoresTextos
|
|
.Select((item, index) =>
|
|
$"=== CONTEXTO {index + 1} ===\n" +
|
|
$"Relevância: {item.Similaridade:P1}\n" +
|
|
$"Conteúdo:\n{item.Conteudo}")
|
|
.ToList();
|
|
|
|
return cabecalho + string.Join("\n\n", resultadosFormatados);
|
|
}
|
|
|
|
double CalcularSimilaridadeCoseno(double[] embedding1, double[] embedding2)
|
|
{
|
|
double dotProduct = 0.0;
|
|
double normA = 0.0;
|
|
double normB = 0.0;
|
|
for (int i = 0; i < embedding1.Length; i++)
|
|
{
|
|
dotProduct += embedding1[i] * embedding2[i];
|
|
normA += Math.Pow(embedding1[i], 2);
|
|
normB += Math.Pow(embedding2[i], 2);
|
|
}
|
|
return dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB));
|
|
}
|
|
|
|
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
|
|
{
|
|
return this.GetResponse(userData, projectId, sessionId, question, "pt");
|
|
}
|
|
}
|
|
|
|
public enum SearchStrategy
|
|
{
|
|
TodosProjeto,
|
|
SimilaridadeComFiltro,
|
|
SimilaridadeGlobal
|
|
}
|
|
}
|
|
|
|
#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.
|