ChatRAG/Services/ResponseService/ResponseCompanyService.cs
2025-06-15 21:34:47 -03:00

272 lines
12 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.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 ProjectDataRepository _projectDataRepository;
private readonly IChatCompletionService _chatCompletionService;
private readonly IVectorSearchService _vectorSearchService;
public ResponseRAGService(
ChatHistoryService chatHistoryService,
Kernel kernel,
TextFilter textFilter,
TextDataRepository textDataRepository,
ProjectDataRepository 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)
{
var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start();
//var resposta = await BuscarTextoRelacionado(question);
//var resposta = await BuscarTopTextosRelacionados(question, projectId);
//var resposta = await BuscarTopTextosRelacionadosDinamico(question, projectId);
var resposta = await BuscarTopTextosRelacionadosComInterface(question, projectId);
var projectData = (await _projectDataRepository.GetAsync()).FirstOrDefault();
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 an expert software analyst and QA professional.
Project Context: {0}
Requirements: {1}
User Request: ""{2}""
Please answer the question above from User Request with a comprehensive response in English. Consider the project context and requirements above to generate accurate and relevant information.";
// Usage
question = string.Format(basePrompt, project, resposta, question);
ChatHistory history = _chatHistoryService.GetSumarizer(sessionId);
history.AddUserMessage(question);
var response = await _chatCompletionService.GetChatMessageContentAsync(history);
history.AddMessage(response.Role, response.Content ?? "");
_chatHistoryService.UpdateHistory(sessionId, history);
stopWatch.Stop();
return $"{response.Content ?? ""}\n\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s";
}
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);
}
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));
}
}
}
#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.