Compare commits
8 Commits
main
...
feat/rag-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5b0c32a66 | ||
|
|
e75abe7fc8 | ||
|
|
bc699abbd3 | ||
|
|
13083ffb5d | ||
|
|
caf50d9d7f | ||
|
|
9a1d75aaf8 | ||
|
|
94c0395e68 | ||
|
|
63b455dc34 |
@ -29,7 +29,7 @@ namespace ChatApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChatHistory GetSumarizer(string sessionId)
|
public ChatHistory GetSumarizer(string sessionId, string language = "en")
|
||||||
{
|
{
|
||||||
if (_keyValues.ContainsKey(sessionId))
|
if (_keyValues.ContainsKey(sessionId))
|
||||||
{
|
{
|
||||||
@ -39,7 +39,12 @@ namespace ChatApi
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var msg = new List<ChatMessageContent>();
|
var msg = new List<ChatMessageContent>();
|
||||||
PromptLiliana(msg);
|
|
||||||
|
if (language == "pt")
|
||||||
|
PromptPt(msg);
|
||||||
|
else
|
||||||
|
PromptEn(msg);
|
||||||
|
|
||||||
string json = JsonSerializer.Serialize(msg);
|
string json = JsonSerializer.Serialize(msg);
|
||||||
var history = new ChatHistory(JsonSerializer.Deserialize<List<ChatMessageContent>>(json));
|
var history = new ChatHistory(JsonSerializer.Deserialize<List<ChatMessageContent>>(json));
|
||||||
_keyValues[sessionId] = history;
|
_keyValues[sessionId] = history;
|
||||||
@ -58,5 +63,25 @@ namespace ChatApi
|
|||||||
msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde sempre em português do Brasil e fala sobre detalhes de projeto, arquitetura e criação de casos de teste."));
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde sempre em português do Brasil e fala sobre detalhes de projeto, arquitetura e criação de casos de teste."));
|
||||||
msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil."));
|
msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void PromptEn(List<ChatMessageContent> msg)
|
||||||
|
{
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "You are an expert software analyst and QA professional."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Please provide a comprehensive response in English (US). Consider the project context and requirements above to generate accurate and relevant information."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "If you have test case requests: Use Gherkin format (Given-When-Then) with realistic scenarios covering happy path, edge cases, and error handling."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "If you have project summaries: Include objectives, key features, technologies, and main challenges."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "If you have a task list request for one developer: Organize tasks by priority and estimated effort for a single developer, including technical dependencies."));
|
||||||
|
//msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil."));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PromptPt(List<ChatMessageContent> msg)
|
||||||
|
{
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Você é um analista de software especialista e profissional de QA."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Por favor, forneça uma resposta abrangente em português do Brasil. Considere o contexto do projeto e os requisitos acima para gerar informações precisas e relevantes."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Se forem solicitados casos de teste: Use o formato Gherkin (Dado-Quando-Então) com cenários realistas cobrindo caminho feliz, casos extremos e tratamento de erros."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Se for solicitado um resumo do projeto: Inclua objetivos, principais funcionalidades, tecnologias e principais desafios."));
|
||||||
|
msg.Add(new ChatMessageContent(AuthorRole.System, "Se for uma solicitação de lista de tarefas para um(ou mais) desenvolvedor(es): Organize as tarefas por prioridade e esforço estimado para um único desenvolvedor, incluindo dependências técnicas."));
|
||||||
|
//msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre português do Brasil."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
|
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
|
||||||
<PackageReference Include="MongoDB.Driver.Core" Version="2.30.0" />
|
<PackageReference Include="MongoDB.Driver.Core" Version="2.30.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Qdrant.Client" Version="1.14.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ActiveDebugProfile>http</ActiveDebugProfile>
|
<ActiveDebugProfile>http</ActiveDebugProfile>
|
||||||
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
|
||||||
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
|
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||||
|
|||||||
11
Configuration/Prompts/Domains/Financeiro.json
Normal file
11
Configuration/Prompts/Domains/Financeiro.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Configuration/Prompts/Domains/QA.json
Normal file
11
Configuration/Prompts/Domains/QA.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Configuration/Prompts/Domains/RH.json
Normal file
11
Configuration/Prompts/Domains/RH.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Configuration/Prompts/Domains/Servicos.json
Normal file
74
Configuration/Prompts/Domains/Servicos.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Configuration/Prompts/Domains/TI.json
Normal file
11
Configuration/Prompts/Domains/TI.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Configuration/Prompts/base-prompts.json
Normal file
18
Configuration/Prompts/base-prompts.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ using ChatRAG.Data;
|
|||||||
using ChatRAG.Models;
|
using ChatRAG.Models;
|
||||||
using ChatRAG.Requests;
|
using ChatRAG.Requests;
|
||||||
using BlazMapper;
|
using BlazMapper;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
|
||||||
#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.
|
#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.
|
||||||
|
|
||||||
@ -20,29 +21,29 @@ namespace ChatApi.Controllers
|
|||||||
private readonly IResponseService _responseService;
|
private readonly IResponseService _responseService;
|
||||||
private readonly TextFilter _textFilter;
|
private readonly TextFilter _textFilter;
|
||||||
private readonly UserDataRepository _userDataRepository;
|
private readonly UserDataRepository _userDataRepository;
|
||||||
private readonly ProjectDataRepository _projectDataRepository;
|
private readonly IProjectDataRepository _projectDataRepository;
|
||||||
private readonly TextData _textData;
|
private readonly ITextDataService _textDataService;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
public ChatController(
|
public ChatController(
|
||||||
ILogger<ChatController> logger,
|
ILogger<ChatController> logger,
|
||||||
IResponseService responseService,
|
IResponseService responseService,
|
||||||
UserDataRepository userDataRepository,
|
UserDataRepository userDataRepository,
|
||||||
TextData textData,
|
ITextDataService textDataService,
|
||||||
ProjectDataRepository projectDataRepository,
|
IProjectDataRepository projectDataRepository,
|
||||||
IHttpClientFactory httpClientFactory)
|
IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_responseService = responseService;
|
_responseService = responseService;
|
||||||
_userDataRepository = userDataRepository;
|
_userDataRepository = userDataRepository;
|
||||||
_textData = textData;
|
_textDataService = textDataService;
|
||||||
_projectDataRepository = projectDataRepository;
|
_projectDataRepository = projectDataRepository;
|
||||||
this._httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("response")]
|
[Route("response")]
|
||||||
public async Task<IActionResult> GetResponse([FromForm] ChatRequest chatRequest)
|
public async Task<IActionResult> GetResponse([FromBody] ChatRequest chatRequest)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -80,7 +81,37 @@ namespace ChatApi.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _textData.SalvarNoMongoDB(request.Id, request.Title, request.Content, request.ProjectId);
|
await _textDataService.SaveDocumentAsync(new DocumentInput
|
||||||
|
{
|
||||||
|
Id = request.Id,
|
||||||
|
Title = request.Title,
|
||||||
|
Content = request.Content,
|
||||||
|
ProjectId = request.ProjectId
|
||||||
|
});
|
||||||
|
return Created();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("savetexts")]
|
||||||
|
public async Task<IActionResult> SaveTexts([FromBody] List<TextRequest> requests)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach(var request in requests)
|
||||||
|
{
|
||||||
|
await _textDataService.SaveDocumentAsync(new DocumentInput
|
||||||
|
{
|
||||||
|
Id = request.Id,
|
||||||
|
Title = request.Title,
|
||||||
|
Content = request.Content,
|
||||||
|
ProjectId = request.ProjectId
|
||||||
|
});
|
||||||
|
}
|
||||||
return Created();
|
return Created();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -91,9 +122,9 @@ namespace ChatApi.Controllers
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("texts")]
|
[Route("texts")]
|
||||||
public async Task<IEnumerable<TextResponse>> GetTexts()
|
public async Task<IEnumerable<TextResponse>> GetTexts(string groupId)
|
||||||
{
|
{
|
||||||
var texts = await _textData.GetAll();
|
var texts = await _textDataService.GetByPorjectId(groupId);
|
||||||
return texts.Select(t => {
|
return texts.Select(t => {
|
||||||
return new TextResponse
|
return new TextResponse
|
||||||
{
|
{
|
||||||
@ -108,7 +139,7 @@ namespace ChatApi.Controllers
|
|||||||
[Route("texts/id/{id}")]
|
[Route("texts/id/{id}")]
|
||||||
public async Task<TextResponse> GetText([FromRoute] string id)
|
public async Task<TextResponse> GetText([FromRoute] string id)
|
||||||
{
|
{
|
||||||
var textItem = await _textData.GetById(id);
|
var textItem = await _textDataService.GetById(id);
|
||||||
|
|
||||||
return new TextResponse {
|
return new TextResponse {
|
||||||
Id = textItem.Id,
|
Id = textItem.Id,
|
||||||
|
|||||||
419
Controllers/MigrationController.cs
Normal file
419
Controllers/MigrationController.cs
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
using ChatApi.Data;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.SearchVectors;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
|
|
||||||
|
#pragma warning disable SKEXP0001
|
||||||
|
|
||||||
|
namespace ChatApi.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class MigrationController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVectorDatabaseFactory _factory;
|
||||||
|
private readonly ILogger<MigrationController> _logger;
|
||||||
|
private readonly VectorDatabaseSettings _settings;
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
private readonly TextDataRepository _mongoRepository;
|
||||||
|
|
||||||
|
public MigrationController(
|
||||||
|
IVectorDatabaseFactory factory,
|
||||||
|
ILogger<MigrationController> logger,
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
ITextEmbeddingGenerationService embeddingService,
|
||||||
|
TextDataRepository mongoRepository)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_logger = logger;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
_mongoRepository = mongoRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status da migração e informações dos providers
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("status")]
|
||||||
|
public async Task<IActionResult> GetMigrationStatus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentProvider = _factory.GetActiveProvider();
|
||||||
|
|
||||||
|
var status = new
|
||||||
|
{
|
||||||
|
CurrentProvider = currentProvider,
|
||||||
|
Settings = new
|
||||||
|
{
|
||||||
|
Provider = _settings.Provider,
|
||||||
|
MongoDB = _settings.MongoDB != null ? new
|
||||||
|
{
|
||||||
|
DatabaseName = _settings.MongoDB.DatabaseName,
|
||||||
|
TextCollection = _settings.MongoDB.TextCollectionName
|
||||||
|
} : null,
|
||||||
|
Qdrant = _settings.Qdrant != null ? new
|
||||||
|
{
|
||||||
|
Host = _settings.Qdrant.Host,
|
||||||
|
Port = _settings.Qdrant.Port,
|
||||||
|
Collection = _settings.Qdrant.CollectionName,
|
||||||
|
VectorSize = _settings.Qdrant.VectorSize
|
||||||
|
} : null
|
||||||
|
},
|
||||||
|
Stats = await GetProvidersStats()
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(status);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao obter status da migração");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migra dados do MongoDB para Qdrant
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("mongo-to-qdrant")]
|
||||||
|
public async Task<IActionResult> MigrateMongoToQdrant(
|
||||||
|
[FromQuery] string? projectId = null,
|
||||||
|
[FromQuery] int batchSize = 50,
|
||||||
|
[FromQuery] bool dryRun = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_settings.Provider != "MongoDB")
|
||||||
|
{
|
||||||
|
return BadRequest("Migração só funciona quando o provider atual é MongoDB");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Iniciando migração MongoDB → Qdrant. ProjectId: {ProjectId}, DryRun: {DryRun}",
|
||||||
|
projectId, dryRun);
|
||||||
|
|
||||||
|
var result = await PerformMigration(projectId, batchSize, dryRun);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro durante migração MongoDB → Qdrant");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migra dados do Qdrant para MongoDB
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("qdrant-to-mongo")]
|
||||||
|
public async Task<IActionResult> MigrateQdrantToMongo(
|
||||||
|
[FromQuery] string? projectId = null,
|
||||||
|
[FromQuery] int batchSize = 50,
|
||||||
|
[FromQuery] bool dryRun = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_settings.Provider != "Qdrant")
|
||||||
|
{
|
||||||
|
return BadRequest("Migração só funciona quando o provider atual é Qdrant");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Iniciando migração Qdrant → MongoDB. ProjectId: {ProjectId}, DryRun: {DryRun}",
|
||||||
|
projectId, dryRun);
|
||||||
|
|
||||||
|
// TODO: Implementar migração reversa se necessário
|
||||||
|
return BadRequest("Migração Qdrant → MongoDB ainda não implementada");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro durante migração Qdrant → MongoDB");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compara dados entre MongoDB e Qdrant
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("compare")]
|
||||||
|
public async Task<IActionResult> CompareProviders([FromQuery] string? projectId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Cria serviços para ambos os providers manualmente
|
||||||
|
var mongoService = CreateMongoService();
|
||||||
|
var qdrantService = await CreateQdrantService();
|
||||||
|
|
||||||
|
var comparison = await CompareData(mongoService, qdrantService, projectId);
|
||||||
|
|
||||||
|
return Ok(comparison);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao comparar providers");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limpa todos os dados do provider atual
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("clear-current")]
|
||||||
|
public async Task<IActionResult> ClearCurrentProvider([FromQuery] string? projectId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vectorSearchService = _factory.CreateVectorSearchService();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
// Limpar tudo - PERIGOSO!
|
||||||
|
return BadRequest("Limpeza completa requer confirmação. Use /clear-current/confirm");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpar apenas um projeto
|
||||||
|
var documents = await vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
var ids = documents.Select(d => d.Id).ToList();
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
await vectorSearchService.DeleteDocumentAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
provider = _settings.Provider,
|
||||||
|
projectId,
|
||||||
|
deletedCount = ids.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao limpar dados do provider");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Testa conectividade dos providers
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("test-connections")]
|
||||||
|
public async Task<IActionResult> TestConnections()
|
||||||
|
{
|
||||||
|
var results = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
// Testar MongoDB
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mongoService = CreateMongoService();
|
||||||
|
var mongoHealth = await mongoService.IsHealthyAsync();
|
||||||
|
var mongoStats = await mongoService.GetStatsAsync();
|
||||||
|
|
||||||
|
results["MongoDB"] = new
|
||||||
|
{
|
||||||
|
healthy = mongoHealth,
|
||||||
|
stats = mongoStats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results["MongoDB"] = new { healthy = false, error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testar Qdrant
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var qdrantService = await CreateQdrantService();
|
||||||
|
var qdrantHealth = await qdrantService.IsHealthyAsync();
|
||||||
|
var qdrantStats = await qdrantService.GetStatsAsync();
|
||||||
|
|
||||||
|
results["Qdrant"] = new
|
||||||
|
{
|
||||||
|
healthy = qdrantHealth,
|
||||||
|
stats = qdrantStats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results["Qdrant"] = new { healthy = false, error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS PRIVADOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
private async Task<object> PerformMigration(string? projectId, int batchSize, bool dryRun)
|
||||||
|
{
|
||||||
|
var mongoService = CreateMongoService();
|
||||||
|
var qdrantService = await CreateQdrantService();
|
||||||
|
|
||||||
|
// 1. Buscar dados do MongoDB
|
||||||
|
List<TextoComEmbedding> documents;
|
||||||
|
if (string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
var allDocs = await _mongoRepository.GetAsync();
|
||||||
|
documents = allDocs.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var projectDocs = await _mongoRepository.GetByProjectIdAsync(projectId);
|
||||||
|
documents = projectDocs.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Encontrados {Count} documentos para migração", documents.Count);
|
||||||
|
|
||||||
|
if (dryRun)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
dryRun = true,
|
||||||
|
documentsFound = documents.Count,
|
||||||
|
projects = documents.GroupBy(d => d.ProjetoId).Select(g => new {
|
||||||
|
projectId = g.Key,
|
||||||
|
count = g.Count()
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Migrar em batches
|
||||||
|
var migrated = 0;
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
for (int i = 0; i < documents.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = documents.Skip(i).Take(batchSize);
|
||||||
|
|
||||||
|
foreach (var doc in batch)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Verificar se já existe no Qdrant
|
||||||
|
var exists = await qdrantService.DocumentExistsAsync(doc.Id);
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Documento {Id} já existe no Qdrant, pulando", doc.Id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrar documento
|
||||||
|
await qdrantService.AddDocumentAsync(
|
||||||
|
title: doc.Titulo,
|
||||||
|
content: doc.Conteudo,
|
||||||
|
projectId: doc.ProjetoId,
|
||||||
|
embedding: doc.Embedding,
|
||||||
|
metadata: new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["project_name"] = doc.ProjetoNome ?? "",
|
||||||
|
["document_type"] = doc.TipoDocumento ?? "",
|
||||||
|
["category"] = doc.Categoria ?? "",
|
||||||
|
["tags"] = doc.Tags ?? Array.Empty<string>(),
|
||||||
|
["migrated_from"] = "MongoDB",
|
||||||
|
["migrated_at"] = DateTime.UtcNow.ToString("O")
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
migrated++;
|
||||||
|
|
||||||
|
if (migrated % 10 == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrados {Migrated}/{Total} documentos", migrated, documents.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var error = $"Erro ao migrar documento {doc.Id}: {ex.Message}";
|
||||||
|
errors.Add(error);
|
||||||
|
_logger.LogError(ex, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
totalDocuments = documents.Count,
|
||||||
|
migrated,
|
||||||
|
errors = errors.Count,
|
||||||
|
errorDetails = errors.Take(10).ToList(), // Primeiros 10 erros
|
||||||
|
batchSize,
|
||||||
|
duration = "Completed"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<object> CompareData(
|
||||||
|
IVectorSearchService mongoService,
|
||||||
|
IVectorSearchService qdrantService,
|
||||||
|
string? projectId)
|
||||||
|
{
|
||||||
|
var mongoStats = await mongoService.GetStatsAsync();
|
||||||
|
var qdrantStats = await qdrantService.GetStatsAsync();
|
||||||
|
|
||||||
|
var mongoCount = await mongoService.GetDocumentCountAsync(projectId);
|
||||||
|
var qdrantCount = await qdrantService.GetDocumentCountAsync(projectId);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
comparison = new
|
||||||
|
{
|
||||||
|
MongoDB = new
|
||||||
|
{
|
||||||
|
documentCount = mongoCount,
|
||||||
|
healthy = await mongoService.IsHealthyAsync(),
|
||||||
|
stats = mongoStats
|
||||||
|
},
|
||||||
|
Qdrant = new
|
||||||
|
{
|
||||||
|
documentCount = qdrantCount,
|
||||||
|
healthy = await qdrantService.IsHealthyAsync(),
|
||||||
|
stats = qdrantStats
|
||||||
|
}
|
||||||
|
},
|
||||||
|
differences = new
|
||||||
|
{
|
||||||
|
documentCountDiff = qdrantCount - mongoCount,
|
||||||
|
inSync = mongoCount == qdrantCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MongoVectorSearchService CreateMongoService()
|
||||||
|
{
|
||||||
|
return new MongoVectorSearchService(_mongoRepository, _embeddingService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<QdrantVectorSearchService> CreateQdrantService()
|
||||||
|
{
|
||||||
|
var qdrantSettings = Microsoft.Extensions.Options.Options.Create(_settings);
|
||||||
|
var logger = HttpContext.RequestServices.GetService<ILogger<QdrantVectorSearchService>>()!;
|
||||||
|
|
||||||
|
return new QdrantVectorSearchService(qdrantSettings, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, object>> GetProvidersStats()
|
||||||
|
{
|
||||||
|
var stats = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentService = _factory.CreateVectorSearchService();
|
||||||
|
stats["current"] = await currentService.GetStatsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stats["current"] = new { error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore SKEXP0001
|
||||||
225
Controllers/RAGStrategyController.cs
Normal file
225
Controllers/RAGStrategyController.cs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.ResponseService;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
|
||||||
|
namespace ChatApi.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class RAGStrategyController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVectorDatabaseFactory _factory;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<RAGStrategyController> _logger;
|
||||||
|
|
||||||
|
public RAGStrategyController(
|
||||||
|
IVectorDatabaseFactory factory,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<RAGStrategyController> logger)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista as estratégias de RAG disponíveis
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("strategies")]
|
||||||
|
public IActionResult GetAvailableStrategies()
|
||||||
|
{
|
||||||
|
var strategies = new[]
|
||||||
|
{
|
||||||
|
new {
|
||||||
|
name = "Standard",
|
||||||
|
description = "RAG padrão com classificação automática de estratégia",
|
||||||
|
service = "ResponseRAGService",
|
||||||
|
features = new[] { "Busca por similaridade", "Filtros dinâmicos", "Classificação automática" }
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
name = "Hierarchical",
|
||||||
|
description = "RAG hierárquico com múltiplas etapas de busca",
|
||||||
|
service = "HierarchicalRAGService",
|
||||||
|
features = new[] { "Análise de query", "Busca em múltiplas etapas", "Expansão de contexto", "Identificação de lacunas" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
currentProvider = _factory.GetActiveProvider(),
|
||||||
|
availableStrategies = strategies
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Testa uma estratégia específica com uma pergunta
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("test/{strategy}")]
|
||||||
|
public async Task<IActionResult> TestStrategy(
|
||||||
|
[FromRoute] string strategy,
|
||||||
|
[FromBody] StrategyTestRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IResponseService responseService = strategy.ToLower() switch
|
||||||
|
{
|
||||||
|
"standard" => _serviceProvider.GetRequiredService<ResponseRAGService>(),
|
||||||
|
"hierarchical" => _serviceProvider.GetRequiredService<HierarchicalRAGService>(),
|
||||||
|
_ => throw new ArgumentException($"Estratégia não suportada: {strategy}")
|
||||||
|
};
|
||||||
|
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// Usar dados mock se não fornecidos
|
||||||
|
var userData = request.UserData ?? new ChatApi.Models.UserData
|
||||||
|
{
|
||||||
|
Id = "test-user",
|
||||||
|
Name = "Test User"
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await responseService.GetResponse(
|
||||||
|
userData,
|
||||||
|
request.ProjectId,
|
||||||
|
request.SessionId ?? Guid.NewGuid().ToString(),
|
||||||
|
request.Question,
|
||||||
|
request.Language ?? "pt"
|
||||||
|
);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
strategy,
|
||||||
|
response,
|
||||||
|
executionTime = stopwatch.ElapsedMilliseconds,
|
||||||
|
provider = _factory.GetActiveProvider()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao testar estratégia {Strategy}", strategy);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compara resultados entre diferentes estratégias
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("compare")]
|
||||||
|
public async Task<IActionResult> CompareStrategies([FromBody] StrategyTestRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userData = request.UserData ?? new ChatApi.Models.UserData
|
||||||
|
{
|
||||||
|
Id = "test-user",
|
||||||
|
Name = "Test User"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sessionId = Guid.NewGuid().ToString();
|
||||||
|
var results = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
// Testar estratégia padrão
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var standardService = _serviceProvider.GetRequiredService<ResponseRAGService>();
|
||||||
|
var stopwatch1 = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
var standardResponse = await standardService.GetResponse(
|
||||||
|
userData, request.ProjectId, sessionId + "-standard",
|
||||||
|
request.Question, request.Language ?? "pt");
|
||||||
|
stopwatch1.Stop();
|
||||||
|
|
||||||
|
results["standard"] = new
|
||||||
|
{
|
||||||
|
response = standardResponse,
|
||||||
|
executionTime = stopwatch1.ElapsedMilliseconds,
|
||||||
|
success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results["standard"] = new { success = false, error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testar estratégia hierárquica
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hierarchicalService = _serviceProvider.GetRequiredService<HierarchicalRAGService>();
|
||||||
|
var stopwatch2 = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
var hierarchicalResponse = await hierarchicalService.GetResponse(
|
||||||
|
userData, request.ProjectId, sessionId + "-hierarchical",
|
||||||
|
request.Question, request.Language ?? "pt");
|
||||||
|
stopwatch2.Stop();
|
||||||
|
|
||||||
|
results["hierarchical"] = new
|
||||||
|
{
|
||||||
|
response = hierarchicalResponse,
|
||||||
|
executionTime = stopwatch2.ElapsedMilliseconds,
|
||||||
|
success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results["hierarchical"] = new { success = false, error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
question = request.Question,
|
||||||
|
projectId = request.ProjectId,
|
||||||
|
provider = _factory.GetActiveProvider(),
|
||||||
|
results
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao comparar estratégias");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém métricas de performance das estratégias
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("metrics")]
|
||||||
|
public IActionResult GetMetrics()
|
||||||
|
{
|
||||||
|
// TODO: Implementar coleta de métricas real
|
||||||
|
var metrics = new
|
||||||
|
{
|
||||||
|
standard = new
|
||||||
|
{
|
||||||
|
avgResponseTime = "1.2s",
|
||||||
|
successRate = "98%",
|
||||||
|
avgContextSize = "3 documentos",
|
||||||
|
usage = "Alto"
|
||||||
|
},
|
||||||
|
hierarchical = new
|
||||||
|
{
|
||||||
|
avgResponseTime = "2.1s",
|
||||||
|
successRate = "95%",
|
||||||
|
avgContextSize = "5-8 documentos",
|
||||||
|
usage = "Médio"
|
||||||
|
},
|
||||||
|
recommendations = new[]
|
||||||
|
{
|
||||||
|
"Use Standard para perguntas simples e rápidas",
|
||||||
|
"Use Hierarchical para análises complexas que precisam de contexto profundo",
|
||||||
|
"Hierarchical é melhor para perguntas técnicas detalhadas"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StrategyTestRequest
|
||||||
|
{
|
||||||
|
public string ProjectId { get; set; } = "";
|
||||||
|
public string Question { get; set; } = "";
|
||||||
|
public string? Language { get; set; }
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
public ChatApi.Models.UserData? UserData { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
225
Controllers/sgnjnzt5.baf~
Normal file
225
Controllers/sgnjnzt5.baf~
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.ResponseService;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
|
||||||
|
namespace ChatApi.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class RAGStrategyController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVectorDatabaseFactory _factory;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<RAGStrategyController> _logger;
|
||||||
|
|
||||||
|
public RAGStrategyController(
|
||||||
|
IVectorDatabaseFactory factory,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<RAGStrategyController> logger)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista as estratégias de RAG disponíveis
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("strategies")]
|
||||||
|
public IActionResult GetAvailableStrategies()
|
||||||
|
{
|
||||||
|
var strategies = new[]
|
||||||
|
{
|
||||||
|
new {
|
||||||
|
name = "Standard",
|
||||||
|
description = "RAG padrão com classificação automática de estratégia",
|
||||||
|
service = "ResponseRAGService",
|
||||||
|
features = new[] { "Busca por similaridade", "Filtros dinâmicos", "Classificação automática" }
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
name = "Hierarchical",
|
||||||
|
description = "RAG hierárquico com múltiplas etapas de busca",
|
||||||
|
service = "HierarchicalRAGService",
|
||||||
|
features = new[] { "Análise de query", "Busca em múltiplas etapas", "Expansão de contexto", "Identificação de lacunas" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
currentProvider = _factory.GetActiveProvider(),
|
||||||
|
availableStrategies = strategies
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Testa uma estratégia específica com uma pergunta
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("test/{strategy}")]
|
||||||
|
public async Task<IActionResult> TestStrategy(
|
||||||
|
[FromRoute] string strategy,
|
||||||
|
[FromBody] StrategyTestRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IResponseService responseService = strategy.ToLower() switch
|
||||||
|
{
|
||||||
|
"standard" => _serviceProvider.GetRequiredService<ResponseRAGService>(),
|
||||||
|
"hierarchical" => _serviceProvider.GetRequiredService<HierarchicalRAGService>(),
|
||||||
|
_ => throw new ArgumentException($"Estratégia não suportada: {strategy}")
|
||||||
|
};
|
||||||
|
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// Usar dados mock se não fornecidos
|
||||||
|
var userData = request.UserData ?? new ChatApi.Models.UserData
|
||||||
|
{
|
||||||
|
Id = "test-user",
|
||||||
|
Name = "Test User"
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await responseService.GetResponse(
|
||||||
|
userData,
|
||||||
|
request.ProjectId,
|
||||||
|
request.SessionId ?? Guid.NewGuid().ToString(),
|
||||||
|
request.Question,
|
||||||
|
request.Language ?? "pt"
|
||||||
|
);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
strategy,
|
||||||
|
response,
|
||||||
|
executionTime = stopwatch.ElapsedMilliseconds,
|
||||||
|
provider = _factory.GetActiveProvider()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao testar estratégia {Strategy}", strategy);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compara resultados entre diferentes estratégias
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("compare")]
|
||||||
|
public async Task<IActionResult> CompareStrategies([FromBody] StrategyTestRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userData = request.UserData ?? new ChatApi.Models.UserData
|
||||||
|
{
|
||||||
|
Id = "test-user",
|
||||||
|
Name = "Test User"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sessionId = Guid.NewGuid().ToString();
|
||||||
|
var results = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
// Testar estratégia padrão
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var standardService = _serviceProvider.GetRequiredService<ResponseRAGService>();
|
||||||
|
var stopwatch1 = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
var standardResponse = await standardService.GetResponse(
|
||||||
|
userData, request.ProjectId, sessionId + "-standard",
|
||||||
|
request.Question, request.Language ?? "pt");
|
||||||
|
stopwatch1.Stop();
|
||||||
|
|
||||||
|
results["standard"] = new
|
||||||
|
{
|
||||||
|
response = standardResponse,
|
||||||
|
executionTime = stopwatch1.ElapsedMilliseconds,
|
||||||
|
success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results["standard"] = new { success = false, error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Testar estratégia hierárquica
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hierarchicalService = _serviceProvider.GetRequiredService<HierarchicalRAGService>();
|
||||||
|
var stopwatch2 = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
var hierarchicalResponse = await hierarchicalService.GetResponse(
|
||||||
|
userData, request.ProjectId, sessionId + "-hierarchical",
|
||||||
|
request.Question, request.Language ?? "pt");
|
||||||
|
stopwatch2.Stop();
|
||||||
|
|
||||||
|
results["hierarchical"] = new
|
||||||
|
{
|
||||||
|
response = hierarchicalResponse,
|
||||||
|
executionTime = stopwatch2.ElapsedMilliseconds,
|
||||||
|
success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results["hierarchical"] = new { success = false, error = ex.Message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
question = request.Question,
|
||||||
|
projectId = request.ProjectId,
|
||||||
|
provider = _factory.GetActiveProvider(),
|
||||||
|
results
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao comparar estratégias");
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém métricas de performance das estratégias
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("metrics")]
|
||||||
|
public IActionResult GetMetrics()
|
||||||
|
{
|
||||||
|
// TODO: Implementar coleta de métricas real
|
||||||
|
var metrics = new
|
||||||
|
{
|
||||||
|
standard = new
|
||||||
|
{
|
||||||
|
avgResponseTime = "1.2s",
|
||||||
|
successRate = "98%",
|
||||||
|
avgContextSize = "3 documentos",
|
||||||
|
usage = "Alto"
|
||||||
|
},
|
||||||
|
hierarchical = new
|
||||||
|
{
|
||||||
|
avgResponseTime = "2.1s",
|
||||||
|
successRate = "95%",
|
||||||
|
avgContextSize = "5-8 documentos",
|
||||||
|
usage = "Médio"
|
||||||
|
},
|
||||||
|
recommendations = new[]
|
||||||
|
{
|
||||||
|
"Use Standard para perguntas simples e rápidas",
|
||||||
|
"Use Hierarchical para análises complexas que precisam de contexto profundo",
|
||||||
|
"Hierarchical é melhor para perguntas técnicas detalhadas"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StrategyTestRequest
|
||||||
|
{
|
||||||
|
public string ProjectId { get; set; } = "";
|
||||||
|
public string Question { get; set; } = "";
|
||||||
|
public string? Language { get; set; }
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
public ChatApi.Models.UserData? UserData { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
255
Data/30u0ddp1.org~
Normal file
255
Data/30u0ddp1.org~
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
public class QdrantProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<QdrantProjectDataRepository> _logger;
|
||||||
|
private readonly QdrantClient _qdrantClient;
|
||||||
|
|
||||||
|
public QdrantProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<QdrantProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var qdrantSettings = settings.Value.Qdrant ?? throw new ArgumentNullException("Qdrant settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{qdrantSettings.Host}:{qdrantSettings.Port}");
|
||||||
|
_collectionName = qdrantSettings.GroupsCollectionName;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Inicializa o QdrantClient - use GRPC (porta 6334) para melhor performance
|
||||||
|
_qdrantClient = new QdrantClient(qdrantSettings.Host, port: 6334, https: false);
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(_collectionName);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Qdrant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scrollRequest = new ScrollPoints
|
||||||
|
{
|
||||||
|
CollectionName = _collectionName,
|
||||||
|
Filter = new Filter(), // Filtro vazio
|
||||||
|
Limit = 1000,
|
||||||
|
WithPayload = true,
|
||||||
|
WithVectors = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _qdrantClient.ScrollAsync(_collectionName, scrollRequest);
|
||||||
|
|
||||||
|
return result.Select(ConvertToProject)
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Qdrant");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var points = await _qdrantClient.RetrieveAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.Parse(id) },
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = points.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToProject(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.Parse(id),
|
||||||
|
Vectors = new float[384], // Vector dummy para projetos
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = newProject.Id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.Parse(id),
|
||||||
|
Vectors = new float[384], // Vector dummy
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = updatedProject.Id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.Parse(id) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var vectorParams = new VectorParams
|
||||||
|
{
|
||||||
|
Size = 384,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.CreateCollectionAsync(_collectionName, vectorParams);
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Qdrant", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project? ConvertToProject(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (point.Payload == null) return null;
|
||||||
|
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = point.Payload.TryGetValue("id", out var idValue) ? idValue.StringValue : point.Id.ToString(),
|
||||||
|
Nome = point.Payload.TryGetValue("nome", out var nomeValue) ? nomeValue.StringValue : "",
|
||||||
|
Descricao = point.Payload.TryGetValue("descricao", out var descValue) ? descValue.StringValue : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollResult
|
||||||
|
{
|
||||||
|
public QdrantScrollData? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollData
|
||||||
|
{
|
||||||
|
public QdrantPoint[]? points { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPointResult
|
||||||
|
{
|
||||||
|
public QdrantPoint? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPoint
|
||||||
|
{
|
||||||
|
public string? id { get; set; }
|
||||||
|
public Dictionary<string, object>? payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
255
Data/32reubjo.e20~
Normal file
255
Data/32reubjo.e20~
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
public class QdrantProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<QdrantProjectDataRepository> _logger;
|
||||||
|
private readonly QdrantClient _qdrantClient;
|
||||||
|
|
||||||
|
public QdrantProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<QdrantProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var qdrantSettings = settings.Value.Qdrant ?? throw new ArgumentNullException("Qdrant settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{qdrantSettings.Host}:{qdrantSettings.Port}");
|
||||||
|
_collectionName = qdrantSettings.GroupsCollectionName;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Inicializa o QdrantClient - use GRPC (porta 6334) para melhor performance
|
||||||
|
_qdrantClient = new QdrantClient(qdrantSettings.Host, port: 6334, https: false);
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(_collectionName);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Qdrant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scrollRequest = new ScrollPoints
|
||||||
|
{
|
||||||
|
CollectionName = _collectionName,
|
||||||
|
Filter = new Filter(), // Filtro vazio
|
||||||
|
Limit = 1000,
|
||||||
|
WithPayload = true,
|
||||||
|
WithVectors = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _qdrantClient.ScrollAsync(_collectionName, scrollRequest);
|
||||||
|
|
||||||
|
return result.Select(ConvertToProject)
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Qdrant");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var points = await _qdrantClient.RetrieveAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.ParseFrom(id) },
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = points.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToProject(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.ParseFrom(id),
|
||||||
|
Vectors = new float[384], // Vector dummy para projetos
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = newProject.Id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.ParseFrom(id),
|
||||||
|
Vectors = new float[384], // Vector dummy
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = updatedProject.Id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.ParseFrom(id) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var vectorParams = new VectorParams
|
||||||
|
{
|
||||||
|
Size = 384,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.CreateCollectionAsync(_collectionName, vectorParams);
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Qdrant", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project? ConvertToProject(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (point.Payload == null) return null;
|
||||||
|
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = point.Payload.TryGetValue("id", out var idValue) ? idValue.StringValue : point.Id.ToString(),
|
||||||
|
Nome = point.Payload.TryGetValue("nome", out var nomeValue) ? nomeValue.StringValue : "",
|
||||||
|
Descricao = point.Payload.TryGetValue("descricao", out var descValue) ? descValue.StringValue : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollResult
|
||||||
|
{
|
||||||
|
public QdrantScrollData? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollData
|
||||||
|
{
|
||||||
|
public QdrantPoint[]? points { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPointResult
|
||||||
|
{
|
||||||
|
public QdrantPoint? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPoint
|
||||||
|
{
|
||||||
|
public string? id { get; set; }
|
||||||
|
public Dictionary<string, object>? payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
255
Data/3qrw3lwa.v4s~
Normal file
255
Data/3qrw3lwa.v4s~
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
public class QdrantProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<QdrantProjectDataRepository> _logger;
|
||||||
|
private readonly QdrantClient _qdrantClient;
|
||||||
|
|
||||||
|
public QdrantProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<QdrantProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var qdrantSettings = settings.Value.Qdrant ?? throw new ArgumentNullException("Qdrant settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{qdrantSettings.Host}:{qdrantSettings.Port}");
|
||||||
|
_collectionName = qdrantSettings.GroupsCollectionName;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Inicializa o QdrantClient - use GRPC (porta 6334) para melhor performance
|
||||||
|
_qdrantClient = new QdrantClient(qdrantSettings.Host, port: 6334, https: false);
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(_collectionName);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Qdrant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scrollRequest = new ScrollPoints
|
||||||
|
{
|
||||||
|
CollectionName = _collectionName,
|
||||||
|
Filter = new Filter(), // Filtro vazio
|
||||||
|
Limit = 1000,
|
||||||
|
WithPayload = true,
|
||||||
|
WithVectors = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _qdrantClient.ScrollAsync(scrollRequest);
|
||||||
|
|
||||||
|
return result.Select(ConvertToProject)
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Qdrant");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var points = await _qdrantClient.RetrieveAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.NewGuid(Guid.Parse(id)) },
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = points.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToProject(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.NewGuid(Guid.Parse(id)),
|
||||||
|
Vectors = new float[384], // Vector dummy para projetos
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = newProject.Id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.NewGuid(Guid.Parse(id)),
|
||||||
|
Vectors = new float[384], // Vector dummy
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = updatedProject.Id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.NewGuid(Guid.Parse(id)) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var vectorParams = new VectorParams
|
||||||
|
{
|
||||||
|
Size = 384,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.CreateCollectionAsync(_collectionName, vectorParams);
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Qdrant", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project? ConvertToProject(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (point.Payload == null) return null;
|
||||||
|
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = point.Payload.TryGetValue("id", out var idValue) ? idValue.StringValue : point.Id.ToString(),
|
||||||
|
Nome = point.Payload.TryGetValue("nome", out var nomeValue) ? nomeValue.StringValue : "",
|
||||||
|
Descricao = point.Payload.TryGetValue("descricao", out var descValue) ? descValue.StringValue : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollResult
|
||||||
|
{
|
||||||
|
public QdrantScrollData? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollData
|
||||||
|
{
|
||||||
|
public QdrantPoint[]? points { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPointResult
|
||||||
|
{
|
||||||
|
public QdrantPoint? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPoint
|
||||||
|
{
|
||||||
|
public string? id { get; set; }
|
||||||
|
public Dictionary<string, object>? payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
331
Data/ChromaProjectDataRepository.cs
Normal file
331
Data/ChromaProjectDataRepository.cs
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.SearchVectors;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
|
||||||
|
public class ChromaProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<ChromaProjectDataRepository> _logger;
|
||||||
|
|
||||||
|
public ChromaProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<ChromaProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var chromaSettings = settings.Value.Chroma ?? throw new ArgumentNullException("Chroma settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{chromaSettings.Host}:{chromaSettings.Port}");
|
||||||
|
_collectionName = "projects"; // Collection separada para projetos
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Verificar se a collection existe, se não, criar
|
||||||
|
var collections = await GetCollectionsAsync();
|
||||||
|
if (!collections.Contains(_collectionName))
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Chroma");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = new
|
||||||
|
{
|
||||||
|
where = new { entity_type = "project" },
|
||||||
|
include = new[] { "documents", "metadatas" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(query);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v2/collections/{_collectionName}/get", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError("Erro ao buscar projetos no Chroma");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
|
var getResult = JsonSerializer.Deserialize<ChromaGetResult>(result);
|
||||||
|
|
||||||
|
var projects = new List<Project>();
|
||||||
|
if (getResult?.ids?.Length > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < getResult.ids.Length; i++)
|
||||||
|
{
|
||||||
|
var metadata = getResult.metadatas?[i];
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
projects.Add(new Project
|
||||||
|
{
|
||||||
|
Id = metadata.GetValueOrDefault("id")?.ToString() ?? getResult.ids[i],
|
||||||
|
Nome = metadata.GetValueOrDefault("nome")?.ToString() ?? "",
|
||||||
|
Descricao = metadata.GetValueOrDefault("descricao")?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Chroma");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = new
|
||||||
|
{
|
||||||
|
ids = new[] { id },
|
||||||
|
include = new[] { "metadatas" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(query);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v2/collections/{_collectionName}/get", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
|
var getResult = JsonSerializer.Deserialize<ChromaGetResult>(result);
|
||||||
|
|
||||||
|
if (getResult?.ids?.Length > 0 && getResult.metadatas?[0] != null)
|
||||||
|
{
|
||||||
|
var metadata = getResult.metadatas[0];
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = metadata.GetValueOrDefault("id")?.ToString() ?? id,
|
||||||
|
Nome = metadata.GetValueOrDefault("nome")?.ToString() ?? "",
|
||||||
|
Descricao = metadata.GetValueOrDefault("descricao")?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Chroma", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var document = new
|
||||||
|
{
|
||||||
|
ids = new[] { id },
|
||||||
|
documents = new[] { $"Projeto: {newProject.Nome}" },
|
||||||
|
metadatas = new[] { new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["id"] = id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}},
|
||||||
|
embeddings = new[] { new double[384] } // Vector dummy
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(document);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v2/collections/{_collectionName}/add", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"Erro ao criar projeto no Chroma: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Chroma não tem update direto, então fazemos upsert
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var document = new
|
||||||
|
{
|
||||||
|
ids = new[] { id },
|
||||||
|
documents = new[] { $"Projeto: {updatedProject.Nome}" },
|
||||||
|
metadatas = new[] { new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["id"] = id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}},
|
||||||
|
embeddings = new[] { new double[384] }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(document);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v2/collections/{_collectionName}/upsert", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"Erro ao atualizar projeto no Chroma: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Chroma", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deleteRequest = new
|
||||||
|
{
|
||||||
|
ids = new[] { id }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(deleteRequest);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v2/collections/{_collectionName}/delete", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Erro ao remover projeto {Id} do Chroma: {Error}", id, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Chroma", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string[]> GetCollectionsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync("/api/v2/collections");
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var collections = JsonSerializer.Deserialize<string[]>(content);
|
||||||
|
return collections ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var collection = new
|
||||||
|
{
|
||||||
|
name = _collectionName,
|
||||||
|
metadata = new
|
||||||
|
{
|
||||||
|
description = "Projects Collection",
|
||||||
|
created_at = DateTime.UtcNow.ToString("O")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(collection);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync("/api/v2/collections", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"Erro ao criar collection de projetos: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Chroma", _collectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaGetResult
|
||||||
|
{
|
||||||
|
public string[] ids { get; set; } = Array.Empty<string>();
|
||||||
|
public string[] documents { get; set; } = Array.Empty<string>();
|
||||||
|
public Dictionary<string, object>[]? metadatas { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,27 @@
|
|||||||
using ChatApi;
|
using ChatApi;
|
||||||
using ChatRAG.Models;
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
namespace ChatRAG.Data
|
namespace ChatRAG.Data
|
||||||
{
|
{
|
||||||
public class ProjectDataRepository
|
public class MongoProjectDataRepository : IProjectDataRepository
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<Project> _textsCollection;
|
private readonly IMongoCollection<Project> _textsCollection;
|
||||||
|
|
||||||
public ProjectDataRepository(
|
public MongoProjectDataRepository(
|
||||||
IOptions<DomvsDatabaseSettings> databaseSettings)
|
IOptions<VectorDatabaseSettings> databaseSettings)
|
||||||
{
|
{
|
||||||
var mongoClient = new MongoClient(
|
var mongoClient = new MongoClient(
|
||||||
databaseSettings.Value.ConnectionString);
|
databaseSettings.Value.MongoDB.ConnectionString);
|
||||||
|
|
||||||
var mongoDatabase = mongoClient.GetDatabase(
|
var mongoDatabase = mongoClient.GetDatabase(
|
||||||
databaseSettings.Value.DatabaseName);
|
databaseSettings.Value.MongoDB.DatabaseName);
|
||||||
|
|
||||||
_textsCollection = mongoDatabase.GetCollection<Project>(
|
_textsCollection = mongoDatabase.GetCollection<Project>(
|
||||||
databaseSettings.Value.ProjectCollectionName);
|
databaseSettings.Value.MongoDB.ProjectCollectionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Project>> GetAsync() =>
|
public async Task<List<Project>> GetAsync() =>
|
||||||
178
Data/MongoTextDataService.cs
Normal file
178
Data/MongoTextDataService.cs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
using ChatApi.Data;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
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.Data
|
||||||
|
{
|
||||||
|
public class MongoTextDataService : ITextDataService
|
||||||
|
{
|
||||||
|
private readonly TextData _textData; // Sua classe existente!
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
|
||||||
|
public MongoTextDataService(
|
||||||
|
TextData textData,
|
||||||
|
ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
_textData = textData;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProviderName => "MongoDB";
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS ORIGINAIS - Delega para TextData
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
|
||||||
|
{
|
||||||
|
await _textData.SalvarNoMongoDB(titulo, texto, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
|
||||||
|
{
|
||||||
|
await _textData.SalvarNoMongoDB(id, titulo, texto, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
|
||||||
|
{
|
||||||
|
await _textData.SalvarTextoComEmbeddingNoMongoDB(textoCompleto, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
|
||||||
|
{
|
||||||
|
return await _textData.GetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId)
|
||||||
|
{
|
||||||
|
return await _textData.GetByPorjectId(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TextoComEmbedding> GetById(string id)
|
||||||
|
{
|
||||||
|
return await _textData.GetById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// NOVOS MÉTODOS UNIFICADOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<string> SaveDocumentAsync(DocumentInput document)
|
||||||
|
{
|
||||||
|
var id = document.Id ?? Guid.NewGuid().ToString();
|
||||||
|
await _textData.SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(string id, DocumentInput document)
|
||||||
|
{
|
||||||
|
await _textData.SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
// Implementar quando necessário ou usar TextDataRepository diretamente
|
||||||
|
throw new NotImplementedException("Delete será implementado quando migrar para interface");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = await _textData.GetById(id);
|
||||||
|
return doc != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentOutput?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = await _textData.GetById(id);
|
||||||
|
if (doc == null) return null;
|
||||||
|
|
||||||
|
return new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
Embedding = doc.Embedding,
|
||||||
|
CreatedAt = DateTime.UtcNow, // MongoDB não tem essa info no modelo atual
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
var docs = await _textData.GetByPorjectId(projectId);
|
||||||
|
return docs.Select(doc => new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
Embedding = doc.Embedding,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
var all = await _textData.GetAll();
|
||||||
|
return all.Count();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var byProject = await _textData.GetByPorjectId(projectId);
|
||||||
|
return byProject.Count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
|
||||||
|
{
|
||||||
|
var ids = new List<string>();
|
||||||
|
foreach (var doc in documents)
|
||||||
|
{
|
||||||
|
var id = await SaveDocumentAsync(doc);
|
||||||
|
ids.Add(id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentsBatchAsync(List<string> ids)
|
||||||
|
{
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
await DeleteDocumentAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
|
||||||
|
{
|
||||||
|
var totalDocs = await GetDocumentCountAsync();
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "MongoDB",
|
||||||
|
["total_documents"] = totalDocs,
|
||||||
|
["health"] = "ok",
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#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.
|
||||||
269
Data/QdrantProjectDataRepository.cs
Normal file
269
Data/QdrantProjectDataRepository.cs
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
using Google.Protobuf;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
public class QdrantProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<QdrantProjectDataRepository> _logger;
|
||||||
|
private readonly QdrantClient _qdrantClient;
|
||||||
|
private volatile bool _collectionInitialized = false;
|
||||||
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
|
|
||||||
|
public QdrantProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<QdrantProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var qdrantSettings = settings.Value.Qdrant ?? throw new ArgumentNullException("Qdrant settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{qdrantSettings.Host}:{qdrantSettings.Port}");
|
||||||
|
_collectionName = qdrantSettings.GroupsCollectionName;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Inicializa o QdrantClient - use GRPC (porta 6334) para melhor performance
|
||||||
|
_qdrantClient = new QdrantClient(qdrantSettings.Host, port: 6334, https: false);
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_collectionInitialized) return;
|
||||||
|
|
||||||
|
await _initializationSemaphore.WaitAsync();
|
||||||
|
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(_collectionName);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
_collectionInitialized = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Qdrant");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_initializationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//var scrollRequest = new ScrollPoints
|
||||||
|
//{
|
||||||
|
// CollectionName = _collectionName,
|
||||||
|
// Filter = new Filter(), // Filtro vazio
|
||||||
|
// Limit = 1000,
|
||||||
|
// WithPayload = true,
|
||||||
|
// WithVectors = false
|
||||||
|
//};
|
||||||
|
|
||||||
|
//var result = await _qdrantClient.ScrollAsync(_collectionName, scrollRequest);
|
||||||
|
var result = await _qdrantClient.ScrollAsync(_collectionName, new Filter(), 1000, null, true, false);
|
||||||
|
|
||||||
|
return result.Result.Select(ConvertToProject)
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Qdrant");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var points = await _qdrantClient.RetrieveAsync(
|
||||||
|
_collectionName,
|
||||||
|
Guid.Parse(id),
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = points.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToProject(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = new PointId { Uuid= id },
|
||||||
|
Vectors = new float[384], // Vector dummy para projetos
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = newProject.Id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = new PointId { Uuid = id },
|
||||||
|
Vectors = new float[384], // Vector dummy
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = updatedProject.Id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { new PointId { Uuid = id }.Num }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var vectorParams = new VectorParams
|
||||||
|
{
|
||||||
|
Size = 384,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.CreateCollectionAsync(_collectionName, vectorParams);
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Qdrant", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project? ConvertToProject(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (point.Payload == null) return null;
|
||||||
|
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = point.Payload.TryGetValue("id", out var idValue) ? idValue.StringValue : point.Id.ToString(),
|
||||||
|
Nome = point.Payload.TryGetValue("nome", out var nomeValue) ? nomeValue.StringValue : "",
|
||||||
|
Descricao = point.Payload.TryGetValue("descricao", out var descValue) ? descValue.StringValue : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollResult
|
||||||
|
{
|
||||||
|
public QdrantScrollData? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollData
|
||||||
|
{
|
||||||
|
public QdrantPoint[]? points { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPointResult
|
||||||
|
{
|
||||||
|
public QdrantPoint? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPoint
|
||||||
|
{
|
||||||
|
public string? id { get; set; }
|
||||||
|
public Dictionary<string, object>? payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
187
Data/TextData.cs
187
Data/TextData.cs
@ -1,5 +1,6 @@
|
|||||||
using ChatRAG.Data;
|
using ChatRAG.Data;
|
||||||
using ChatRAG.Models;
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
using Microsoft.SemanticKernel;
|
using Microsoft.SemanticKernel;
|
||||||
using Microsoft.SemanticKernel.Embeddings;
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -8,7 +9,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace ChatApi.Data
|
namespace ChatApi.Data
|
||||||
{
|
{
|
||||||
public class TextData
|
public class TextData : ITextDataService
|
||||||
{
|
{
|
||||||
private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService;
|
private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService;
|
||||||
private readonly TextDataRepository _textDataService;
|
private readonly TextDataRepository _textDataService;
|
||||||
@ -19,6 +20,12 @@ namespace ChatApi.Data
|
|||||||
_textDataService = textDataService;
|
_textDataService = textDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ProviderName => "MongoDB";
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS ORIGINAIS (já implementados)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
|
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
|
||||||
{
|
{
|
||||||
var textoArray = new List<string>();
|
var textoArray = new List<string>();
|
||||||
@ -47,7 +54,7 @@ namespace ChatApi.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach(var item in textoArray)
|
foreach (var item in textoArray)
|
||||||
{
|
{
|
||||||
await SalvarNoMongoDB(title, item, projectId);
|
await SalvarNoMongoDB(title, item, projectId);
|
||||||
}
|
}
|
||||||
@ -55,7 +62,7 @@ namespace ChatApi.Data
|
|||||||
|
|
||||||
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
|
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
|
||||||
{
|
{
|
||||||
await SalvarNoMongoDB(null, titulo, texto);
|
await SalvarNoMongoDB(null, titulo, texto, projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
|
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
|
||||||
@ -67,12 +74,13 @@ namespace ChatApi.Data
|
|||||||
// Converter embedding para um formato serializável (como um array de floats)
|
// Converter embedding para um formato serializável (como um array de floats)
|
||||||
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
var exists = id!=null ? await this.GetById(id) : null;
|
var exists = id != null ? await this.GetById(id) : null;
|
||||||
|
|
||||||
if (exists == null)
|
if (exists == null)
|
||||||
{
|
{
|
||||||
var documento = new TextoComEmbedding
|
var documento = new TextoComEmbedding
|
||||||
{
|
{
|
||||||
|
Id = id ?? Guid.NewGuid().ToString(),
|
||||||
Titulo = titulo,
|
Titulo = titulo,
|
||||||
Conteudo = texto,
|
Conteudo = texto,
|
||||||
ProjetoId = projectId,
|
ProjetoId = projectId,
|
||||||
@ -85,14 +93,14 @@ namespace ChatApi.Data
|
|||||||
{
|
{
|
||||||
var documento = new TextoComEmbedding
|
var documento = new TextoComEmbedding
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id!,
|
||||||
Titulo = titulo,
|
Titulo = titulo,
|
||||||
Conteudo = texto,
|
Conteudo = texto,
|
||||||
ProjetoId = projectId,
|
ProjetoId = projectId,
|
||||||
Embedding = embeddingArray
|
Embedding = embeddingArray
|
||||||
};
|
};
|
||||||
|
|
||||||
await _textDataService.UpdateAsync(id, documento);
|
await _textDataService.UpdateAsync(id!, documento);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,8 +116,173 @@ namespace ChatApi.Data
|
|||||||
|
|
||||||
public async Task<TextoComEmbedding> GetById(string id)
|
public async Task<TextoComEmbedding> GetById(string id)
|
||||||
{
|
{
|
||||||
return await _textDataService.GetAsync(id);
|
return (await _textDataService.GetAsync(id))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS NOVOS DA INTERFACE (implementação completa)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<string> SaveDocumentAsync(DocumentInput document)
|
||||||
|
{
|
||||||
|
var id = document.Id ?? Guid.NewGuid().ToString();
|
||||||
|
await SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(string id, DocumentInput document)
|
||||||
|
{
|
||||||
|
await SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
await _textDataService.RemoveAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = await GetById(id);
|
||||||
|
return doc != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentOutput?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = await GetById(id);
|
||||||
|
if (doc == null) return null;
|
||||||
|
|
||||||
|
return new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
Embedding = doc.Embedding,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
Metadata = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["source"] = "MongoDB",
|
||||||
|
["has_embedding"] = doc.Embedding != null,
|
||||||
|
["embedding_size"] = doc.Embedding?.Length ?? 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
var docs = await GetByPorjectId(projectId);
|
||||||
|
return docs.Select(doc => new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
Embedding = doc.Embedding,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
Metadata = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["source"] = "MongoDB",
|
||||||
|
["has_embedding"] = doc.Embedding != null,
|
||||||
|
["embedding_size"] = doc.Embedding?.Length ?? 0
|
||||||
|
}
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
var all = await GetAll();
|
||||||
|
return all.Count();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var byProject = await GetByPorjectId(projectId);
|
||||||
|
return byProject.Count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
|
||||||
|
{
|
||||||
|
var ids = new List<string>();
|
||||||
|
|
||||||
|
foreach (var doc in documents)
|
||||||
|
{
|
||||||
|
var id = await SaveDocumentAsync(doc);
|
||||||
|
ids.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentsBatchAsync(List<string> ids)
|
||||||
|
{
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
await DeleteDocumentAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var totalDocs = await GetDocumentCountAsync();
|
||||||
|
var allDocs = await GetAll();
|
||||||
|
|
||||||
|
var docsWithEmbedding = allDocs.Count(d => d.Embedding != null && d.Embedding.Length > 0);
|
||||||
|
var avgContentLength = allDocs.Any() ? allDocs.Average(d => d.Conteudo?.Length ?? 0) : 0;
|
||||||
|
|
||||||
|
var projectStats = allDocs
|
||||||
|
.GroupBy(d => d.ProjetoId)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key ?? "unknown",
|
||||||
|
g => g.Count()
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "MongoDB",
|
||||||
|
["total_documents"] = totalDocs,
|
||||||
|
["documents_with_embedding"] = docsWithEmbedding,
|
||||||
|
["embedding_coverage"] = totalDocs > 0 ? (double)docsWithEmbedding / totalDocs : 0,
|
||||||
|
["average_content_length"] = Math.Round(avgContentLength, 1),
|
||||||
|
["projects_count"] = projectStats.Count,
|
||||||
|
["documents_by_project"] = projectStats,
|
||||||
|
["health"] = "ok",
|
||||||
|
["last_check"] = DateTime.UtcNow,
|
||||||
|
["connection_status"] = "connected"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "MongoDB",
|
||||||
|
["health"] = "error",
|
||||||
|
["error"] = ex.Message,
|
||||||
|
["last_check"] = DateTime.UtcNow,
|
||||||
|
["connection_status"] = "error"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#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.
|
#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.
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using ChatApi;
|
using ChatApi;
|
||||||
using ChatRAG.Models;
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
@ -11,16 +12,16 @@ namespace ChatRAG.Data
|
|||||||
private readonly IMongoCollection<TextoComEmbedding> _textsCollection;
|
private readonly IMongoCollection<TextoComEmbedding> _textsCollection;
|
||||||
|
|
||||||
public TextDataRepository(
|
public TextDataRepository(
|
||||||
IOptions<DomvsDatabaseSettings> bookStoreDatabaseSettings)
|
IOptions<VectorDatabaseSettings> vectorStoreDatabaseSettings)
|
||||||
{
|
{
|
||||||
var mongoClient = new MongoClient(
|
var mongoClient = new MongoClient(
|
||||||
bookStoreDatabaseSettings.Value.ConnectionString);
|
vectorStoreDatabaseSettings.Value.MongoDB.ConnectionString);
|
||||||
|
|
||||||
var mongoDatabase = mongoClient.GetDatabase(
|
var mongoDatabase = mongoClient.GetDatabase(
|
||||||
bookStoreDatabaseSettings.Value.DatabaseName);
|
vectorStoreDatabaseSettings.Value.MongoDB.DatabaseName);
|
||||||
|
|
||||||
_textsCollection = mongoDatabase.GetCollection<TextoComEmbedding>(
|
_textsCollection = mongoDatabase.GetCollection<TextoComEmbedding>(
|
||||||
bookStoreDatabaseSettings.Value.TextCollectionName);
|
vectorStoreDatabaseSettings.Value.MongoDB.TextCollectionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IMongoCollection<TextoComEmbedding> GetCollection()
|
public IMongoCollection<TextoComEmbedding> GetCollection()
|
||||||
@ -32,7 +33,7 @@ namespace ChatRAG.Data
|
|||||||
await _textsCollection.Find(_ => true).ToListAsync();
|
await _textsCollection.Find(_ => true).ToListAsync();
|
||||||
|
|
||||||
public async Task<List<TextoComEmbedding>> GetByProjectIdAsync(string projectId) =>
|
public async Task<List<TextoComEmbedding>> GetByProjectIdAsync(string projectId) =>
|
||||||
await _textsCollection.Find(s => s.ProjetoId == ObjectId.Parse(projectId).ToString()).ToListAsync();
|
await _textsCollection.Find(s => s.ProjetoId == projectId).ToListAsync();
|
||||||
|
|
||||||
public async Task<TextoComEmbedding?> GetAsync(string id) =>
|
public async Task<TextoComEmbedding?> GetAsync(string id) =>
|
||||||
await _textsCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
|
await _textsCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using ChatApi.Models;
|
using ChatApi.Models;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
@ -9,16 +10,16 @@ namespace ChatApi
|
|||||||
private readonly IMongoCollection<UserData> _userCollection;
|
private readonly IMongoCollection<UserData> _userCollection;
|
||||||
|
|
||||||
public UserDataRepository(
|
public UserDataRepository(
|
||||||
IOptions<DomvsDatabaseSettings> bookStoreDatabaseSettings)
|
IOptions<VectorDatabaseSettings> vectorStoreDatabaseSettings)
|
||||||
{
|
{
|
||||||
var mongoClient = new MongoClient(
|
var mongoClient = new MongoClient(
|
||||||
bookStoreDatabaseSettings.Value.ConnectionString);
|
vectorStoreDatabaseSettings.Value.MongoDB.ConnectionString);
|
||||||
|
|
||||||
var mongoDatabase = mongoClient.GetDatabase(
|
var mongoDatabase = mongoClient.GetDatabase(
|
||||||
bookStoreDatabaseSettings.Value.DatabaseName);
|
vectorStoreDatabaseSettings.Value.MongoDB.DatabaseName);
|
||||||
|
|
||||||
_userCollection = mongoDatabase.GetCollection<UserData>(
|
_userCollection = mongoDatabase.GetCollection<UserData>(
|
||||||
bookStoreDatabaseSettings.Value.UserDataName);
|
vectorStoreDatabaseSettings.Value.MongoDB.UserDataName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<UserData>> GetAsync() =>
|
public async Task<List<UserData>> GetAsync() =>
|
||||||
|
|||||||
263
Data/hnwhoaao.xfh~
Normal file
263
Data/hnwhoaao.xfh~
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
public class QdrantProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<QdrantProjectDataRepository> _logger;
|
||||||
|
private readonly QdrantClient _qdrantClient;
|
||||||
|
private volatile bool _collectionInitialized = false;
|
||||||
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
|
|
||||||
|
public QdrantProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<QdrantProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var qdrantSettings = settings.Value.Qdrant ?? throw new ArgumentNullException("Qdrant settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{qdrantSettings.Host}:{qdrantSettings.Port}");
|
||||||
|
_collectionName = qdrantSettings.GroupsCollectionName;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Inicializa o QdrantClient - use GRPC (porta 6334) para melhor performance
|
||||||
|
_qdrantClient = new QdrantClient(qdrantSettings.Host, port: 6334, https: false);
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_collectionInitialized) return;
|
||||||
|
|
||||||
|
await _initializationSemaphore.WaitAsync();
|
||||||
|
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(_collectionName);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
_collectionInitialized = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Qdrant");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//var scrollRequest = new ScrollPoints
|
||||||
|
//{
|
||||||
|
// CollectionName = _collectionName,
|
||||||
|
// Filter = new Filter(), // Filtro vazio
|
||||||
|
// Limit = 1000,
|
||||||
|
// WithPayload = true,
|
||||||
|
// WithVectors = false
|
||||||
|
//};
|
||||||
|
|
||||||
|
//var result = await _qdrantClient.ScrollAsync(_collectionName, scrollRequest);
|
||||||
|
var result = await _qdrantClient.ScrollAsync(_collectionName, new Filter(), 1000, null, true, false);
|
||||||
|
|
||||||
|
return result.Result.Select(ConvertToProject)
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Qdrant");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var points = await _qdrantClient.RetrieveAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)) },
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = points.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToProject(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)),
|
||||||
|
Vectors = new float[384], // Vector dummy para projetos
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = newProject.Id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)),
|
||||||
|
Vectors = new float[384], // Vector dummy
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = updatedProject.Id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)).Num }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var vectorParams = new VectorParams
|
||||||
|
{
|
||||||
|
Size = 384,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.CreateCollectionAsync(_collectionName, vectorParams);
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Qdrant", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project? ConvertToProject(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (point.Payload == null) return null;
|
||||||
|
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = point.Payload.TryGetValue("id", out var idValue) ? idValue.StringValue : point.Id.ToString(),
|
||||||
|
Nome = point.Payload.TryGetValue("nome", out var nomeValue) ? nomeValue.StringValue : "",
|
||||||
|
Descricao = point.Payload.TryGetValue("descricao", out var descValue) ? descValue.StringValue : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollResult
|
||||||
|
{
|
||||||
|
public QdrantScrollData? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollData
|
||||||
|
{
|
||||||
|
public QdrantPoint[]? points { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPointResult
|
||||||
|
{
|
||||||
|
public QdrantPoint? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPoint
|
||||||
|
{
|
||||||
|
public string? id { get; set; }
|
||||||
|
public Dictionary<string, object>? payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
268
Data/wgbnjwfg.nr3~
Normal file
268
Data/wgbnjwfg.nr3~
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Data
|
||||||
|
{
|
||||||
|
public class QdrantProjectDataRepository : IProjectDataRepository
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
private readonly ILogger<QdrantProjectDataRepository> _logger;
|
||||||
|
private readonly QdrantClient _qdrantClient;
|
||||||
|
private volatile bool _collectionInitialized = false;
|
||||||
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
|
|
||||||
|
public QdrantProjectDataRepository(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<QdrantProjectDataRepository> logger)
|
||||||
|
{
|
||||||
|
var qdrantSettings = settings.Value.Qdrant ?? throw new ArgumentNullException("Qdrant settings not configured");
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{qdrantSettings.Host}:{qdrantSettings.Port}");
|
||||||
|
_collectionName = qdrantSettings.GroupsCollectionName;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Inicializa o QdrantClient - use GRPC (porta 6334) para melhor performance
|
||||||
|
_qdrantClient = new QdrantClient(qdrantSettings.Host, port: 6334, https: false);
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_collectionInitialized) return;
|
||||||
|
|
||||||
|
await _initializationSemaphore.WaitAsync();
|
||||||
|
|
||||||
|
var exists = await _qdrantClient.CollectionExistsAsync(_collectionName);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
await CreateProjectsCollection();
|
||||||
|
}
|
||||||
|
_collectionInitialized = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar collection de projetos no Qdrant");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_initializationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Project>> GetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//var scrollRequest = new ScrollPoints
|
||||||
|
//{
|
||||||
|
// CollectionName = _collectionName,
|
||||||
|
// Filter = new Filter(), // Filtro vazio
|
||||||
|
// Limit = 1000,
|
||||||
|
// WithPayload = true,
|
||||||
|
// WithVectors = false
|
||||||
|
//};
|
||||||
|
|
||||||
|
//var result = await _qdrantClient.ScrollAsync(_collectionName, scrollRequest);
|
||||||
|
var result = await _qdrantClient.ScrollAsync(_collectionName, new Filter(), 1000, null, true, false);
|
||||||
|
|
||||||
|
return result.Result.Select(ConvertToProject)
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar projetos do Qdrant");
|
||||||
|
return new List<Project>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var points = await _qdrantClient.RetrieveAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)) },
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = points.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToProject(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar projeto {Id} no Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateAsync(Project newProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = string.IsNullOrEmpty(newProject.Id) ? Guid.NewGuid().ToString() : newProject.Id;
|
||||||
|
newProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)),
|
||||||
|
Vectors = new float[384], // Vector dummy para projetos
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = newProject.Id,
|
||||||
|
["nome"] = newProject.Nome,
|
||||||
|
["descricao"] = newProject.Descricao,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao criar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(string id, Project updatedProject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updatedProject.Id = id;
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)),
|
||||||
|
Vectors = new float[384], // Vector dummy
|
||||||
|
Payload =
|
||||||
|
{
|
||||||
|
["id"] = updatedProject.Id,
|
||||||
|
["nome"] = updatedProject.Nome,
|
||||||
|
["descricao"] = updatedProject.Descricao,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["entity_type"] = "project"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.UpsertAsync(_collectionName, new[] { point });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar projeto {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Project project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(project.Id))
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var existing = await GetAsync(project.Id);
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
await CreateAsync(project);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateAsync(project.Id, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar projeto no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _qdrantClient.DeleteAsync(
|
||||||
|
_collectionName,
|
||||||
|
new[] { PointId.Parser.ParseFrom(Encoding.ASCII.GetBytes(id)).Num }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover projeto {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateProjectsCollection()
|
||||||
|
{
|
||||||
|
var vectorParams = new VectorParams
|
||||||
|
{
|
||||||
|
Size = 384,
|
||||||
|
Distance = Distance.Cosine
|
||||||
|
};
|
||||||
|
|
||||||
|
await _qdrantClient.CreateCollectionAsync(_collectionName, vectorParams);
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection de projetos '{CollectionName}' criada no Qdrant", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Project? ConvertToProject(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (point.Payload == null) return null;
|
||||||
|
|
||||||
|
return new Project
|
||||||
|
{
|
||||||
|
Id = point.Payload.TryGetValue("id", out var idValue) ? idValue.StringValue : point.Id.ToString(),
|
||||||
|
Nome = point.Payload.TryGetValue("nome", out var nomeValue) ? nomeValue.StringValue : "",
|
||||||
|
Descricao = point.Payload.TryGetValue("descricao", out var descValue) ? descValue.StringValue : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollResult
|
||||||
|
{
|
||||||
|
public QdrantScrollData? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantScrollData
|
||||||
|
{
|
||||||
|
public QdrantPoint[]? points { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPointResult
|
||||||
|
{
|
||||||
|
public QdrantPoint? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantPoint
|
||||||
|
{
|
||||||
|
public string? id { get; set; }
|
||||||
|
public Dictionary<string, object>? payload { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +0,0 @@
|
|||||||
namespace ChatApi
|
|
||||||
{
|
|
||||||
public class DomvsDatabaseSettings
|
|
||||||
{
|
|
||||||
public string ConnectionString { get; set; } = null!;
|
|
||||||
|
|
||||||
public string DatabaseName { get; set; } = null!;
|
|
||||||
|
|
||||||
public string TextCollectionName { get; set; } = null!;
|
|
||||||
|
|
||||||
public string UserDataName { get; set; } = null!;
|
|
||||||
|
|
||||||
public string ProjectCollectionName { get; set; } = null!;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
100
Extensions/ServiceCollectionExtensions.cs
Normal file
100
Extensions/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using ChatApi.Data;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Services;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.ResponseService;
|
||||||
|
using ChatRAG.Services.SearchVectors;
|
||||||
|
using ChatRAG.Services.TextServices;
|
||||||
|
using ChatRAG.Settings;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Qdrant.Client;
|
||||||
|
|
||||||
|
namespace ChatRAG.Extensions
|
||||||
|
{
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registra o sistema completo de Vector Database
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddVectorDatabase(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
// Registra e valida configurações
|
||||||
|
services.Configure<VectorDatabaseSettings>(
|
||||||
|
configuration.GetSection("VectorDatabase"));
|
||||||
|
|
||||||
|
services.AddSingleton<IValidateOptions<VectorDatabaseSettings>,
|
||||||
|
ChatRAG.Settings.VectorDatabaseSettingsValidator>();
|
||||||
|
|
||||||
|
// Registra factory principal
|
||||||
|
services.AddScoped<IVectorDatabaseFactory, VectorDatabaseFactory>();
|
||||||
|
|
||||||
|
// Registra implementações de todos os providers
|
||||||
|
services.AddMongoDbProvider();
|
||||||
|
services.AddQdrantProvider(); // 👈 Agora ativo!
|
||||||
|
|
||||||
|
// Registra interfaces principais usando factory
|
||||||
|
services.AddScoped<IVectorSearchService>(provider =>
|
||||||
|
{
|
||||||
|
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
|
||||||
|
return factory.CreateVectorSearchService();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddScoped<ITextDataService>(provider =>
|
||||||
|
{
|
||||||
|
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
|
||||||
|
return factory.CreateTextDataService();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddScoped<IResponseService>(provider =>
|
||||||
|
{
|
||||||
|
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
|
||||||
|
return factory.CreateResponseService();
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registra implementações MongoDB (suas classes atuais)
|
||||||
|
/// </summary>
|
||||||
|
private static IServiceCollection AddMongoDbProvider(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<TextData>(); // Implementa ITextDataService
|
||||||
|
services.AddScoped<TextDataRepository>();
|
||||||
|
services.AddScoped<ResponseRAGService>(); // Implementa IResponseService
|
||||||
|
services.AddScoped<MongoVectorSearchService>(); // Wrapper para IVectorSearchService
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registra implementações Qdrant
|
||||||
|
/// </summary>
|
||||||
|
private static IServiceCollection AddQdrantProvider(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// ✅ Cliente Qdrant configurado
|
||||||
|
services.AddScoped<QdrantClient>(provider =>
|
||||||
|
{
|
||||||
|
var settings = provider.GetRequiredService<IOptions<VectorDatabaseSettings>>();
|
||||||
|
var qdrantSettings = settings.Value.Qdrant;
|
||||||
|
|
||||||
|
return new QdrantClient(
|
||||||
|
host: qdrantSettings.Host,
|
||||||
|
port: qdrantSettings.Port,
|
||||||
|
https: qdrantSettings.UseTls
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Serviços Qdrant
|
||||||
|
services.AddScoped<QdrantVectorSearchService>();
|
||||||
|
services.AddScoped<QdrantTextDataService>();
|
||||||
|
services.AddScoped<QdrantResponseService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Models/DocumentInput.cs
Normal file
43
Models/DocumentInput.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
namespace ChatRAG.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Modelo para entrada de dados (CREATE/UPDATE)
|
||||||
|
/// </summary>
|
||||||
|
public class DocumentInput
|
||||||
|
{
|
||||||
|
public string? Id { get; set; }
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public string ProjectId { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
public DateTime? CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public static DocumentInput FromTextoComEmbedding(TextoComEmbedding texto)
|
||||||
|
{
|
||||||
|
return new DocumentInput
|
||||||
|
{
|
||||||
|
Id = texto.Id,
|
||||||
|
Title = texto.Titulo,
|
||||||
|
Content = texto.Conteudo,
|
||||||
|
ProjectId = texto.ProjetoId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public DocumentInput WithMetadata(string key, object value)
|
||||||
|
{
|
||||||
|
Metadata ??= new Dictionary<string, object>();
|
||||||
|
Metadata[key] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValid()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(Title) &&
|
||||||
|
!string.IsNullOrWhiteSpace(Content) &&
|
||||||
|
!string.IsNullOrWhiteSpace(ProjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Models/DocumentOutput.cs
Normal file
70
Models/DocumentOutput.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
namespace ChatRAG.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Modelo para saída de dados (READ)
|
||||||
|
/// </summary>
|
||||||
|
public class DocumentOutput
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public string ProjectId { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public double[]? Embedding { get; set; }
|
||||||
|
|
||||||
|
public TextoComEmbedding ToTextoComEmbedding()
|
||||||
|
{
|
||||||
|
return new TextoComEmbedding
|
||||||
|
{
|
||||||
|
Id = Id,
|
||||||
|
Titulo = Title,
|
||||||
|
Conteudo = Content,
|
||||||
|
ProjetoId = ProjectId,
|
||||||
|
Embedding = Embedding
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DocumentOutput FromTextoComEmbedding(TextoComEmbedding texto)
|
||||||
|
{
|
||||||
|
return new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = texto.Id,
|
||||||
|
Title = texto.Titulo,
|
||||||
|
Content = texto.Conteudo,
|
||||||
|
ProjectId = texto.ProjetoId,
|
||||||
|
Embedding = texto.Embedding,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetContentPreview(int maxLength = 200)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Content))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (Content.Length <= maxLength)
|
||||||
|
return Content;
|
||||||
|
|
||||||
|
return Content.Substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasEmbedding() => Embedding != null && Embedding.Length > 0;
|
||||||
|
|
||||||
|
public DocumentInput ToInput()
|
||||||
|
{
|
||||||
|
return new DocumentInput
|
||||||
|
{
|
||||||
|
Id = Id,
|
||||||
|
Title = Title,
|
||||||
|
Content = Content,
|
||||||
|
ProjectId = ProjectId,
|
||||||
|
Metadata = Metadata,
|
||||||
|
CreatedAt = CreatedAt,
|
||||||
|
UpdatedAt = UpdatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Models/MigrationResult.cs
Normal file
22
Models/MigrationResult.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
namespace ChatRAG.Models
|
||||||
|
{
|
||||||
|
public class MigrationResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public TimeSpan Duration { get; set; }
|
||||||
|
public int TotalDocuments { get; set; }
|
||||||
|
public int MigratedDocuments { get; set; }
|
||||||
|
public List<string> Errors { get; set; } = new();
|
||||||
|
public ValidationResult? ValidationResult { get; set; }
|
||||||
|
|
||||||
|
public double SuccessRate => TotalDocuments > 0 ? (double)MigratedDocuments / TotalDocuments : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidationResult
|
||||||
|
{
|
||||||
|
public bool IsValid { get; set; }
|
||||||
|
public List<string> Issues { get; set; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Models/Models.cs
Normal file
47
Models/Models.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
namespace ChatRAG.Models
|
||||||
|
{
|
||||||
|
public class ResponseOptions
|
||||||
|
{
|
||||||
|
public int MaxContextDocuments { get; set; } = 3;
|
||||||
|
public double SimilarityThreshold { get; set; } = 0.3;
|
||||||
|
public bool IncludeSourceDetails { get; set; } = false;
|
||||||
|
public bool IncludeTiming { get; set; } = true;
|
||||||
|
public Dictionary<string, object>? Filters { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseResult
|
||||||
|
{
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public List<SourceDocument> Sources { get; set; } = new();
|
||||||
|
public ResponseMetrics Metrics { get; set; } = new();
|
||||||
|
public string Provider { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SourceDocument
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public double Similarity { get; set; }
|
||||||
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseMetrics
|
||||||
|
{
|
||||||
|
public long TotalTimeMs { get; set; }
|
||||||
|
public long SearchTimeMs { get; set; }
|
||||||
|
public long LlmTimeMs { get; set; }
|
||||||
|
public int DocumentsFound { get; set; }
|
||||||
|
public int DocumentsUsed { get; set; }
|
||||||
|
public double AverageSimilarity { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseStats
|
||||||
|
{
|
||||||
|
public int TotalRequests { get; set; }
|
||||||
|
public double AverageResponseTime { get; set; }
|
||||||
|
public Dictionary<string, int> RequestsByProject { get; set; } = new();
|
||||||
|
public DateTime LastRequest { get; set; }
|
||||||
|
public string Provider { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Models/VectorSearchResult.cs
Normal file
144
Models/VectorSearchResult.cs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
namespace ChatRAG.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resultado padronizado de busca vetorial
|
||||||
|
/// Funciona com qualquer provider (MongoDB, Qdrant, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public class VectorSearchResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ID único do documento
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Título do documento
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conteúdo completo do documento
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ID do projeto ao qual pertence
|
||||||
|
/// </summary>
|
||||||
|
public string ProjectId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score de similaridade (0.0 a 1.0, onde 1.0 é idêntico)
|
||||||
|
/// </summary>
|
||||||
|
public double Score { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Embedding vetorial (opcional - nem sempre retornado por performance)
|
||||||
|
/// </summary>
|
||||||
|
public double[]? Embedding { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadados adicionais (tags, categoria, autor, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data de criação do documento
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data da última atualização
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// INFORMAÇÕES DO PROVIDER
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome do provider que retornou este resultado (MongoDB, Qdrant, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public string Provider { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Informações específicas do provider (índices, shards, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object>? ProviderSpecific { get; set; }
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS DE CONVENIÊNCIA
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preview do conteúdo (primeiros N caracteres)
|
||||||
|
/// </summary>
|
||||||
|
public string GetContentPreview(int maxLength = 200)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Content))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (Content.Length <= maxLength)
|
||||||
|
return Content;
|
||||||
|
|
||||||
|
return Content.Substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score formatado como percentual
|
||||||
|
/// </summary>
|
||||||
|
public string GetScorePercentage()
|
||||||
|
{
|
||||||
|
return $"{Score:P1}"; // Ex: "85.3%"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indica se é um resultado relevante (score alto)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsHighRelevance(double threshold = 0.7)
|
||||||
|
{
|
||||||
|
return Score >= threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converte para o modelo atual do sistema (compatibilidade)
|
||||||
|
/// </summary>
|
||||||
|
public TextoComEmbedding ToTextoComEmbedding()
|
||||||
|
{
|
||||||
|
return new TextoComEmbedding
|
||||||
|
{
|
||||||
|
Id = Id,
|
||||||
|
Titulo = Title,
|
||||||
|
Conteudo = Content,
|
||||||
|
ProjetoId = ProjectId,
|
||||||
|
Embedding = Embedding
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converte do modelo atual do sistema
|
||||||
|
/// </summary>
|
||||||
|
public static VectorSearchResult FromTextoComEmbedding(
|
||||||
|
TextoComEmbedding texto,
|
||||||
|
double score = 1.0,
|
||||||
|
string provider = "Unknown")
|
||||||
|
{
|
||||||
|
return new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = texto.Id,
|
||||||
|
Title = texto.Titulo,
|
||||||
|
Content = texto.Conteudo,
|
||||||
|
ProjectId = texto.ProjetoId,
|
||||||
|
Score = score,
|
||||||
|
Embedding = texto.Embedding,
|
||||||
|
Provider = provider,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Title} (Score: {GetScorePercentage()}, Provider: {Provider})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Program.cs
138
Program.cs
@ -3,9 +3,18 @@ using ChatApi.Data;
|
|||||||
using ChatApi.Middlewares;
|
using ChatApi.Middlewares;
|
||||||
using ChatApi.Services.Crypt;
|
using ChatApi.Services.Crypt;
|
||||||
using ChatApi.Settings;
|
using ChatApi.Settings;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
using ChatRAG.Data;
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Extensions;
|
||||||
using ChatRAG.Services;
|
using ChatRAG.Services;
|
||||||
|
using ChatRAG.Services.Confidence;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.PromptConfiguration;
|
||||||
using ChatRAG.Services.ResponseService;
|
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.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
@ -16,6 +25,7 @@ using Microsoft.SemanticKernel;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using static OllamaSharp.OllamaApiClient;
|
using static OllamaSharp.OllamaApiClient;
|
||||||
using static System.Net.Mime.MediaTypeNames;
|
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.
|
#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.
|
||||||
|
|
||||||
@ -69,28 +79,134 @@ builder.Services.AddSwaggerGen(c =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.Configure<DomvsDatabaseSettings>(
|
builder.Services.Configure<ConfidenceSettings>(
|
||||||
builder.Configuration.GetSection("DomvsDatabase"));
|
builder.Configuration.GetSection("Confidence"));
|
||||||
|
|
||||||
builder.Services.Configure<ChatRHSettings>(
|
builder.Services.Configure<ConfidenceAwareSettings>(
|
||||||
builder.Configuration.GetSection("ChatRHSettings"));
|
builder.Configuration.GetSection("ConfidenceAware"));
|
||||||
|
|
||||||
|
|
||||||
|
//builder.Services.AddScoped<IVectorSearchService, MongoVectorSearchService>();
|
||||||
|
builder.Services.AddScoped<QdrantVectorSearchService>();
|
||||||
|
builder.Services.AddScoped<MongoVectorSearchService>();
|
||||||
|
builder.Services.AddScoped<ChromaVectorSearchService>();
|
||||||
|
|
||||||
|
builder.Services.AddVectorDatabase(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IVectorSearchService>(provider =>
|
||||||
|
{
|
||||||
|
var useQdrant = builder.Configuration["Features:UseQdrant"] == "true";
|
||||||
|
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
|
||||||
|
return factory.CreateVectorSearchService();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<QdrantProjectDataRepository>();
|
||||||
|
builder.Services.AddScoped<MongoProjectDataRepository>();
|
||||||
|
builder.Services.AddScoped<ChromaProjectDataRepository>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IProjectDataRepository>(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<QdrantProjectDataRepository>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("MongoDB", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<MongoProjectDataRepository>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("Chroma", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<ChromaProjectDataRepository>();
|
||||||
|
}
|
||||||
|
return provider.GetRequiredService<MongoProjectDataRepository>();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<QdrantTextDataService>();
|
||||||
|
builder.Services.AddScoped<MongoTextDataService>();
|
||||||
|
builder.Services.AddScoped<ChromaTextDataService>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ITextDataService>(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<QdrantTextDataService>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("MongoDB", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<MongoTextDataService>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("Chroma", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<ChromaTextDataService>();
|
||||||
|
}
|
||||||
|
return provider.GetRequiredService<MongoTextDataService>();
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddSingleton<ChatHistoryService>();
|
builder.Services.AddSingleton<ChatHistoryService>();
|
||||||
builder.Services.AddScoped<TextDataRepository>();
|
builder.Services.AddScoped<TextDataRepository>();
|
||||||
builder.Services.AddScoped<ProjectDataRepository>();
|
|
||||||
builder.Services.AddSingleton<TextFilter>();
|
builder.Services.AddSingleton<TextFilter>();
|
||||||
|
|
||||||
builder.Services.AddScoped<IResponseService, ResponseRAGService>();
|
//builder.Services.AddScoped<IResponseService, ResponseRAGService>();
|
||||||
|
builder.Services.AddScoped<ResponseRAGService>();
|
||||||
|
builder.Services.AddScoped<HierarchicalRAGService>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IResponseService>(provider =>
|
||||||
|
{
|
||||||
|
var configuration = provider.GetService<IConfiguration>();
|
||||||
|
var useHierarchical = configuration?.GetValue<bool>("Features:UseHierarchicalRAG") ?? false;
|
||||||
|
var useConfidence = configuration?.GetValue<bool>("Features:UseConfidenceAwareRAG") ?? false;
|
||||||
|
|
||||||
|
return useConfidence && useHierarchical
|
||||||
|
? provider.GetRequiredService<ConfidenceAwareRAGService>()
|
||||||
|
: useHierarchical
|
||||||
|
? provider.GetRequiredService<HierarchicalRAGService>()
|
||||||
|
: provider.GetRequiredService<ResponseRAGService>();
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddTransient<UserDataRepository>();
|
builder.Services.AddTransient<UserDataRepository>();
|
||||||
builder.Services.AddTransient<TextData>();
|
builder.Services.AddTransient<TextData>();
|
||||||
builder.Services.AddSingleton<CryptUtil>();
|
builder.Services.AddSingleton<CryptUtil>();
|
||||||
|
|
||||||
|
// Registrar serviços de confiança
|
||||||
|
builder.Services.AddScoped<ConfidenceVerifier>();
|
||||||
|
builder.Services.AddSingleton<PromptConfigurationService>();
|
||||||
|
|
||||||
|
// Registrar ConfidenceAwareRAGService
|
||||||
|
builder.Services.AddScoped<ConfidenceAwareRAGService>();
|
||||||
|
|
||||||
//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435"));
|
//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435"));
|
||||||
//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435"));
|
//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435"));
|
||||||
//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin");
|
//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin");
|
||||||
|
|
||||||
//Olllama
|
//Olllama
|
||||||
builder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:11434"));
|
//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("tinydolphin", new Uri("http://localhost:11435"));
|
||||||
//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435"));
|
//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435"));
|
||||||
@ -103,7 +219,13 @@ builder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:1
|
|||||||
//builder.Services.AddOllamaChatCompletion("llama3.1:latest", new Uri("http://192.168.0.150:11434"));
|
//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"));
|
//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://192.168.0.150:11434"));
|
||||||
builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost: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.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435"));
|
||||||
//builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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");
|
//builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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");
|
||||||
//builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8");
|
//builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8");
|
||||||
|
|||||||
389
Services/Confidence/ConfidenceVerifier.cs
Normal file
389
Services/Confidence/ConfidenceVerifier.cs
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.ResponseService;
|
||||||
|
using ChatRAG.Settings;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.Confidence
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se o RAG deve responder baseado na confiança dos resultados
|
||||||
|
/// </summary>
|
||||||
|
public class ConfidenceVerifier
|
||||||
|
{
|
||||||
|
private readonly ILogger<ConfidenceVerifier> _logger;
|
||||||
|
private readonly ConfidenceSettings _settings;
|
||||||
|
|
||||||
|
public ConfidenceVerifier(
|
||||||
|
ILogger<ConfidenceVerifier> logger,
|
||||||
|
IOptions<ConfidenceSettings> settings)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_settings = settings.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se deve responder baseado na análise, resultados e contexto
|
||||||
|
/// </summary>
|
||||||
|
public ConfidenceResult VerifyConfidence(
|
||||||
|
QueryAnalysis analysis,
|
||||||
|
List<VectorSearchResult> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula métricas detalhadas de confiança
|
||||||
|
/// </summary>
|
||||||
|
private ConfidenceMetrics CalculateConfidenceMetrics(
|
||||||
|
List<VectorSearchResult> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se há conteúdo específico relacionado aos conceitos da query
|
||||||
|
/// </summary>
|
||||||
|
private bool HasSpecificContent(List<VectorSearchResult> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula score geral considerando múltiplos fatores
|
||||||
|
/// </summary>
|
||||||
|
private double CalculateOverallScore(List<VectorSearchResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula variância dos scores para medir consistência
|
||||||
|
/// </summary>
|
||||||
|
private double CalculateScoreVariance(List<VectorSearchResult> 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula diversidade do conteúdo (documentos diferentes)
|
||||||
|
/// </summary>
|
||||||
|
private double CalculateContentDiversity(List<VectorSearchResult> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calcula cobertura dos conceitos da query
|
||||||
|
/// </summary>
|
||||||
|
private double CalculateConceptCoverage(List<VectorSearchResult> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decide se deve responder baseado na estratégia e métricas
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lógica específica para estratégia Overview
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lógica específica para estratégia Specific
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lógica específica para estratégia Detailed
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém thresholds ajustados para modo restrito/relaxado
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gera explicação do motivo da decisão
|
||||||
|
/// </summary>
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gera resposta de fallback apropriada
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resultado da verificação de confiança
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Métricas detalhadas de confiança
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Services/Contracts/IProjectDataRepository.cs
Normal file
14
Services/Contracts/IProjectDataRepository.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.Contracts
|
||||||
|
{
|
||||||
|
public interface IProjectDataRepository
|
||||||
|
{
|
||||||
|
Task<List<Project>> GetAsync();
|
||||||
|
Task<Project?> GetAsync(string id);
|
||||||
|
Task CreateAsync(Project newProject);
|
||||||
|
Task UpdateAsync(string id, Project updatedProject);
|
||||||
|
Task SaveAsync(Project project);
|
||||||
|
Task RemoveAsync(string id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
using ChatApi.Models;
|
using ChatApi.Models;
|
||||||
|
|
||||||
namespace ChatRAG.Services.ResponseService
|
namespace ChatRAG.Services.Contracts
|
||||||
{
|
{
|
||||||
public interface IResponseService
|
public interface IResponseService
|
||||||
{
|
{
|
||||||
Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question);
|
Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question);
|
||||||
|
Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
Services/Contracts/IResponseServiceExtended.cs
Normal file
17
Services/Contracts/IResponseServiceExtended.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using ChatApi.Models;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.Contracts
|
||||||
|
{
|
||||||
|
public interface IResponseServiceExtended : IResponseService
|
||||||
|
{
|
||||||
|
Task<ResponseResult> GetResponseDetailed(
|
||||||
|
UserData userData,
|
||||||
|
string projectId,
|
||||||
|
string sessionId,
|
||||||
|
string question,
|
||||||
|
ResponseOptions? options = null);
|
||||||
|
|
||||||
|
Task<ResponseStats> GetStatsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
143
Services/Contracts/ITextDataService.cs
Normal file
143
Services/Contracts/ITextDataService.cs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.Contracts
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface unificada para operações de documentos de texto.
|
||||||
|
/// Permite alternar entre MongoDB, Qdrant, ou outros providers sem quebrar código.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITextDataService
|
||||||
|
{
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS ORIGINAIS (compatibilidade com TextData.cs atual)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva texto no banco (método original do seu TextData.cs)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="titulo">Título do documento</param>
|
||||||
|
/// <param name="texto">Conteúdo do documento</param>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
Task SalvarNoMongoDB(string titulo, string texto, string projectId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva ou atualiza texto com ID específico (método original)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento (null para criar novo)</param>
|
||||||
|
/// <param name="titulo">Título do documento</param>
|
||||||
|
/// <param name="texto">Conteúdo do documento</param>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processa texto completo dividindo por seções (método original)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="textoCompleto">Texto com divisões marcadas por **</param>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera todos os documentos (método original)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Lista de todos os documentos</returns>
|
||||||
|
Task<IEnumerable<TextoComEmbedding>> GetAll();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera documentos por projeto (método original)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
/// <returns>Lista de documentos do projeto</returns>
|
||||||
|
Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera documento por ID (método original)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <returns>Documento ou null se não encontrado</returns>
|
||||||
|
Task<TextoComEmbedding> GetById(string id);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS NOVOS (interface moderna e unificada)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva documento usando modelo unificado
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="document">Dados do documento</param>
|
||||||
|
/// <returns>ID do documento criado</returns>
|
||||||
|
Task<string> SaveDocumentAsync(DocumentInput document);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza documento existente
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <param name="document">Novos dados do documento</param>
|
||||||
|
Task UpdateDocumentAsync(string id, DocumentInput document);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove documento
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
Task DeleteDocumentAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se documento existe
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <returns>True se existe, False caso contrário</returns>
|
||||||
|
Task<bool> DocumentExistsAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera documento por ID (formato moderno)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <returns>Documento ou null se não encontrado</returns>
|
||||||
|
Task<DocumentOutput?> GetDocumentAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista documentos por projeto (formato moderno)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
/// <returns>Lista de documentos do projeto</returns>
|
||||||
|
Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conta documentos
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="projectId">Filtrar por projeto (opcional)</param>
|
||||||
|
/// <returns>Número de documentos</returns>
|
||||||
|
Task<int> GetDocumentCountAsync(string? projectId = null);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// OPERAÇÕES EM LOTE
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salva múltiplos documentos de uma vez
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="documents">Lista de documentos</param>
|
||||||
|
/// <returns>Lista de IDs dos documentos criados</returns>
|
||||||
|
Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove múltiplos documentos de uma vez
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ids">Lista de IDs para remover</param>
|
||||||
|
Task DeleteDocumentsBatchAsync(List<string> ids);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// INFORMAÇÕES DO PROVIDER
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nome do provider (MongoDB, Qdrant, etc.)
|
||||||
|
/// </summary>
|
||||||
|
string ProviderName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estatísticas e métricas do provider
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Informações sobre performance, saúde, etc.</returns>
|
||||||
|
Task<Dictionary<string, object>> GetProviderStatsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Services/Contracts/IVectorDatabaseFactory.cs
Normal file
14
Services/Contracts/IVectorDatabaseFactory.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.Contracts
|
||||||
|
{
|
||||||
|
public interface IVectorDatabaseFactory
|
||||||
|
{
|
||||||
|
IVectorSearchService CreateVectorSearchService();
|
||||||
|
ITextDataService CreateTextDataService();
|
||||||
|
IResponseService CreateResponseService();
|
||||||
|
string GetActiveProvider();
|
||||||
|
VectorDatabaseSettings GetSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Services/Contracts/IVectorSearchService.cs
Normal file
138
Services/Contracts/IVectorSearchService.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
using ChatRAG.Models;
|
||||||
|
using Microsoft.Extensions.VectorData;
|
||||||
|
|
||||||
|
namespace ChatRAG.Contracts.VectorSearch
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface unificada para operações de busca vetorial.
|
||||||
|
/// Pode ser implementada por MongoDB, Qdrant, Pinecone, etc.
|
||||||
|
/// </summary>
|
||||||
|
public interface IVectorSearchService
|
||||||
|
{
|
||||||
|
// ========================================
|
||||||
|
// BUSCA VETORIAL
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca documentos similares usando embedding vetorial
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryEmbedding">Embedding da query (ex: 1536 dimensões para OpenAI)</param>
|
||||||
|
/// <param name="projectId">Filtrar por projeto específico (opcional)</param>
|
||||||
|
/// <param name="threshold">Score mínimo de similaridade (0.0 a 1.0)</param>
|
||||||
|
/// <param name="limit">Número máximo de resultados</param>
|
||||||
|
/// <param name="filters">Filtros adicionais (metadata, data, etc.)</param>
|
||||||
|
/// <returns>Lista de documentos ordenados por similaridade</returns>
|
||||||
|
Task<List<VectorSearchResult>> SearchSimilarAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string? projectId = null,
|
||||||
|
double threshold = 0.3,
|
||||||
|
int limit = 5,
|
||||||
|
Dictionary<string, object>? filters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca adaptativa - relaxa threshold se não encontrar resultados suficientes
|
||||||
|
/// (Implementa a mesma lógica do seu ResponseRAGService atual)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryEmbedding">Embedding da query</param>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
/// <param name="minThreshold">Threshold inicial (será reduzido se necessário)</param>
|
||||||
|
/// <param name="limit">Número máximo de resultados</param>
|
||||||
|
/// <returns>Lista de documentos com busca adaptativa</returns>
|
||||||
|
Task<List<VectorSearchResult>> SearchSimilarDynamicAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string projectId,
|
||||||
|
double minThreshold = 0.5,
|
||||||
|
int limit = 5);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CRUD DE DOCUMENTOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona um novo documento com embedding
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">Título do documento</param>
|
||||||
|
/// <param name="content">Conteúdo do documento</param>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
/// <param name="embedding">Embedding pré-calculado</param>
|
||||||
|
/// <param name="metadata">Metadados adicionais (tags, data, autor, etc.)</param>
|
||||||
|
/// <returns>ID do documento criado</returns>
|
||||||
|
Task<string> AddDocumentAsync(
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza um documento existente
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <param name="title">Novo título</param>
|
||||||
|
/// <param name="content">Novo conteúdo</param>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
/// <param name="embedding">Novo embedding</param>
|
||||||
|
/// <param name="metadata">Novos metadados</param>
|
||||||
|
Task UpdateDocumentAsync(
|
||||||
|
string id,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove um documento
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
Task DeleteDocumentAsync(string id);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CONSULTAS AUXILIARES
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se um documento existe
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <returns>True se existe, False caso contrário</returns>
|
||||||
|
Task<bool> DocumentExistsAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera um documento específico por ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID do documento</param>
|
||||||
|
/// <returns>Documento ou null se não encontrado</returns>
|
||||||
|
Task<VectorSearchResult?> GetDocumentAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lista todos os documentos de um projeto
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="projectId">ID do projeto</param>
|
||||||
|
/// <returns>Lista de documentos do projeto</returns>
|
||||||
|
Task<List<VectorSearchResult>> GetDocumentsByProjectAsync(string projectId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conta total de documentos
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="projectId">Filtrar por projeto (opcional)</param>
|
||||||
|
/// <returns>Número de documentos</returns>
|
||||||
|
Task<int> GetDocumentCountAsync(string? projectId = null);
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// HEALTH CHECK E MÉTRICAS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se o serviço está saudável
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True se está funcionando, False caso contrário</returns>
|
||||||
|
Task<bool> IsHealthyAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retorna estatísticas e métricas do provider
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Dicionário com estatísticas (documentos, performance, etc.)</returns>
|
||||||
|
Task<Dictionary<string, object>> GetStatsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
753
Services/PromptConfiguration/PromptConfigurationService.cs
Normal file
753
Services/PromptConfiguration/PromptConfigurationService.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
375
Services/ResponseService/0vwtdxh2.jyi~
Normal file
375
Services/ResponseService/0vwtdxh2.jyi~
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
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 HierarchicalRAGService : IResponseService
|
||||||
|
{
|
||||||
|
private readonly ChatHistoryService _chatHistoryService;
|
||||||
|
private readonly Kernel _kernel;
|
||||||
|
private readonly TextFilter _textFilter;
|
||||||
|
private readonly ProjectDataRepository _projectDataRepository;
|
||||||
|
private readonly IChatCompletionService _chatCompletionService;
|
||||||
|
private readonly IVectorSearchService _vectorSearchService;
|
||||||
|
private readonly ILogger<HierarchicalRAGService> _logger;
|
||||||
|
|
||||||
|
public HierarchicalRAGService(
|
||||||
|
ChatHistoryService chatHistoryService,
|
||||||
|
Kernel kernel,
|
||||||
|
TextFilter textFilter,
|
||||||
|
ProjectDataRepository projectDataRepository,
|
||||||
|
IChatCompletionService chatCompletionService,
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ILogger<HierarchicalRAGService> logger)
|
||||||
|
{
|
||||||
|
_chatHistoryService = chatHistoryService;
|
||||||
|
_kernel = kernel;
|
||||||
|
_textFilter = textFilter;
|
||||||
|
_projectDataRepository = projectDataRepository;
|
||||||
|
_chatCompletionService = chatCompletionService;
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> 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<QueryAnalysis> AnalyzeQuery(string question, string language)
|
||||||
|
{
|
||||||
|
var analysisPrompt = language == "pt" ?
|
||||||
|
@"Analise esta pergunta e classifique:
|
||||||
|
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
|
||||||
|
}}
|
||||||
|
|
||||||
|
REGRAS:
|
||||||
|
- overview: pergunta sobre projeto inteiro
|
||||||
|
- specific: pergunta sobre módulo/funcionalidade específica
|
||||||
|
- detailed: pergunta técnica que precisa de contexto profundo
|
||||||
|
- needs_hierarchy: true se precisar de múltiplas buscas" :
|
||||||
|
|
||||||
|
@"Analyze this question and classify:
|
||||||
|
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
|
||||||
|
}}
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- overview: question about entire project
|
||||||
|
- specific: question about specific module/functionality
|
||||||
|
- detailed: technical question needing deep context
|
||||||
|
- needs_hierarchy: true if needs multiple searches";
|
||||||
|
|
||||||
|
var prompt = string.Format(analysisPrompt, question);
|
||||||
|
|
||||||
|
var executionSettings = new OpenAIPromptExecutionSettings
|
||||||
|
{
|
||||||
|
Temperature = 0.1,
|
||||||
|
MaxTokens = 200
|
||||||
|
};
|
||||||
|
|
||||||
|
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 analysis = System.Text.Json.JsonSerializer.Deserialize<QueryAnalysis>(jsonResponse);
|
||||||
|
return analysis ?? new QueryAnalysis { Strategy = "specific", Complexity = "medium" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Erro ao parsear análise da query, usando padrão");
|
||||||
|
return new QueryAnalysis { Strategy = "specific", Complexity = "medium" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HierarchicalContext> ExecuteHierarchicalSearch(string question, string projectId, QueryAnalysis analysis)
|
||||||
|
{
|
||||||
|
var context = new HierarchicalContext();
|
||||||
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
||||||
|
|
||||||
|
switch (analysis.Strategy)
|
||||||
|
{
|
||||||
|
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 ExecuteOverviewStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
// Etapa 1: Buscar resumos/títulos primeiro
|
||||||
|
context.AddStep("Buscando visão geral do projeto");
|
||||||
|
var overviewResults = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
|
||||||
|
// Etapa 2: Identificar documentos principais baseado na pergunta
|
||||||
|
var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question));
|
||||||
|
var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
|
context.AddStep("Identificando documentos relevantes");
|
||||||
|
var relevantDocs = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 5);
|
||||||
|
|
||||||
|
context.CombinedContext = $"VISÃO GERAL DO PROJETO:\n{FormatResults(overviewResults.Take(3))}\n\nDOCUMENTOS RELEVANTES:\n{FormatResults(relevantDocs)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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<VectorSearchResult>();
|
||||||
|
|
||||||
|
if (analysis.Concepts?.Any() == true)
|
||||||
|
{
|
||||||
|
foreach (var concept in analysis.Concepts)
|
||||||
|
{
|
||||||
|
var conceptEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(concept));
|
||||||
|
var conceptArray = conceptEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var conceptResults = await _vectorSearchService.SearchSimilarAsync(conceptArray, projectId, 0.3, 2);
|
||||||
|
conceptualResults.AddRange(conceptResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<List<VectorSearchResult>> ExpandContext(List<VectorSearchResult> initialResults, string projectId, ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
var expandedResults = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
// 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<string> 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<string> 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.
|
||||||
|
|
||||||
|
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<VectorSearchResult> 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<string> 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<string>();
|
||||||
|
public bool Needs_Hierarchy { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HierarchicalContext
|
||||||
|
{
|
||||||
|
public List<string> Steps { get; set; } = new();
|
||||||
|
public string CombinedContext { get; set; } = "";
|
||||||
|
public Dictionary<string, object> 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.
|
||||||
385
Services/ResponseService/ConfidenceAwareRAGService.cs
Normal file
385
Services/ResponseService/ConfidenceAwareRAGService.cs
Normal file
@ -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<ConfidenceAwareRAGService> _logger;
|
||||||
|
private readonly ConfidenceVerifier _confidenceVerifier;
|
||||||
|
private readonly PromptConfigurationService _promptService;
|
||||||
|
private readonly ConfidenceAwareSettings _settings;
|
||||||
|
|
||||||
|
public ConfidenceAwareRAGService(
|
||||||
|
ChatHistoryService chatHistoryService,
|
||||||
|
Kernel kernel,
|
||||||
|
TextFilter textFilter,
|
||||||
|
IProjectDataRepository projectDataRepository,
|
||||||
|
IChatCompletionService chatCompletionService,
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ILogger<ConfidenceAwareRAGService> logger,
|
||||||
|
ConfidenceVerifier confidenceVerifier,
|
||||||
|
PromptConfigurationService promptService,
|
||||||
|
IOptions<ConfidenceAwareSettings> settings)
|
||||||
|
{
|
||||||
|
_chatHistoryService = chatHistoryService;
|
||||||
|
_kernel = kernel;
|
||||||
|
_textFilter = textFilter;
|
||||||
|
_projectDataRepository = projectDataRepository;
|
||||||
|
_chatCompletionService = chatCompletionService;
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_logger = logger;
|
||||||
|
_confidenceVerifier = confidenceVerifier;
|
||||||
|
_promptService = promptService;
|
||||||
|
_settings = settings.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt")
|
||||||
|
{
|
||||||
|
var stopWatch = new System.Diagnostics.Stopwatch();
|
||||||
|
stopWatch.Start();
|
||||||
|
string detectedLanguage = language;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
detectedLanguage = _settings.Languages.AutoDetectLanguage
|
||||||
|
? _promptService.DetectLanguage(question)
|
||||||
|
: language;
|
||||||
|
|
||||||
|
var projectData = await _projectDataRepository.GetAsync(projectId);
|
||||||
|
var detectedDomain = _promptService.DetectDomain(question, projectData?.Descricao);
|
||||||
|
var prompts = _promptService.GetPrompts(detectedDomain, detectedLanguage);
|
||||||
|
var queryAnalysis = await AnalyzeQuery(question, detectedLanguage, prompts.QueryAnalysis);
|
||||||
|
var context = await ExecuteHierarchicalSearch(question, projectId, queryAnalysis, prompts, detectedLanguage);
|
||||||
|
var confidenceResult = await VerifyConfidenceIfEnabled(queryAnalysis, context, projectId, detectedLanguage);
|
||||||
|
|
||||||
|
if (!confidenceResult.ShouldRespond)
|
||||||
|
{
|
||||||
|
stopWatch.Stop();
|
||||||
|
var fallbackResponse = confidenceResult.SuggestedResponse ?? GetGenericFallbackMessage(detectedLanguage);
|
||||||
|
return FormatFinalResponse(fallbackResponse, stopWatch.ElapsedMilliseconds, context.Steps.Count, confidenceResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await GenerateResponse(question, projectId, context, sessionId, detectedLanguage, prompts.Response, detectedDomain);
|
||||||
|
stopWatch.Stop();
|
||||||
|
return FormatFinalResponse(response, stopWatch.ElapsedMilliseconds, context.Steps.Count, confidenceResult);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro no ConfidenceAwareRAG");
|
||||||
|
stopWatch.Stop();
|
||||||
|
var errorMessage = detectedLanguage == "en" ? $"Error: {ex.Message}" : $"Erro: {ex.Message}";
|
||||||
|
return $"{errorMessage}\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetGenericFallbackMessage(string language)
|
||||||
|
{
|
||||||
|
return language == "en"
|
||||||
|
? "I don't have enough information to respond safely. Could you try rephrasing the question?"
|
||||||
|
: "Não tenho informações suficientes para responder com segurança. Pode tentar reformular a pergunta?";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<QueryAnalysis> AnalyzeQuery(string question, string language, string promptTemplate)
|
||||||
|
{
|
||||||
|
var prompt = string.Format(promptTemplate, question);
|
||||||
|
var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, new OpenAIPromptExecutionSettings { Temperature = 0.1, MaxTokens = 300 });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonResponse = response.Content?.Trim() ?? "{}";
|
||||||
|
var startIndex = jsonResponse.IndexOf('{');
|
||||||
|
var endIndex = jsonResponse.LastIndexOf('}');
|
||||||
|
if (startIndex >= 0 && endIndex >= startIndex)
|
||||||
|
jsonResponse = jsonResponse.Substring(startIndex, endIndex - startIndex + 1);
|
||||||
|
|
||||||
|
var analysis = JsonSerializer.Deserialize<QueryAnalysis>(jsonResponse, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
return analysis ?? new QueryAnalysis { Strategy = "specific", Complexity = "medium" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Erro ao parsear análise da query, usando padrão");
|
||||||
|
return new QueryAnalysis { Strategy = "specific", Complexity = "medium" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HierarchicalContext> ExecuteHierarchicalSearch(string question, string projectId, QueryAnalysis analysis, PromptTemplates prompts, string language)
|
||||||
|
{
|
||||||
|
var context = new HierarchicalContext();
|
||||||
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
||||||
|
context.Metadata["DetectedLanguage"] = language;
|
||||||
|
context.Metadata["SearchResults"] = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
switch (analysis.Strategy)
|
||||||
|
{
|
||||||
|
case "overview":
|
||||||
|
await ExecuteOverviewStrategy(context, question, projectId, embeddingService, prompts);
|
||||||
|
break;
|
||||||
|
case "detailed":
|
||||||
|
await ExecuteDetailedStrategy(context, question, projectId, embeddingService, analysis, prompts);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await ExecuteSpecificStrategy(context, question, projectId, embeddingService, prompts);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ConfidenceResult> VerifyConfidenceIfEnabled(QueryAnalysis analysis, HierarchicalContext context, string projectId, string language)
|
||||||
|
{
|
||||||
|
if (!_settings.EnableConfidenceCheck)
|
||||||
|
{
|
||||||
|
return new ConfidenceResult
|
||||||
|
{
|
||||||
|
ShouldRespond = true,
|
||||||
|
ConfidenceScore = 1.0,
|
||||||
|
Reason = language == "en" ? "Confidence check disabled" : "Verificação de confiança desabilitada"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = ExtractResultsFromContext(context, projectId);
|
||||||
|
return _confidenceVerifier.VerifyConfidence(analysis, results, context, _settings.UseStrictMode, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<VectorSearchResult> ExtractResultsFromContext(HierarchicalContext context, string projectId)
|
||||||
|
{
|
||||||
|
if (context.Metadata.ContainsKey("SearchResults") && context.Metadata["SearchResults"] is List<VectorSearchResult> storedResults)
|
||||||
|
return storedResults;
|
||||||
|
|
||||||
|
var results = new List<VectorSearchResult>();
|
||||||
|
var contextLength = context.CombinedContext?.Length ?? 0;
|
||||||
|
|
||||||
|
if (contextLength > 0)
|
||||||
|
{
|
||||||
|
var estimatedDocuments = Math.Max(1, Math.Min(10, contextLength / 500));
|
||||||
|
for (int i = 0; i < estimatedDocuments; i++)
|
||||||
|
{
|
||||||
|
results.Add(new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = $"estimated_doc_{i}",
|
||||||
|
Score = Math.Max(0.1, 0.8 - (i * 0.1)),
|
||||||
|
Content = "Conteúdo estimado do contexto",
|
||||||
|
Title = $"Documento estimado {i + 1}",
|
||||||
|
ProjectId = projectId ?? "unknown"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteOverviewStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, PromptTemplates prompts)
|
||||||
|
{
|
||||||
|
context.AddStep("Buscando todos os documentos do projeto");
|
||||||
|
var allProjectDocs = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
StoreSearchResults(context, allProjectDocs);
|
||||||
|
|
||||||
|
var requirementsDocs = allProjectDocs.Where(d => d.Title.ToLower().Contains("requisito") || d.Content.ToLower().Contains("requisito")).ToList();
|
||||||
|
var architectureDocs = allProjectDocs.Where(d => d.Title.ToLower().Contains("arquitetura") || d.Content.ToLower().Contains("arquitetura")).ToList();
|
||||||
|
var otherDocs = allProjectDocs.Except(requirementsDocs).Except(architectureDocs).ToList();
|
||||||
|
|
||||||
|
context.AddStep("Resumindo documentos por categoria");
|
||||||
|
var requirementsSummary = await SummarizeDocuments(requirementsDocs, "requisitos", prompts.Summary);
|
||||||
|
var architectureSummary = await SummarizeDocuments(architectureDocs, "arquitetura", prompts.Summary);
|
||||||
|
var otherSummary = await SummarizeDocuments(otherDocs, "outros documentos", prompts.Summary);
|
||||||
|
|
||||||
|
var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question));
|
||||||
|
var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var relevantDocs = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 8);
|
||||||
|
AddToSearchResults(context, relevantDocs);
|
||||||
|
|
||||||
|
var contextParts = new List<string>();
|
||||||
|
if (!string.IsNullOrEmpty(requirementsSummary)) contextParts.Add($"RESUMO DOS REQUISITOS:\n{requirementsSummary}");
|
||||||
|
if (!string.IsNullOrEmpty(architectureSummary)) contextParts.Add($"RESUMO DA ARQUITETURA:\n{architectureSummary}");
|
||||||
|
if (!string.IsNullOrEmpty(otherSummary)) contextParts.Add($"OUTROS DOCUMENTOS:\n{otherSummary}");
|
||||||
|
contextParts.Add($"DOCUMENTOS RELEVANTES:\n{FormatResults(relevantDocs)}");
|
||||||
|
context.CombinedContext = string.Join("\n\n", contextParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteSpecificStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, PromptTemplates prompts)
|
||||||
|
{
|
||||||
|
var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question));
|
||||||
|
var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var initialResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.4, 3);
|
||||||
|
StoreSearchResults(context, initialResults);
|
||||||
|
|
||||||
|
if (initialResults.Any())
|
||||||
|
{
|
||||||
|
var expandedContext = await ExpandContext(initialResults, projectId, embeddingService);
|
||||||
|
AddToSearchResults(context, expandedContext);
|
||||||
|
context.CombinedContext = $"CONTEXTO PRINCIPAL:\n{FormatResults(initialResults)}\n\nCONTEXTO EXPANDIDO:\n{FormatResults(expandedContext)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var fallbackResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.2, 5);
|
||||||
|
StoreSearchResults(context, fallbackResults);
|
||||||
|
context.CombinedContext = FormatResults(fallbackResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteDetailedStrategy(HierarchicalContext context, string question, string projectId, ITextEmbeddingGenerationService embeddingService, QueryAnalysis analysis, PromptTemplates prompts)
|
||||||
|
{
|
||||||
|
var questionEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(question));
|
||||||
|
var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var conceptualResults = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
if (analysis.Concepts?.Any() == true)
|
||||||
|
{
|
||||||
|
foreach (var concept in analysis.Concepts)
|
||||||
|
{
|
||||||
|
var conceptEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(concept));
|
||||||
|
var conceptArray = conceptEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var conceptResults = await _vectorSearchService.SearchSimilarAsync(conceptArray, projectId, 0.3, 2);
|
||||||
|
conceptualResults.AddRange(conceptResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var directResults = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.3, 3);
|
||||||
|
var allResults = conceptualResults.Concat(directResults).DistinctBy(r => r.Id).ToList();
|
||||||
|
StoreSearchResults(context, allResults);
|
||||||
|
|
||||||
|
var intermediateContext = FormatResults(allResults);
|
||||||
|
var gaps = await IdentifyKnowledgeGaps(question, intermediateContext, prompts.GapAnalysis);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(gaps))
|
||||||
|
{
|
||||||
|
var gapEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(gaps));
|
||||||
|
var gapArray = gapEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var gapResults = await _vectorSearchService.SearchSimilarAsync(gapArray, projectId, 0.25, 2);
|
||||||
|
AddToSearchResults(context, gapResults);
|
||||||
|
context.CombinedContext = $"CONTEXTO CONCEITUAL:\n{FormatResults(conceptualResults)}\n\nCONTEXTO DIRETO:\n{FormatResults(directResults)}\n\nCONTEXTO COMPLEMENTAR:\n{FormatResults(gapResults)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.CombinedContext = $"CONTEXTO CONCEITUAL:\n{FormatResults(conceptualResults)}\n\nCONTEXTO DIRETO:\n{FormatResults(directResults)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateResponse(string question, string projectId, HierarchicalContext context, string sessionId, string language, string promptTemplate, string? domain)
|
||||||
|
{
|
||||||
|
var projectData = await _projectDataRepository.GetAsync(projectId);
|
||||||
|
var project = $"Nome: {projectData.Nome}\nDescrição: {projectData.Descricao}";
|
||||||
|
if (!string.IsNullOrEmpty(domain)) project += $"\nDomínio: {domain}";
|
||||||
|
|
||||||
|
var finalPrompt = string.Format(promptTemplate, project, question, context.CombinedContext, string.Join(" → ", context.Steps));
|
||||||
|
var history = _chatHistoryService.GetSumarizer(sessionId);
|
||||||
|
history.AddUserMessage(finalPrompt);
|
||||||
|
|
||||||
|
var response = await _chatCompletionService.GetChatMessageContentAsync(history, new OpenAIPromptExecutionSettings { Temperature = 0.6 });
|
||||||
|
history.AddMessage(response.Role, response.Content ?? "");
|
||||||
|
_chatHistoryService.UpdateHistory(sessionId, history);
|
||||||
|
return response.Content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatFinalResponse(string response, long milliseconds, int steps, ConfidenceResult? confidence = null)
|
||||||
|
{
|
||||||
|
var result = response;
|
||||||
|
if (_settings.ShowDebugInfo)
|
||||||
|
{
|
||||||
|
result += $"\n\n📊 **Debug Info:**\n⏱️ Tempo: {milliseconds / 1000}s\n🔍 Etapas: {steps}";
|
||||||
|
if (confidence != null)
|
||||||
|
{
|
||||||
|
result += $"\n🎯 Confiança: {confidence.ConfidenceScore:P1}\n📋 Estratégia: {confidence.Strategy}";
|
||||||
|
result += $"\n✅ Deve responder: {(confidence.ShouldRespond ? "Sim" : "Não")}";
|
||||||
|
if (!string.IsNullOrEmpty(confidence.Reason)) result += $"\n💭 Motivo: {confidence.Reason}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatResults(IEnumerable<VectorSearchResult> results)
|
||||||
|
{
|
||||||
|
return string.Join("\n\n", results.Select((item, index) => $"=== DOCUMENTO {index + 1} ===\nRelevância: {item.Score:P1}\nConteúdo: {item.Content}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StoreSearchResults(HierarchicalContext context, List<VectorSearchResult> results)
|
||||||
|
{
|
||||||
|
if (!context.Metadata.ContainsKey("SearchResults")) context.Metadata["SearchResults"] = new List<VectorSearchResult>();
|
||||||
|
((List<VectorSearchResult>)context.Metadata["SearchResults"]).AddRange(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToSearchResults(HierarchicalContext context, List<VectorSearchResult> additionalResults)
|
||||||
|
{
|
||||||
|
if (context.Metadata.ContainsKey("SearchResults"))
|
||||||
|
{
|
||||||
|
var storedResults = (List<VectorSearchResult>)context.Metadata["SearchResults"];
|
||||||
|
storedResults.AddRange(additionalResults.Where(r => !storedResults.Any(sr => sr.Id == r.Id)));
|
||||||
|
}
|
||||||
|
else StoreSearchResults(context, additionalResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<VectorSearchResult>> ExpandContext(List<VectorSearchResult> initialResults, string projectId, ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
var expandedResults = new List<VectorSearchResult>();
|
||||||
|
foreach (var result in initialResults.Take(2))
|
||||||
|
{
|
||||||
|
var resultEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(result.Content));
|
||||||
|
var embeddingArray = resultEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var relatedDocs = await _vectorSearchService.SearchSimilarAsync(embeddingArray, projectId, 0.4, 2);
|
||||||
|
expandedResults.AddRange(relatedDocs.Where(r => !initialResults.Any(ir => ir.Id == r.Id)));
|
||||||
|
}
|
||||||
|
return expandedResults.DistinctBy(r => r.Id).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> SummarizeDocuments(List<VectorSearchResult> documents, string category, string promptTemplate)
|
||||||
|
{
|
||||||
|
if (!documents.Any()) return string.Empty;
|
||||||
|
if (documents.Count <= 3) return FormatResults(documents);
|
||||||
|
|
||||||
|
var chunks = documents.Chunk(5).ToList();
|
||||||
|
var tasks = chunks.Select(async chunk =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var prompt = string.Format(promptTemplate, category, FormatResults(chunk));
|
||||||
|
var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, new OpenAIPromptExecutionSettings { Temperature = 0.1, MaxTokens = 300 });
|
||||||
|
return response.Content ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch { return FormatResults(chunk); }
|
||||||
|
});
|
||||||
|
|
||||||
|
var summaries = await Task.WhenAll(tasks);
|
||||||
|
var validSummaries = summaries.Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||||
|
return validSummaries.Count > 1 ? string.Join("\n\n", validSummaries) : validSummaries.FirstOrDefault() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> IdentifyKnowledgeGaps(string question, string currentContext, string promptTemplate)
|
||||||
|
{
|
||||||
|
var prompt = string.Format(promptTemplate, question, currentContext.Substring(0, Math.Min(1000, currentContext.Length)));
|
||||||
|
var response = await _chatCompletionService.GetChatMessageContentAsync(prompt, new OpenAIPromptExecutionSettings { Temperature = 0.2, MaxTokens = 100 });
|
||||||
|
var gaps = response.Content?.Trim() ?? "";
|
||||||
|
return gaps.Equals("SUFICIENTE", StringComparison.OrdinalIgnoreCase) ? "" : gaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
|
||||||
|
{
|
||||||
|
return GetResponse(userData, projectId, sessionId, question, "pt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore SKEXP0001
|
||||||
572
Services/ResponseService/HierarchicalRAGService.cs
Normal file
572
Services/ResponseService/HierarchicalRAGService.cs
Normal file
@ -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<HierarchicalRAGService> _logger;
|
||||||
|
|
||||||
|
public HierarchicalRAGService(
|
||||||
|
ChatHistoryService chatHistoryService,
|
||||||
|
Kernel kernel,
|
||||||
|
TextFilter textFilter,
|
||||||
|
IProjectDataRepository projectDataRepository,
|
||||||
|
IChatCompletionService chatCompletionService,
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ILogger<HierarchicalRAGService> logger)
|
||||||
|
{
|
||||||
|
_chatHistoryService = chatHistoryService;
|
||||||
|
_kernel = kernel;
|
||||||
|
_textFilter = textFilter;
|
||||||
|
_projectDataRepository = projectDataRepository;
|
||||||
|
_chatCompletionService = chatCompletionService;
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> 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<QueryAnalysis> 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<QueryAnalysis>(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<HierarchicalContext> ExecuteHierarchicalSearch(string question, string projectId, QueryAnalysis analysis)
|
||||||
|
{
|
||||||
|
var context = new HierarchicalContext();
|
||||||
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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<string> SummarizeDocuments(List<VectorSearchResult> 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<Task<string>>();
|
||||||
|
|
||||||
|
// 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<VectorSearchResult>();
|
||||||
|
|
||||||
|
if (analysis.Concepts?.Any() == true)
|
||||||
|
{
|
||||||
|
foreach (var concept in analysis.Concepts)
|
||||||
|
{
|
||||||
|
var conceptEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(concept));
|
||||||
|
var conceptArray = conceptEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var conceptResults = await _vectorSearchService.SearchSimilarAsync(conceptArray, projectId, 0.3, 2);
|
||||||
|
conceptualResults.AddRange(conceptResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<List<VectorSearchResult>> ExpandContext(List<VectorSearchResult> initialResults, string projectId, ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
var expandedResults = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
// 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<string> 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<string> 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. 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.
|
||||||
|
|
||||||
|
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<VectorSearchResult> 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<string> 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<string>();
|
||||||
|
public bool Needs_Hierarchy { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HierarchicalContext
|
||||||
|
{
|
||||||
|
public List<string> Steps { get; set; } = new();
|
||||||
|
public string CombinedContext { get; set; } = "";
|
||||||
|
public Dictionary<string, object> 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.
|
||||||
118
Services/ResponseService/MongoResponseService.cs
Normal file
118
Services/ResponseService/MongoResponseService.cs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
using ChatApi.Models;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
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 MongoResponseService : IResponseService
|
||||||
|
{
|
||||||
|
private readonly ResponseRAGService _originalService; // Sua classe atual!
|
||||||
|
private readonly IVectorSearchService _vectorSearchService;
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
private readonly TextFilter _textFilter;
|
||||||
|
|
||||||
|
public MongoResponseService(
|
||||||
|
ResponseRAGService originalService,
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ITextEmbeddingGenerationService embeddingService,
|
||||||
|
TextFilter textFilter)
|
||||||
|
{
|
||||||
|
_originalService = originalService;
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
_textFilter = textFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProviderName => "MongoDB";
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODO ORIGINAL - Delega para ResponseRAGService
|
||||||
|
// ========================================
|
||||||
|
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language="pt")
|
||||||
|
{
|
||||||
|
return await _originalService.GetResponse(userData, projectId, sessionId, question);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODO ESTENDIDO COM MAIS DETALHES
|
||||||
|
// ========================================
|
||||||
|
public async Task<ResponseResult> GetResponseDetailed(
|
||||||
|
UserData userData,
|
||||||
|
string projectId,
|
||||||
|
string sessionId,
|
||||||
|
string question,
|
||||||
|
ResponseOptions? options = null)
|
||||||
|
{
|
||||||
|
options ??= new ResponseOptions();
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// Gera embedding da pergunta
|
||||||
|
var embeddingPergunta = await _embeddingService.GenerateEmbeddingAsync(
|
||||||
|
_textFilter.ToLowerAndWithoutAccents(question));
|
||||||
|
var embeddingArray = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
|
var searchStart = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
// Busca documentos similares usando a interface
|
||||||
|
var documentos = await _vectorSearchService.SearchSimilarDynamicAsync(
|
||||||
|
embeddingArray,
|
||||||
|
projectId,
|
||||||
|
options.SimilarityThreshold,
|
||||||
|
options.MaxContextDocuments);
|
||||||
|
|
||||||
|
var searchTime = stopwatch.ElapsedMilliseconds - searchStart;
|
||||||
|
var llmStart = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
// Chama o método original para gerar resposta
|
||||||
|
var response = await _originalService.GetResponse(userData, projectId, sessionId, question);
|
||||||
|
|
||||||
|
var llmTime = stopwatch.ElapsedMilliseconds - llmStart;
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
// Monta resultado detalhado
|
||||||
|
return new ResponseResult
|
||||||
|
{
|
||||||
|
Content = response,
|
||||||
|
Provider = "MongoDB",
|
||||||
|
Sources = documentos.Select(d => new SourceDocument
|
||||||
|
{
|
||||||
|
Id = d.Id,
|
||||||
|
Title = d.Title,
|
||||||
|
Content = d.Content,
|
||||||
|
Similarity = d.Score,
|
||||||
|
Metadata = d.Metadata
|
||||||
|
}).ToList(),
|
||||||
|
Metrics = new ResponseMetrics
|
||||||
|
{
|
||||||
|
TotalTimeMs = stopwatch.ElapsedMilliseconds,
|
||||||
|
SearchTimeMs = searchTime,
|
||||||
|
LlmTimeMs = llmTime,
|
||||||
|
DocumentsFound = documentos.Count,
|
||||||
|
DocumentsUsed = documentos.Count,
|
||||||
|
AverageSimilarity = documentos.Any() ? documentos.Average(d => d.Score) : 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ResponseStats> GetStatsAsync()
|
||||||
|
{
|
||||||
|
// Implementação básica - pode ser expandida
|
||||||
|
return new ResponseStats
|
||||||
|
{
|
||||||
|
TotalRequests = 0,
|
||||||
|
AverageResponseTime = 0,
|
||||||
|
RequestsByProject = new Dictionary<string, int>(),
|
||||||
|
LastRequest = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
|
||||||
|
{
|
||||||
|
return this.GetResponse(userData, projectId, sessionId, question, "pt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#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.
|
||||||
212
Services/ResponseService/QdrantResponseService.cs
Normal file
212
Services/ResponseService/QdrantResponseService.cs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
#pragma warning disable SKEXP0001
|
||||||
|
|
||||||
|
using ChatApi.Models;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using Microsoft.SemanticKernel;
|
||||||
|
using Microsoft.SemanticKernel.ChatCompletion;
|
||||||
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.ResponseService
|
||||||
|
{
|
||||||
|
public class QdrantResponseService : IResponseService
|
||||||
|
{
|
||||||
|
private readonly IVectorSearchService _vectorSearchService;
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
private readonly IChatCompletionService _chatService;
|
||||||
|
private readonly ILogger<QdrantResponseService> _logger;
|
||||||
|
|
||||||
|
public QdrantResponseService(
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ITextEmbeddingGenerationService embeddingService,
|
||||||
|
IChatCompletionService chatService,
|
||||||
|
ILogger<QdrantResponseService> logger)
|
||||||
|
{
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
_chatService = chatService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProviderName => "Qdrant";
|
||||||
|
|
||||||
|
public async Task<string> GetResponse(
|
||||||
|
UserData userData,
|
||||||
|
string projectId,
|
||||||
|
string sessionId,
|
||||||
|
string userMessage,
|
||||||
|
string language = "pt")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Processando consulta RAG com Qdrant para projeto {ProjectId}", projectId);
|
||||||
|
|
||||||
|
// 1. Gerar embedding da pergunta do usuário
|
||||||
|
var questionEmbedding = await _embeddingService.GenerateEmbeddingAsync(userMessage);
|
||||||
|
var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
|
// 2. Buscar documentos similares no Qdrant
|
||||||
|
var searchResults = await _vectorSearchService.SearchSimilarDynamicAsync(
|
||||||
|
queryEmbedding: embeddingArray,
|
||||||
|
projectId: projectId,
|
||||||
|
minThreshold: 0.5,
|
||||||
|
limit: 5
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Construir contexto a partir dos resultados
|
||||||
|
var context = BuildContextFromResults(searchResults);
|
||||||
|
|
||||||
|
// 4. Criar prompt com contexto
|
||||||
|
var prompt = BuildRagPrompt(userMessage, context);
|
||||||
|
|
||||||
|
// 5. Gerar resposta usando LLM
|
||||||
|
var response = await _chatService.GetChatMessageContentAsync(prompt);
|
||||||
|
|
||||||
|
_logger.LogDebug("Resposta RAG gerada com {ResultCount} documentos do Qdrant",
|
||||||
|
searchResults.Count);
|
||||||
|
|
||||||
|
return response.Content ?? "Desculpe, não foi possível gerar uma resposta.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar consulta RAG com Qdrant");
|
||||||
|
return "Ocorreu um erro ao processar sua consulta. Tente novamente.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetResponseWithHistory(
|
||||||
|
UserData userData,
|
||||||
|
string projectId,
|
||||||
|
string sessionId,
|
||||||
|
string userMessage,
|
||||||
|
List<string> conversationHistory)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Combina histórico com mensagem atual para melhor contexto
|
||||||
|
var enhancedMessage = BuildEnhancedMessageWithHistory(userMessage, conversationHistory);
|
||||||
|
|
||||||
|
return await GetResponse(userData, projectId, sessionId, enhancedMessage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar consulta RAG com histórico");
|
||||||
|
return "Ocorreu um erro ao processar sua consulta. Tente novamente.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS AUXILIARES PRIVADOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
private string BuildContextFromResults(List<VectorSearchResult> results)
|
||||||
|
{
|
||||||
|
if (!results.Any())
|
||||||
|
{
|
||||||
|
return "Nenhum documento relevante encontrado.";
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextBuilder = new System.Text.StringBuilder();
|
||||||
|
contextBuilder.AppendLine("=== CONTEXTO DOS DOCUMENTOS ===");
|
||||||
|
|
||||||
|
foreach (var result in results.Take(5)) // Limita a 5 documentos
|
||||||
|
{
|
||||||
|
contextBuilder.AppendLine($"\n--- Documento: {result.Title} (Relevância: {result.GetScorePercentage()}) ---");
|
||||||
|
contextBuilder.AppendLine(result.Content);
|
||||||
|
contextBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildRagPrompt(string userQuestion, string context)
|
||||||
|
{
|
||||||
|
return $@"
|
||||||
|
Você é um assistente especializado que responde perguntas baseado nos documentos fornecidos.
|
||||||
|
|
||||||
|
CONTEXTO DOS DOCUMENTOS:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
PERGUNTA DO USUÁRIO:
|
||||||
|
{userQuestion}
|
||||||
|
|
||||||
|
INSTRUÇÕES:
|
||||||
|
- Responda baseado APENAS nas informações dos documentos fornecidos
|
||||||
|
- Se a informação não estiver nos documentos, diga que não encontrou a informação
|
||||||
|
- Seja preciso e cite trechos relevantes quando possível
|
||||||
|
- Mantenha um tom profissional e prestativo
|
||||||
|
- Se houver múltiplas informações relevantes, organize-as de forma clara
|
||||||
|
|
||||||
|
RESPOSTA:
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildEnhancedMessageWithHistory(string currentMessage, List<string> history)
|
||||||
|
{
|
||||||
|
if (!history.Any())
|
||||||
|
return currentMessage;
|
||||||
|
|
||||||
|
var enhancedMessage = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
enhancedMessage.AppendLine("HISTÓRICO DA CONVERSA:");
|
||||||
|
foreach (var message in history.TakeLast(3)) // Últimas 3 mensagens para contexto
|
||||||
|
{
|
||||||
|
enhancedMessage.AppendLine($"- {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
enhancedMessage.AppendLine($"\nPERGUNTA ATUAL: {currentMessage}");
|
||||||
|
|
||||||
|
return enhancedMessage.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS DE ESTATÍSTICAS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vectorStats = await _vectorSearchService.GetStatsAsync();
|
||||||
|
|
||||||
|
return new Dictionary<string, object>(vectorStats)
|
||||||
|
{
|
||||||
|
["response_service_provider"] = "Qdrant",
|
||||||
|
["supports_history"] = true,
|
||||||
|
["supports_dynamic_threshold"] = true,
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["response_service_provider"] = "Qdrant",
|
||||||
|
["health"] = "error",
|
||||||
|
["error"] = ex.Message,
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsHealthyAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vectorSearchService.IsHealthyAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
|
||||||
|
{
|
||||||
|
return this.GetResponse(userData, projectId, sessionId, question, "pt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore SKEXP0001
|
||||||
@ -1,10 +1,13 @@
|
|||||||
|
|
||||||
using ChatApi;
|
using ChatApi;
|
||||||
using ChatApi.Models;
|
using ChatApi.Models;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
using ChatRAG.Data;
|
using ChatRAG.Data;
|
||||||
using ChatRAG.Models;
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
using Microsoft.SemanticKernel;
|
using Microsoft.SemanticKernel;
|
||||||
using Microsoft.SemanticKernel.ChatCompletion;
|
using Microsoft.SemanticKernel.ChatCompletion;
|
||||||
|
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||||
using Microsoft.SemanticKernel.Embeddings;
|
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.
|
#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.
|
||||||
@ -17,16 +20,19 @@ namespace ChatRAG.Services.ResponseService
|
|||||||
private readonly Kernel _kernel;
|
private readonly Kernel _kernel;
|
||||||
private readonly TextFilter _textFilter;
|
private readonly TextFilter _textFilter;
|
||||||
private readonly TextDataRepository _textDataRepository;
|
private readonly TextDataRepository _textDataRepository;
|
||||||
private readonly ProjectDataRepository _projectDataRepository;
|
private readonly IProjectDataRepository _projectDataRepository;
|
||||||
private readonly IChatCompletionService _chatCompletionService;
|
private readonly IChatCompletionService _chatCompletionService;
|
||||||
|
private readonly IVectorSearchService _vectorSearchService;
|
||||||
|
|
||||||
public ResponseRAGService(
|
public ResponseRAGService(
|
||||||
ChatHistoryService chatHistoryService,
|
ChatHistoryService chatHistoryService,
|
||||||
Kernel kernel,
|
Kernel kernel,
|
||||||
TextFilter textFilter,
|
TextFilter textFilter,
|
||||||
TextDataRepository textDataRepository,
|
TextDataRepository textDataRepository,
|
||||||
ProjectDataRepository projectDataRepository,
|
IProjectDataRepository projectDataRepository,
|
||||||
IChatCompletionService chatCompletionService)
|
IChatCompletionService chatCompletionService,
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ITextDataService textDataService)
|
||||||
{
|
{
|
||||||
this._chatHistoryService = chatHistoryService;
|
this._chatHistoryService = chatHistoryService;
|
||||||
this._kernel = kernel;
|
this._kernel = kernel;
|
||||||
@ -34,27 +40,71 @@ namespace ChatRAG.Services.ResponseService
|
|||||||
this._textDataRepository = textDataRepository;
|
this._textDataRepository = textDataRepository;
|
||||||
this._projectDataRepository = projectDataRepository;
|
this._projectDataRepository = projectDataRepository;
|
||||||
this._chatCompletionService = chatCompletionService;
|
this._chatCompletionService = chatCompletionService;
|
||||||
|
this._vectorSearchService = vectorSearchService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
|
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt")
|
||||||
{
|
{
|
||||||
var stopWatch = new System.Diagnostics.Stopwatch();
|
var stopWatch = new System.Diagnostics.Stopwatch();
|
||||||
stopWatch.Start();
|
stopWatch.Start();
|
||||||
|
|
||||||
//var resposta = await BuscarTextoRelacionado(question);
|
var searchStrategy = await ClassificarEstrategiaDeBusca(question, language);
|
||||||
//var resposta = await BuscarTopTextosRelacionados(question, projectId);
|
|
||||||
var resposta = await BuscarTopTextosRelacionadosDinamico(question, projectId);
|
|
||||||
|
|
||||||
var projectData = (await _projectDataRepository.GetAsync()).FirstOrDefault();
|
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}";
|
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}\"";
|
//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);
|
ChatHistory history = _chatHistoryService.GetSumarizer(sessionId);
|
||||||
|
|
||||||
history.AddUserMessage(question);
|
history.AddUserMessage(question);
|
||||||
|
|
||||||
var response = await _chatCompletionService.GetChatMessageContentAsync(history);
|
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 ?? "");
|
history.AddMessage(response.Role, response.Content ?? "");
|
||||||
|
|
||||||
_chatHistoryService.UpdateHistory(sessionId, history);
|
_chatHistoryService.UpdateHistory(sessionId, history);
|
||||||
@ -64,6 +114,68 @@ namespace ChatRAG.Services.ResponseService
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
async Task<string> BuscarTextoRelacionado(string pergunta)
|
||||||
{
|
{
|
||||||
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
||||||
@ -88,7 +200,49 @@ namespace ChatRAG.Services.ResponseService
|
|||||||
return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada.";
|
return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adicione esta nova rotina no seu ResponseRAGService
|
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)
|
async Task<string> BuscarTopTextosRelacionadosDinamico(string pergunta, string projectId, int size = 3)
|
||||||
{
|
{
|
||||||
@ -221,6 +375,18 @@ namespace ChatRAG.Services.ResponseService
|
|||||||
}
|
}
|
||||||
return dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB));
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
572
Services/ResponseService/dxsmsw5a.kio~
Normal file
572
Services/ResponseService/dxsmsw5a.kio~
Normal file
@ -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<HierarchicalRAGService> _logger;
|
||||||
|
|
||||||
|
public HierarchicalRAGService(
|
||||||
|
ChatHistoryService chatHistoryService,
|
||||||
|
Kernel kernel,
|
||||||
|
TextFilter textFilter,
|
||||||
|
IProjectDataRepository projectDataRepository,
|
||||||
|
IChatCompletionService chatCompletionService,
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ILogger<HierarchicalRAGService> logger)
|
||||||
|
{
|
||||||
|
_chatHistoryService = chatHistoryService;
|
||||||
|
_kernel = kernel;
|
||||||
|
_textFilter = textFilter;
|
||||||
|
_projectDataRepository = projectDataRepository;
|
||||||
|
_chatCompletionService = chatCompletionService;
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> 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<QueryAnalysis> 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<QueryAnalysis>(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<HierarchicalContext> ExecuteHierarchicalSearch(string question, string projectId, QueryAnalysis analysis)
|
||||||
|
{
|
||||||
|
var context = new HierarchicalContext();
|
||||||
|
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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<string> SummarizeDocuments(List<VectorSearchResult> 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<Task<string>>();
|
||||||
|
|
||||||
|
// 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<VectorSearchResult>();
|
||||||
|
|
||||||
|
if (analysis.Concepts?.Any() == true)
|
||||||
|
{
|
||||||
|
foreach (var concept in analysis.Concepts)
|
||||||
|
{
|
||||||
|
var conceptEmbedding = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(concept));
|
||||||
|
var conceptArray = conceptEmbedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
var conceptResults = await _vectorSearchService.SearchSimilarAsync(conceptArray, projectId, 0.3, 2);
|
||||||
|
conceptualResults.AddRange(conceptResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<List<VectorSearchResult>> ExpandContext(List<VectorSearchResult> initialResults, string projectId, ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
var expandedResults = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
// 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<string> 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<string> 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<VectorSearchResult> 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<string> 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<string>();
|
||||||
|
public bool Needs_Hierarchy { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HierarchicalContext
|
||||||
|
{
|
||||||
|
public List<string> Steps { get; set; } = new();
|
||||||
|
public string CombinedContext { get; set; } = "";
|
||||||
|
public Dictionary<string, object> 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.
|
||||||
651
Services/SearchVectors/ChromaVectorSearchService.cs
Normal file
651
Services/SearchVectors/ChromaVectorSearchService.cs
Normal file
@ -0,0 +1,651 @@
|
|||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.SearchVectors
|
||||||
|
{
|
||||||
|
public class ChromaVectorSearchService : IVectorSearchService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<ChromaVectorSearchService> _logger;
|
||||||
|
private readonly ChromaSettings _settings;
|
||||||
|
private readonly string _collectionName;
|
||||||
|
|
||||||
|
public ChromaVectorSearchService(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
ILogger<ChromaVectorSearchService> logger,
|
||||||
|
HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_settings = settings.Value.Chroma ?? throw new ArgumentNullException("Chroma settings not configured");
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_httpClient.BaseAddress = new Uri($"http://{_settings.Host}:{_settings.Port}");
|
||||||
|
_collectionName = _settings.CollectionName;
|
||||||
|
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Verificar se a collection existe, se não, criar
|
||||||
|
var collections = await GetCollectionsAsync();
|
||||||
|
if (!collections.Contains(_collectionName))
|
||||||
|
{
|
||||||
|
await CreateCollectionAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao inicializar Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// BUSCA VETORIAL
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> SearchSimilarAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string? projectId = null,
|
||||||
|
double threshold = 0.3,
|
||||||
|
int limit = 5,
|
||||||
|
Dictionary<string, object>? filters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Construir filtros WHERE
|
||||||
|
var whereClause = BuildWhereClause(projectId, filters);
|
||||||
|
|
||||||
|
var query = new
|
||||||
|
{
|
||||||
|
query_embeddings = new[] { queryEmbedding },
|
||||||
|
n_results = limit,
|
||||||
|
where = whereClause,
|
||||||
|
include = new[] { "documents", "metadatas", "distances" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(query);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/query", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Erro na busca Chroma: {Error}", error);
|
||||||
|
return new List<VectorSearchResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
|
var queryResult = JsonSerializer.Deserialize<ChromaQueryResult>(result);
|
||||||
|
|
||||||
|
return ParseQueryResults(queryResult, threshold);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar similares no Chroma");
|
||||||
|
return new List<VectorSearchResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> SearchSimilarDynamicAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string projectId,
|
||||||
|
double minThreshold = 0.5,
|
||||||
|
int limit = 5)
|
||||||
|
{
|
||||||
|
// Estratégia 1: Busca com threshold alto
|
||||||
|
var results = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold, limit);
|
||||||
|
|
||||||
|
if (results.Count >= limit)
|
||||||
|
{
|
||||||
|
return results.Take(limit).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estratégia 2: Relaxar threshold se não conseguiu o suficiente
|
||||||
|
if (results.Count < limit && minThreshold > 0.35)
|
||||||
|
{
|
||||||
|
var mediumResults = await SearchSimilarAsync(queryEmbedding, projectId, 0.35, limit * 2);
|
||||||
|
if (mediumResults.Count >= limit)
|
||||||
|
{
|
||||||
|
return mediumResults.Take(limit).ToList();
|
||||||
|
}
|
||||||
|
results = mediumResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estratégia 3: Threshold baixo como último recurso
|
||||||
|
if (results.Count < limit && minThreshold > 0.2)
|
||||||
|
{
|
||||||
|
var lowResults = await SearchSimilarAsync(queryEmbedding, projectId, 0.2, limit * 3);
|
||||||
|
results = lowResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.Take(limit).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CRUD DE DOCUMENTOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<string> AddDocumentAsync(
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var documentId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
var combinedMetadata = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["title"] = title,
|
||||||
|
["project_id"] = projectId,
|
||||||
|
["created_at"] = DateTime.UtcNow.ToString("O")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in metadata)
|
||||||
|
{
|
||||||
|
combinedMetadata[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = new
|
||||||
|
{
|
||||||
|
ids = new[] { documentId },
|
||||||
|
documents = new[] { content },
|
||||||
|
metadatas = new[] { combinedMetadata },
|
||||||
|
embeddings = new[] { embedding }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(document);
|
||||||
|
var requestContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/add", requestContent);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"Erro ao adicionar documento: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentId;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao adicionar documento no Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(
|
||||||
|
string id,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Chroma não tem update direto, então fazemos delete + add
|
||||||
|
await DeleteDocumentAsync(id);
|
||||||
|
|
||||||
|
var combinedMetadata = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["title"] = title,
|
||||||
|
["project_id"] = projectId,
|
||||||
|
["updated_at"] = DateTime.UtcNow.ToString("O")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadata != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in metadata)
|
||||||
|
{
|
||||||
|
combinedMetadata[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = new
|
||||||
|
{
|
||||||
|
ids = new[] { id },
|
||||||
|
documents = new[] { content },
|
||||||
|
metadatas = new[] { combinedMetadata },
|
||||||
|
embeddings = new[] { embedding }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(document);
|
||||||
|
var requestContent = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/upsert", requestContent);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"Erro ao atualizar documento: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar documento no Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deleteRequest = new
|
||||||
|
{
|
||||||
|
ids = new[] { id }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(deleteRequest);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/delete", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Erro ao deletar documento {Id}: {Error}", id, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao deletar documento {Id} no Chroma", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// CONSULTAS AUXILIARES
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var doc = await GetDocumentAsync(id);
|
||||||
|
return doc != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VectorSearchResult?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = new
|
||||||
|
{
|
||||||
|
ids = new[] { id },
|
||||||
|
include = new[] { "documents", "metadatas" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(query);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/get", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
|
var getResult = JsonSerializer.Deserialize<ChromaGetResult>(result);
|
||||||
|
|
||||||
|
if (getResult?.ids?.Length > 0)
|
||||||
|
{
|
||||||
|
return new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = getResult.ids[0],
|
||||||
|
Content = getResult.documents?[0] ?? "",
|
||||||
|
Score = 1.0,
|
||||||
|
Metadata = getResult.metadatas?[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar documento {Id} no Chroma", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = new
|
||||||
|
{
|
||||||
|
where = new { project_id = projectId },
|
||||||
|
include = new[] { "documents", "metadatas" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(query);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/get", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Erro ao buscar documentos do projeto {ProjectId}: {Error}", projectId, error);
|
||||||
|
return new List<VectorSearchResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
|
var getResult = JsonSerializer.Deserialize<ChromaGetResult>(result);
|
||||||
|
|
||||||
|
var results = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
if (getResult?.documents?.Length > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < getResult.documents.Length; i++)
|
||||||
|
{
|
||||||
|
results.Add(new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = getResult.ids[i],
|
||||||
|
Content = getResult.documents[i],
|
||||||
|
Score = 1.0, // Todos os documentos do projeto
|
||||||
|
Metadata = getResult.metadatas?[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar documentos do projeto {ProjectId} no Chroma", projectId);
|
||||||
|
return new List<VectorSearchResult>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = new
|
||||||
|
{
|
||||||
|
where = projectId != null ? new { project_id = projectId } : null
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(query);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsync($"/api/v1/collections/{_collectionName}/count", content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Erro ao contar documentos no Chroma");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
|
var countResult = JsonSerializer.Deserialize<ChromaCountResult>(result);
|
||||||
|
return countResult?.count ?? 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao contar documentos no Chroma");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// HEALTH CHECK E MÉTRICAS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<bool> IsHealthyAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync("/api/v1/heartbeat");
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro no health check do Chroma");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stats = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "Chroma",
|
||||||
|
["collection"] = _collectionName,
|
||||||
|
["host"] = _settings.Host,
|
||||||
|
["port"] = _settings.Port
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tentar obter informações da collection
|
||||||
|
var response = await _httpClient.GetAsync($"/api/v1/collections/{_collectionName}");
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var collectionInfo = JsonSerializer.Deserialize<Dictionary<string, object>>(content);
|
||||||
|
if (collectionInfo != null)
|
||||||
|
{
|
||||||
|
stats["collection_info"] = collectionInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contar documentos totais
|
||||||
|
stats["total_documents"] = await GetDocumentCountAsync();
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao obter stats do Chroma");
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "Chroma",
|
||||||
|
["error"] = ex.Message,
|
||||||
|
["status"] = "error"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS AUXILIARES PRIVADOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
private async Task<string[]> GetCollectionsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync("/api/v1/collections");
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Erro ao obter collections: {StatusCode}", response.StatusCode);
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
// Tentar desserializar como array de strings (versão simples)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var collections = JsonSerializer.Deserialize<string[]>(content);
|
||||||
|
return collections ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Tentar desserializar como array de objetos (versão mais nova)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var collectionsObj = JsonSerializer.Deserialize<CollectionInfo[]>(content);
|
||||||
|
return collectionsObj?.Select(c => c.name).ToArray() ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Não foi possível parsear lista de collections");
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar collections");
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classe auxiliar para desserialização
|
||||||
|
private class CollectionInfo
|
||||||
|
{
|
||||||
|
public string name { get; set; } = "";
|
||||||
|
public Dictionary<string, object>? metadata { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateCollectionAsync()
|
||||||
|
{
|
||||||
|
var collection = new
|
||||||
|
{
|
||||||
|
name = _collectionName,
|
||||||
|
metadata = new
|
||||||
|
{
|
||||||
|
description = "RAG Collection",
|
||||||
|
created_at = DateTime.UtcNow.ToString("O")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(collection);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
// Tentar primeira abordagem (versão mais nova)
|
||||||
|
var response = await _httpClient.PostAsync("/api/v1/collections", content);
|
||||||
|
|
||||||
|
// Se falhar, tentar segunda abordagem (criar collection via get_or_create)
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Método POST falhou, tentando abordagem alternativa");
|
||||||
|
|
||||||
|
// Criar usando get_or_create approach
|
||||||
|
var createPayload = new
|
||||||
|
{
|
||||||
|
name = _collectionName,
|
||||||
|
metadata = new
|
||||||
|
{
|
||||||
|
description = "RAG Collection",
|
||||||
|
created_at = DateTime.UtcNow.ToString("O")
|
||||||
|
},
|
||||||
|
get_or_create = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var createJson = JsonSerializer.Serialize(createPayload);
|
||||||
|
var createContent = new StringContent(createJson, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var createResponse = await _httpClient.PostAsync("/api/v1/collections", createContent);
|
||||||
|
|
||||||
|
if (!createResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await createResponse.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("Erro ao criar collection: {Error}", error);
|
||||||
|
|
||||||
|
// Última tentativa: assumir que collection já existe
|
||||||
|
_logger.LogWarning("Assumindo que collection {CollectionName} já existe", _collectionName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection {CollectionName} criada/verificada com sucesso", _collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? BuildWhereClause(string? projectId, Dictionary<string, object>? filters)
|
||||||
|
{
|
||||||
|
var where = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
where["project_id"] = projectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters != null)
|
||||||
|
{
|
||||||
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
where[filter.Key] = filter.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return where.Any() ? where : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<VectorSearchResult> ParseQueryResults(ChromaQueryResult? queryResult, double threshold)
|
||||||
|
{
|
||||||
|
var results = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
if (queryResult?.documents?.Length > 0 && queryResult.documents[0].Length > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < queryResult.documents[0].Length; i++)
|
||||||
|
{
|
||||||
|
var distance = queryResult.distances?[0][i] ?? 1.0;
|
||||||
|
|
||||||
|
// Chroma retorna distâncias, converter para similaridade (1 - distance)
|
||||||
|
var similarity = 1.0 - distance;
|
||||||
|
|
||||||
|
if (similarity >= threshold)
|
||||||
|
{
|
||||||
|
results.Add(new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = queryResult.ids[0][i],
|
||||||
|
Content = queryResult.documents[0][i],
|
||||||
|
Score = similarity,
|
||||||
|
Metadata = queryResult.metadatas?[0][i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.OrderByDescending(r => r.Score).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// DTOs PARA CHROMA API
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public class ChromaQueryResult
|
||||||
|
{
|
||||||
|
public string[][] ids { get; set; } = Array.Empty<string[]>();
|
||||||
|
public string[][] documents { get; set; } = Array.Empty<string[]>();
|
||||||
|
public double[][]? distances { get; set; }
|
||||||
|
public Dictionary<string, object>[][]? metadatas { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaGetResult
|
||||||
|
{
|
||||||
|
public string[] ids { get; set; } = Array.Empty<string>();
|
||||||
|
public string[] documents { get; set; } = Array.Empty<string>();
|
||||||
|
public Dictionary<string, object>[]? metadatas { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaCountResult
|
||||||
|
{
|
||||||
|
public int count { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
228
Services/SearchVectors/MongoVectorSearchService.cs
Normal file
228
Services/SearchVectors/MongoVectorSearchService.cs
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using Microsoft.Extensions.VectorData;
|
||||||
|
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.SearchVectors
|
||||||
|
{
|
||||||
|
public class MongoVectorSearchService : IVectorSearchService
|
||||||
|
{
|
||||||
|
private readonly TextDataRepository _textDataRepository;
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
|
||||||
|
public MongoVectorSearchService(
|
||||||
|
TextDataRepository textDataRepository,
|
||||||
|
ITextEmbeddingGenerationService embeddingService)
|
||||||
|
{
|
||||||
|
_textDataRepository = textDataRepository;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... resto da implementação permanece igual ...
|
||||||
|
// (copiar do código anterior)
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> SearchSimilarAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string? projectId = null,
|
||||||
|
double threshold = 0.3,
|
||||||
|
int limit = 5,
|
||||||
|
Dictionary<string, object>? filters = null)
|
||||||
|
{
|
||||||
|
List<TextoComEmbedding> textos = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
textos = string.IsNullOrEmpty(projectId)
|
||||||
|
? await _textDataRepository.GetAsync()
|
||||||
|
: await _textDataRepository.GetByProjectIdAsync(projectId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Erro ao buscar documentos: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultados = textos
|
||||||
|
.Select(texto => new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = texto.Id,
|
||||||
|
Title = texto.Titulo,
|
||||||
|
Content = texto.Conteudo,
|
||||||
|
ProjectId = texto.ProjetoId,
|
||||||
|
Score = CalcularSimilaridadeCoseno(queryEmbedding, texto.Embedding),
|
||||||
|
Embedding = texto.Embedding,
|
||||||
|
Provider = "MongoDB",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
})
|
||||||
|
.Where(r => r.Score >= threshold)
|
||||||
|
.OrderByDescending(r => r.Score)
|
||||||
|
.Take(limit)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return resultados;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> SearchSimilarDynamicAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string projectId,
|
||||||
|
double minThreshold = 0.5,
|
||||||
|
int limit = 5)
|
||||||
|
{
|
||||||
|
var resultados = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold, limit);
|
||||||
|
|
||||||
|
if (resultados.Count < 3)
|
||||||
|
{
|
||||||
|
resultados = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold * 0.7, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultados.Count < 3)
|
||||||
|
{
|
||||||
|
resultados = await SearchSimilarAsync(queryEmbedding, projectId, 0.2, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultados.Take(limit).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> AddDocumentAsync(
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
var documento = new TextoComEmbedding
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Titulo = title,
|
||||||
|
Conteudo = content,
|
||||||
|
ProjetoId = projectId,
|
||||||
|
Embedding = embedding
|
||||||
|
};
|
||||||
|
|
||||||
|
await _textDataRepository.CreateAsync(documento);
|
||||||
|
return documento.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(
|
||||||
|
string id,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
var documento = new TextoComEmbedding
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Titulo = title,
|
||||||
|
Conteudo = content,
|
||||||
|
ProjetoId = projectId,
|
||||||
|
Embedding = embedding
|
||||||
|
};
|
||||||
|
|
||||||
|
await _textDataRepository.UpdateAsync(id, documento);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
await _textDataRepository.RemoveAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
var doc = await _textDataRepository.GetAsync(id);
|
||||||
|
return doc != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VectorSearchResult?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
var doc = await _textDataRepository.GetAsync(id);
|
||||||
|
if (doc == null) return null;
|
||||||
|
|
||||||
|
return new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
Score = 1.0,
|
||||||
|
Embedding = doc.Embedding,
|
||||||
|
Provider = "MongoDB",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
var docs = await _textDataRepository.GetByProjectIdAsync(projectId);
|
||||||
|
return docs.Select(doc => new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
Score = 1.0,
|
||||||
|
Embedding = doc.Embedding,
|
||||||
|
Provider = "MongoDB",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
var all = await _textDataRepository.GetAsync();
|
||||||
|
return all.Count;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var byProject = await _textDataRepository.GetByProjectIdAsync(projectId);
|
||||||
|
return byProject.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsHealthyAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var count = await GetDocumentCountAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetStatsAsync()
|
||||||
|
{
|
||||||
|
var totalDocs = await GetDocumentCountAsync();
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "MongoDB",
|
||||||
|
["total_documents"] = totalDocs,
|
||||||
|
["health"] = await IsHealthyAsync(),
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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.
|
||||||
557
Services/SearchVectors/QdrantVectorSearchService.cs
Normal file
557
Services/SearchVectors/QdrantVectorSearchService.cs
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using Qdrant.Client;
|
||||||
|
using static Qdrant.Client.Grpc.Conditions;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
#pragma warning disable SKEXP0001
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.SearchVectors
|
||||||
|
{
|
||||||
|
public class QdrantVectorSearchService : IVectorSearchService
|
||||||
|
{
|
||||||
|
private readonly QdrantClient _client;
|
||||||
|
private readonly QdrantSettings _settings;
|
||||||
|
private readonly ILogger<QdrantVectorSearchService> _logger;
|
||||||
|
private volatile bool _collectionInitialized = false;
|
||||||
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
|
private readonly ConcurrentDictionary<string, bool> _collectionCache = new();
|
||||||
|
|
||||||
|
public QdrantVectorSearchService(
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
ILogger<QdrantVectorSearchService> logger)
|
||||||
|
{
|
||||||
|
_settings = settings.Value.Qdrant;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
_client = new QdrantClient(_settings.Host, _settings.Port, https: _settings.UseTls);
|
||||||
|
|
||||||
|
_logger.LogInformation("QdrantVectorSearchService inicializado para {Host}:{Port}",
|
||||||
|
_settings.Host, _settings.Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureCollectionExistsAsync()
|
||||||
|
{
|
||||||
|
if (_collectionInitialized) return;
|
||||||
|
|
||||||
|
await _initializationSemaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_collectionInitialized) return;
|
||||||
|
|
||||||
|
// Verifica cache primeiro
|
||||||
|
if (_collectionCache.TryGetValue(_settings.CollectionName, out bool exists) && exists)
|
||||||
|
{
|
||||||
|
_collectionInitialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectionExists = await _client.CollectionExistsAsync(_settings.CollectionName);
|
||||||
|
_collectionCache.TryAdd(_settings.CollectionName, collectionExists);
|
||||||
|
|
||||||
|
if (!collectionExists)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Criando collection {CollectionName}...", _settings.CollectionName);
|
||||||
|
|
||||||
|
var vectorsConfig = new VectorParams
|
||||||
|
{
|
||||||
|
Size = (ulong)_settings.VectorSize,
|
||||||
|
Distance = _settings.Distance.ToLower() switch
|
||||||
|
{
|
||||||
|
"cosine" => Distance.Cosine,
|
||||||
|
"euclid" => Distance.Euclid,
|
||||||
|
"dot" => Distance.Dot,
|
||||||
|
"manhattan" => Distance.Manhattan,
|
||||||
|
_ => Distance.Cosine
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configurações HNSW otimizadas
|
||||||
|
if (_settings.HnswM > 0)
|
||||||
|
{
|
||||||
|
vectorsConfig.HnswConfig = new HnswConfigDiff
|
||||||
|
{
|
||||||
|
M = (ulong)_settings.HnswM,
|
||||||
|
EfConstruct = (ulong)_settings.HnswEfConstruct,
|
||||||
|
OnDisk = _settings.OnDisk
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await _client.CreateCollectionAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
vectorsConfig: vectorsConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
_collectionCache.TryAdd(_settings.CollectionName, true);
|
||||||
|
_logger.LogInformation("✅ Collection {CollectionName} criada", _settings.CollectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectionInitialized = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_initializationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> SearchSimilarAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string? projectId = null,
|
||||||
|
double threshold = 0.3,
|
||||||
|
int limit = 5,
|
||||||
|
Dictionary<string, object>? filters = null)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vector = queryEmbedding.Select(x => (float)x).ToArray();
|
||||||
|
|
||||||
|
Filter? filter = null;
|
||||||
|
if (!string.IsNullOrEmpty(projectId) || filters?.Any() == true)
|
||||||
|
{
|
||||||
|
var mustConditions = new List<Condition>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
mustConditions.Add(MatchKeyword("project_id", projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.Any() == true)
|
||||||
|
{
|
||||||
|
foreach (var kvp in filters)
|
||||||
|
{
|
||||||
|
mustConditions.Add(MatchKeyword(kvp.Key, kvp.Value.ToString()!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mustConditions.Any())
|
||||||
|
{
|
||||||
|
filter = new Filter();
|
||||||
|
filter.Must.AddRange(mustConditions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchResult = await _client.SearchAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
vector: vector,
|
||||||
|
filter: filter,
|
||||||
|
limit: (ulong)limit,
|
||||||
|
scoreThreshold: (float)threshold,
|
||||||
|
payloadSelector: true,
|
||||||
|
vectorsSelector: false // Otimização: não buscar vetores desnecessariamente
|
||||||
|
);
|
||||||
|
|
||||||
|
return searchResult.Select(ConvertToVectorSearchResult).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro na busca vetorial Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> SearchSimilarDynamicAsync(
|
||||||
|
double[] queryEmbedding,
|
||||||
|
string projectId,
|
||||||
|
double minThreshold = 0.5,
|
||||||
|
int limit = 5)
|
||||||
|
{
|
||||||
|
var results = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold, limit);
|
||||||
|
|
||||||
|
if (results.Count < 3 && minThreshold > 0.2)
|
||||||
|
{
|
||||||
|
results = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold * 0.7, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count < 3)
|
||||||
|
{
|
||||||
|
results = await SearchSimilarAsync(queryEmbedding, projectId, 0.2, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.Take(limit).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> AddDocumentAsync(
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid().ToString();
|
||||||
|
var vector = embedding.Select(x => (float)x).ToArray();
|
||||||
|
|
||||||
|
var payload = CreatePayload(title, content, projectId, metadata, isUpdate: false);
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = new PointId { Uuid = id },
|
||||||
|
Vectors = vector,
|
||||||
|
Payload = { payload }
|
||||||
|
};
|
||||||
|
|
||||||
|
await _client.UpsertAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
points: new[] { point }
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} adicionado ao Qdrant", id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao adicionar documento no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(
|
||||||
|
string id,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
double[] embedding,
|
||||||
|
Dictionary<string, object>? metadata = null)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vector = embedding.Select(x => (float)x).ToArray();
|
||||||
|
var payload = CreatePayload(title, content, projectId, metadata, isUpdate: true);
|
||||||
|
|
||||||
|
var point = new PointStruct
|
||||||
|
{
|
||||||
|
Id = new PointId { Uuid = id },
|
||||||
|
Vectors = vector,
|
||||||
|
Payload = { payload }
|
||||||
|
};
|
||||||
|
|
||||||
|
await _client.UpsertAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
points: new[] { point }
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} atualizado no Qdrant", id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar documento {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pointId = new PointId { Uuid = id };
|
||||||
|
|
||||||
|
await _client.DeleteAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
ids: new ulong[] { pointId.Num }
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} removido do Qdrant", id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao remover documento {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
var pointId = new PointId { Uuid = id };
|
||||||
|
|
||||||
|
var results = await _client.RetrieveAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
ids: new PointId[] { pointId },
|
||||||
|
withPayload: false, // Otimização: só queremos saber se existe
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.Any();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VectorSearchResult?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pointId = new PointId { Uuid = id };
|
||||||
|
|
||||||
|
var results = await _client.RetrieveAsync(
|
||||||
|
collectionName: _settings.CollectionName,
|
||||||
|
ids: new PointId[] { pointId },
|
||||||
|
withPayload: true,
|
||||||
|
withVectors: false
|
||||||
|
);
|
||||||
|
|
||||||
|
var point = results.FirstOrDefault();
|
||||||
|
return point != null ? ConvertToVectorSearchResult(point) : null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<VectorSearchResult>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = new Filter();
|
||||||
|
filter.Must.Add(MatchKeyword("project_id", projectId));
|
||||||
|
|
||||||
|
var scrollRequest = new ScrollPoints
|
||||||
|
{
|
||||||
|
CollectionName = _settings.CollectionName,
|
||||||
|
Filter = filter,
|
||||||
|
Limit = 10000,
|
||||||
|
WithPayload = true,
|
||||||
|
WithVectors = false // Otimização: não buscar vetores
|
||||||
|
};
|
||||||
|
|
||||||
|
var results = await _client.ScrollAsync(_settings.CollectionName, filter, 10000, null, true, false);
|
||||||
|
|
||||||
|
return results.Result.Select(ConvertToVectorSearchResult).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao buscar documentos do projeto {ProjectId} no Qdrant", projectId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Filter? filter = null;
|
||||||
|
if (!string.IsNullOrEmpty(projectId))
|
||||||
|
{
|
||||||
|
filter = new Filter();
|
||||||
|
filter.Must.Add(MatchKeyword("project_id", projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _client.CountAsync(_settings.CollectionName, filter);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao contar documentos no Qdrant");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsHealthyAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var collections = await _client.ListCollectionsAsync();
|
||||||
|
return collections != null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnsureCollectionExistsAsync();
|
||||||
|
|
||||||
|
var collectionInfo = await _client.GetCollectionInfoAsync(_settings.CollectionName);
|
||||||
|
var totalDocs = await GetDocumentCountAsync();
|
||||||
|
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "Qdrant",
|
||||||
|
["total_documents"] = totalDocs,
|
||||||
|
["collection_name"] = _settings.CollectionName,
|
||||||
|
["vector_size"] = _settings.VectorSize,
|
||||||
|
["distance_metric"] = _settings.Distance,
|
||||||
|
["points_count"] = collectionInfo.PointsCount,
|
||||||
|
["segments_count"] = collectionInfo.SegmentsCount,
|
||||||
|
["health"] = await IsHealthyAsync(),
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "Qdrant",
|
||||||
|
["health"] = false,
|
||||||
|
["error"] = ex.Message,
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métodos auxiliares otimizados
|
||||||
|
private static Dictionary<string, Value> CreatePayload(
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string projectId,
|
||||||
|
Dictionary<string, object>? metadata,
|
||||||
|
bool isUpdate)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, Value>
|
||||||
|
{
|
||||||
|
["title"] = title,
|
||||||
|
["content"] = content,
|
||||||
|
["project_id"] = projectId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUpdate)
|
||||||
|
{
|
||||||
|
payload["updated_at"] = DateTime.UtcNow.ToString("O");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
payload["created_at"] = DateTime.UtcNow.ToString("O");
|
||||||
|
payload["updated_at"] = DateTime.UtcNow.ToString("O");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.Any() == true)
|
||||||
|
{
|
||||||
|
foreach (var kvp in metadata)
|
||||||
|
{
|
||||||
|
payload[$"meta_{kvp.Key}"] = ConvertToValue(kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VectorSearchResult ConvertToVectorSearchResult(ScoredPoint point)
|
||||||
|
{
|
||||||
|
return new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = point.Id.Uuid ?? point.Id.Num.ToString(),
|
||||||
|
Title = GetStringFromPayload(point.Payload, "title"),
|
||||||
|
Content = GetStringFromPayload(point.Payload, "content"),
|
||||||
|
ProjectId = GetStringFromPayload(point.Payload, "project_id"),
|
||||||
|
Score = point.Score,
|
||||||
|
Provider = "Qdrant",
|
||||||
|
CreatedAt = GetDateTimeFromPayload(point.Payload, "created_at"),
|
||||||
|
UpdatedAt = GetDateTimeFromPayload(point.Payload, "updated_at"),
|
||||||
|
Metadata = ConvertPayloadToMetadata(point.Payload)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VectorSearchResult ConvertToVectorSearchResult(RetrievedPoint point)
|
||||||
|
{
|
||||||
|
return new VectorSearchResult
|
||||||
|
{
|
||||||
|
Id = point.Id.Uuid ?? point.Id.Num.ToString(),
|
||||||
|
Title = GetStringFromPayload(point.Payload, "title"),
|
||||||
|
Content = GetStringFromPayload(point.Payload, "content"),
|
||||||
|
ProjectId = GetStringFromPayload(point.Payload, "project_id"),
|
||||||
|
Score = 1.0,
|
||||||
|
Provider = "Qdrant",
|
||||||
|
CreatedAt = GetDateTimeFromPayload(point.Payload, "created_at"),
|
||||||
|
UpdatedAt = GetDateTimeFromPayload(point.Payload, "updated_at"),
|
||||||
|
Metadata = ConvertPayloadToMetadata(point.Payload)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Value ConvertToValue(object value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
string s => s,
|
||||||
|
int i => i,
|
||||||
|
long l => l,
|
||||||
|
double d => d,
|
||||||
|
float f => f,
|
||||||
|
bool b => b,
|
||||||
|
DateTime dt => dt.ToString("O"),
|
||||||
|
_ => value?.ToString() ?? ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetStringFromPayload(
|
||||||
|
IDictionary<string, Value> payload,
|
||||||
|
string key,
|
||||||
|
string defaultValue = "")
|
||||||
|
{
|
||||||
|
return payload.TryGetValue(key, out var value) ? value.StringValue : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime GetDateTimeFromPayload(
|
||||||
|
IDictionary<string, Value> payload,
|
||||||
|
string key)
|
||||||
|
{
|
||||||
|
if (payload.TryGetValue(key, out var value) &&
|
||||||
|
DateTime.TryParse(value.StringValue, out var date))
|
||||||
|
{
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
return DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, object>? ConvertPayloadToMetadata(
|
||||||
|
IDictionary<string, Value> payload)
|
||||||
|
{
|
||||||
|
var metadata = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
foreach (var kvp in payload.Where(p => p.Key.StartsWith("meta_")))
|
||||||
|
{
|
||||||
|
var key = kvp.Key.Substring(5);
|
||||||
|
var value = kvp.Value;
|
||||||
|
|
||||||
|
metadata[key] = value.KindCase switch
|
||||||
|
{
|
||||||
|
Value.KindOneofCase.StringValue => value.StringValue,
|
||||||
|
Value.KindOneofCase.IntegerValue => value.IntegerValue,
|
||||||
|
Value.KindOneofCase.DoubleValue => value.DoubleValue,
|
||||||
|
Value.KindOneofCase.BoolValue => value.BoolValue,
|
||||||
|
_ => value.StringValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.Any() ? metadata : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_initializationSemaphore?.Dispose();
|
||||||
|
_client?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore SKEXP0001
|
||||||
107
Services/SearchVectors/VectorDatabaseFactory.cs
Normal file
107
Services/SearchVectors/VectorDatabaseFactory.cs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
using ChatApi.Data;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.ResponseService;
|
||||||
|
using ChatRAG.Services.TextServices;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.SearchVectors
|
||||||
|
{
|
||||||
|
public class VectorDatabaseFactory : IVectorDatabaseFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly VectorDatabaseSettings _settings;
|
||||||
|
private readonly ILogger<VectorDatabaseFactory> _logger;
|
||||||
|
|
||||||
|
public VectorDatabaseFactory(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
ILogger<VectorDatabaseFactory> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetActiveProvider()
|
||||||
|
{
|
||||||
|
return _settings.Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VectorDatabaseSettings GetSettings()
|
||||||
|
{
|
||||||
|
return _settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IVectorSearchService CreateVectorSearchService()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Criando VectorSearchService para provider: {Provider}", _settings.Provider);
|
||||||
|
|
||||||
|
return _settings.Provider.ToLower() switch
|
||||||
|
{
|
||||||
|
"qdrant" => GetService<QdrantVectorSearchService>(),
|
||||||
|
"mongodb" => GetService<MongoVectorSearchService>(),
|
||||||
|
"chroma" => GetService<ChromaVectorSearchService>(),
|
||||||
|
_ => throw new ArgumentException($"Provider de VectorSearch não suportado: {_settings.Provider}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITextDataService CreateTextDataService()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Criando TextDataService para provider: {Provider}", _settings.Provider);
|
||||||
|
|
||||||
|
return _settings.Provider.ToLower() switch
|
||||||
|
{
|
||||||
|
"qdrant" => GetService<QdrantTextDataService>(),
|
||||||
|
"mongodb" => GetService<MongoTextDataService>(),
|
||||||
|
"chroma" => GetService<ChromaTextDataService>(),
|
||||||
|
_ => throw new ArgumentException($"Provider de TextDataService não suportado: {_settings.Provider}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public IResponseService CreateResponseService()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Criando ResponseService para provider: {Provider}", _settings.Provider);
|
||||||
|
|
||||||
|
// Verificar se deve usar RAG Hierárquico
|
||||||
|
var configuration = _serviceProvider.GetService<IConfiguration>();
|
||||||
|
var useHierarchical = configuration?.GetValue<bool>("Features:UseHierarchicalRAG") ?? false;
|
||||||
|
var useConfidenceAware = configuration?.GetValue<bool>("Features:UseConfidenceAwareRAG") ?? false;
|
||||||
|
|
||||||
|
if (useHierarchical && !useConfidenceAware)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Usando HierarchicalRAGService");
|
||||||
|
return GetService<HierarchicalRAGService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useConfidenceAware)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Usando ConfidenceAwareRAGService");
|
||||||
|
return GetService<ConfidenceAwareRAGService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usar estratégia baseada no provider ou configuração
|
||||||
|
var ragStrategy = configuration?.GetValue<string>("Features:RAGStrategy");
|
||||||
|
|
||||||
|
return ragStrategy?.ToLower() switch
|
||||||
|
{
|
||||||
|
"hierarchical" => GetService<HierarchicalRAGService>(),
|
||||||
|
"standard" => GetService<ResponseRAGService>(),
|
||||||
|
_ => GetService<ResponseRAGService>() // Padrão
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private T GetService<T>() where T : class
|
||||||
|
{
|
||||||
|
var service = _serviceProvider.GetService<T>();
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Serviço {typeof(T).Name} não está registrado no DI container. " +
|
||||||
|
$"Verifique se o serviço foi registrado para o provider '{_settings.Provider}'.");
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
Services/SearchVectors/anv10eil.vdi~
Normal file
55
Services/SearchVectors/anv10eil.vdi~
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
using ChatApi.Data;
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.ResponseService;
|
||||||
|
using ChatRAG.Services.TextServices;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.SearchVectors
|
||||||
|
{
|
||||||
|
public class VectorDatabaseFactory : IVectorDatabaseFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly VectorDatabaseSettings _settings;
|
||||||
|
private readonly ILogger<VectorDatabaseFactory> _logger;
|
||||||
|
|
||||||
|
public VectorDatabaseFactory(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IOptions<VectorDatabaseSettings> settings,
|
||||||
|
ILogger<VectorDatabaseFactory> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetActiveProvider()
|
||||||
|
{
|
||||||
|
return _settings.Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IVectorSearchService CreateVectorSearchService()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Criando VectorSearchService para provider: {Provider}", _settings.Provider);
|
||||||
|
|
||||||
|
return _settings.Provider.ToLower() switch
|
||||||
|
{
|
||||||
|
"qdrant" => GetService<QdrantVectorSearchService>(),
|
||||||
|
"mongodb" => GetService<MongoVectorSearchService>(),
|
||||||
|
"chroma" => GetService<ChromaVectorSearchService>(),
|
||||||
|
_ => throw new ArgumentException($"Provider de VectorSearch não suportado: {_settings.Provider}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private T GetService<T>() where T : class
|
||||||
|
{
|
||||||
|
var service = _serviceProvider.GetService<T>();
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Serviço {typeof(T).Name} não está registrado no DI container");
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
537
Services/TextServices/ChromaTextDataService.cs
Normal file
537
Services/TextServices/ChromaTextDataService.cs
Normal file
@ -0,0 +1,537 @@
|
|||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
#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
|
||||||
|
{
|
||||||
|
// ========================================
|
||||||
|
// CHROMA TEXT DATA SERVICE - IMPLEMENTAÇÃO COMPLETA
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public class ChromaTextDataService : ITextDataService
|
||||||
|
{
|
||||||
|
private readonly IVectorSearchService _vectorSearchService;
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
private readonly ILogger<ChromaTextDataService> _logger;
|
||||||
|
|
||||||
|
public ChromaTextDataService(
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ITextEmbeddingGenerationService embeddingService,
|
||||||
|
ILogger<ChromaTextDataService> logger)
|
||||||
|
{
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProviderName => "Chroma";
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS ORIGINAIS (compatibilidade com MongoDB)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
|
||||||
|
{
|
||||||
|
await SalvarNoMongoDB(null, titulo, texto, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conteudo = $"**{titulo}** \n\n {texto}";
|
||||||
|
|
||||||
|
// Gera embedding
|
||||||
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
|
||||||
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
// Cria novo documento
|
||||||
|
await _vectorSearchService.AddDocumentAsync(titulo, texto, projectId, embeddingArray);
|
||||||
|
_logger.LogDebug("Documento '{Title}' criado no Chroma", titulo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Atualiza documento existente
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(id, titulo, texto, projectId, embeddingArray);
|
||||||
|
_logger.LogDebug("Documento '{Id}' atualizado no Chroma", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar documento '{Title}' no Chroma", titulo);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var textoArray = new List<string>();
|
||||||
|
string[] textolinhas = textoCompleto.Split(
|
||||||
|
new string[] { "\n" },
|
||||||
|
StringSplitOptions.None
|
||||||
|
);
|
||||||
|
|
||||||
|
var title = textolinhas[0];
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (string line in textolinhas)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("**") || line.StartsWith("\r**"))
|
||||||
|
{
|
||||||
|
if (builder.Length > 0)
|
||||||
|
{
|
||||||
|
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
|
||||||
|
builder = new StringBuilder();
|
||||||
|
title = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AppendLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adiciona último bloco se houver
|
||||||
|
if (builder.Length > 0)
|
||||||
|
{
|
||||||
|
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processa cada seção
|
||||||
|
foreach (var item in textoArray)
|
||||||
|
{
|
||||||
|
await SalvarNoMongoDB(title.Replace("**", "").Replace("\r", ""), item, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Texto completo processado: {SectionCount} seções salvas no Chroma", textoArray.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar texto completo no Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Busca todos os projetos e depois todos os documentos
|
||||||
|
var allDocuments = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
// Como Chroma não tem um "GetAll" direto, vamos usar scroll
|
||||||
|
// Isso é uma limitação vs MongoDB, mas é mais eficiente
|
||||||
|
var projects = await GetAllProjectIds();
|
||||||
|
|
||||||
|
foreach (var projectId in projects)
|
||||||
|
{
|
||||||
|
var projectDocs = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
allDocuments.AddRange(projectDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDocuments.Select(ConvertToTextoComEmbedding);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar todos os documentos do Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var documents = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
return documents.Select(ConvertToTextoComEmbedding);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} no Chroma", projectId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TextoComEmbedding> GetById(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var document = await _vectorSearchService.GetDocumentAsync(id);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Documento {id} não encontrado no Chroma");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertToTextoComEmbedding(document);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Chroma", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS NOVOS DA INTERFACE
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<string> SaveDocumentAsync(DocumentInput document)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conteudo = $"**{document.Title}** \n\n {document.Content}";
|
||||||
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
|
||||||
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
|
string id;
|
||||||
|
if (!string.IsNullOrEmpty(document.Id))
|
||||||
|
{
|
||||||
|
// Atualizar documento existente
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(
|
||||||
|
document.Id,
|
||||||
|
document.Title,
|
||||||
|
document.Content,
|
||||||
|
document.ProjectId,
|
||||||
|
embeddingArray,
|
||||||
|
document.Metadata);
|
||||||
|
id = document.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Criar novo documento
|
||||||
|
id = await _vectorSearchService.AddDocumentAsync(
|
||||||
|
document.Title,
|
||||||
|
document.Content,
|
||||||
|
document.ProjectId,
|
||||||
|
embeddingArray,
|
||||||
|
document.Metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} salvo no Chroma via SaveDocumentAsync", id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar documento no Chroma");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(string id, DocumentInput document)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conteudo = $"**{document.Title}** \n\n {document.Content}";
|
||||||
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
|
||||||
|
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(
|
||||||
|
id,
|
||||||
|
document.Title,
|
||||||
|
document.Content,
|
||||||
|
document.ProjectId,
|
||||||
|
embeddingArray,
|
||||||
|
document.Metadata);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} atualizado no Chroma", id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar documento {Id} no Chroma", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vectorSearchService.DeleteDocumentAsync(id);
|
||||||
|
_logger.LogDebug("Documento {Id} deletado do Chroma", id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao deletar documento {Id} do Chroma", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vectorSearchService.DocumentExistsAsync(id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao verificar existência do documento {Id} no Chroma", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentOutput?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _vectorSearchService.GetDocumentAsync(id);
|
||||||
|
if (result == null) return null;
|
||||||
|
|
||||||
|
return new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Title = result.Metadata?.GetValueOrDefault("title")?.ToString() ?? "",
|
||||||
|
Content = result.Content,
|
||||||
|
ProjectId = result.Metadata?.GetValueOrDefault("project_id")?.ToString() ?? "",
|
||||||
|
Embedding = Array.Empty<double>(), // Chroma não retorna embedding na busca
|
||||||
|
CreatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("created_at")?.ToString()).Value,
|
||||||
|
UpdatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("updated_at")?.ToString()).Value,
|
||||||
|
Metadata = result.Metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Chroma", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
|
||||||
|
return results.Select(result => new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Title = result.Metadata?.GetValueOrDefault("title")?.ToString() ?? "",
|
||||||
|
Content = result.Content,
|
||||||
|
ProjectId = projectId,
|
||||||
|
Embedding = Array.Empty<double>(),
|
||||||
|
CreatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("created_at")?.ToString()).Value,
|
||||||
|
UpdatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("updated_at")?.ToString()).Value,
|
||||||
|
Metadata = result.Metadata
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} do Chroma", projectId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vectorSearchService.GetDocumentCountAsync(projectId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao contar documentos no Chroma");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// OPERAÇÕES EM LOTE
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
|
||||||
|
{
|
||||||
|
var ids = new List<string>();
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
|
||||||
|
// Processa em lotes menores para performance
|
||||||
|
var batchSize = 10;
|
||||||
|
for (int i = 0; i < documents.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = documents.Skip(i).Take(batchSize);
|
||||||
|
var tasks = batch.Select(async doc =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = await SaveDocumentAsync(doc);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add(ex);
|
||||||
|
_logger.LogError(ex, "Erro ao salvar documento '{Title}' em lote", doc.Title);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var batchResults = await Task.WhenAll(tasks);
|
||||||
|
ids.AddRange(batchResults.Where(id => id != null)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Batch save completado com {ErrorCount} erros de {TotalCount} documentos",
|
||||||
|
errors.Count, documents.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Batch save: {SuccessCount}/{TotalCount} documentos salvos no Chroma",
|
||||||
|
ids.Count, documents.Count);
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentsBatchAsync(List<string> ids)
|
||||||
|
{
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
|
||||||
|
// Processa em lotes para não sobrecarregar
|
||||||
|
var batchSize = 20;
|
||||||
|
for (int i = 0; i < ids.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = ids.Skip(i).Take(batchSize);
|
||||||
|
var tasks = batch.Select(async id =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DeleteDocumentAsync(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add(ex);
|
||||||
|
_logger.LogError(ex, "Erro ao deletar documento {Id} em lote", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Batch delete completado com {ErrorCount} erros de {TotalCount} documentos",
|
||||||
|
errors.Count, ids.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Batch delete: {TotalCount} documentos removidos do Chroma", ids.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ESTATÍSTICAS DO PROVIDER
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseStats = await _vectorSearchService.GetStatsAsync();
|
||||||
|
var totalDocs = await GetDocumentCountAsync();
|
||||||
|
|
||||||
|
// Adiciona estatísticas específicas do TextData
|
||||||
|
var projectIds = await GetAllProjectIds();
|
||||||
|
var projectStats = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
foreach (var projectId in projectIds)
|
||||||
|
{
|
||||||
|
var count = await GetDocumentCountAsync(projectId);
|
||||||
|
projectStats[projectId] = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enhancedStats = new Dictionary<string, object>(baseStats)
|
||||||
|
{
|
||||||
|
["text_service_provider"] = "Chroma",
|
||||||
|
["total_documents_via_text_service"] = totalDocs,
|
||||||
|
["projects_count"] = projectIds.Count,
|
||||||
|
["documents_by_project"] = projectStats,
|
||||||
|
["supports_batch_operations"] = true,
|
||||||
|
["supports_metadata"] = true,
|
||||||
|
["embedding_auto_generation"] = true
|
||||||
|
};
|
||||||
|
|
||||||
|
return enhancedStats;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "Chroma",
|
||||||
|
["text_service_provider"] = "Chroma",
|
||||||
|
["health"] = "error",
|
||||||
|
["error"] = ex.Message,
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS AUXILIARES PRIVADOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
private static TextoComEmbedding ConvertToTextoComEmbedding(VectorSearchResult result)
|
||||||
|
{
|
||||||
|
return new TextoComEmbedding
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Titulo = result.Metadata?.GetValueOrDefault("title")?.ToString() ?? "",
|
||||||
|
Conteudo = result.Content,
|
||||||
|
ProjetoId = result.Metadata?.GetValueOrDefault("project_id")?.ToString() ?? "",
|
||||||
|
Embedding = Array.Empty<double>(), // Chroma não retorna embedding na busca
|
||||||
|
// Campos que podem não existir no Chroma
|
||||||
|
ProjetoNome = result.Metadata?.GetValueOrDefault("project_name")?.ToString() ?? "",
|
||||||
|
TipoDocumento = result.Metadata?.GetValueOrDefault("document_type")?.ToString() ?? "",
|
||||||
|
Categoria = result.Metadata?.GetValueOrDefault("category")?.ToString() ?? "",
|
||||||
|
Tags = result.Metadata?.GetValueOrDefault("tags") as string[] ?? Array.Empty<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<string>> GetAllProjectIds()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Esta é uma operação custosa no Chroma
|
||||||
|
// Em produção, seria melhor manter um cache de project IDs
|
||||||
|
// ou usar uma estrutura de dados separada
|
||||||
|
|
||||||
|
// Por agora, vamos usar uma busca com um vetor dummy para pegar todos os documentos
|
||||||
|
var dummyVector = new double[384]; // Assumindo embeddings padrão
|
||||||
|
var allResults = await _vectorSearchService.SearchSimilarAsync(
|
||||||
|
dummyVector,
|
||||||
|
projectId: null,
|
||||||
|
threshold: 0.0,
|
||||||
|
limit: 10000);
|
||||||
|
|
||||||
|
return allResults
|
||||||
|
.Select(r => r.Metadata?.GetValueOrDefault("project_id")?.ToString())
|
||||||
|
.Where(pid => !string.IsNullOrEmpty(pid))
|
||||||
|
.Distinct()
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar IDs de projetos do Chroma");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ParseDateTime(string? dateString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dateString))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (DateTime.TryParse(dateString, out var date))
|
||||||
|
return date;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#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.
|
||||||
|
|
||||||
674
Services/TextServices/QdrantTextDataService.cs
Normal file
674
Services/TextServices/QdrantTextDataService.cs
Normal file
@ -0,0 +1,674 @@
|
|||||||
|
#pragma warning disable SKEXP0001
|
||||||
|
|
||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using Microsoft.SemanticKernel.Embeddings;
|
||||||
|
using System.Text;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.TextServices
|
||||||
|
{
|
||||||
|
public class QdrantTextDataService : ITextDataService
|
||||||
|
{
|
||||||
|
private readonly IVectorSearchService _vectorSearchService;
|
||||||
|
private readonly ITextEmbeddingGenerationService _embeddingService;
|
||||||
|
private readonly ILogger<QdrantTextDataService> _logger;
|
||||||
|
|
||||||
|
// Cache para project IDs para evitar buscas custosas
|
||||||
|
private readonly ConcurrentDictionary<string, DateTime> _projectIdCache = new();
|
||||||
|
private readonly TimeSpan _cacheTimeout = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
public QdrantTextDataService(
|
||||||
|
IVectorSearchService vectorSearchService,
|
||||||
|
ITextEmbeddingGenerationService embeddingService,
|
||||||
|
ILogger<QdrantTextDataService> logger)
|
||||||
|
{
|
||||||
|
_vectorSearchService = vectorSearchService;
|
||||||
|
_embeddingService = embeddingService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProviderName => "Qdrant";
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS ORIGINAIS (compatibilidade com MongoDB)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
|
||||||
|
{
|
||||||
|
await SalvarNoMongoDB(null, titulo, texto, projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conteudo = $"**{titulo}** \n\n {texto}";
|
||||||
|
|
||||||
|
// Gera embedding uma única vez
|
||||||
|
var embedding = await GenerateEmbeddingOptimized(conteudo);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
// Cria novo documento
|
||||||
|
var newId = await _vectorSearchService.AddDocumentAsync(titulo, texto, projectId, embedding);
|
||||||
|
|
||||||
|
// Atualiza cache de project IDs
|
||||||
|
_projectIdCache.TryAdd(projectId, DateTime.UtcNow);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento '{Title}' criado no Qdrant com ID {Id}", titulo, newId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Atualiza documento existente
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(id, titulo, texto, projectId, embedding);
|
||||||
|
_logger.LogDebug("Documento '{Id}' atualizado no Qdrant", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar documento '{Title}' no Qdrant", titulo);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var textoArray = ParseTextIntoSections(textoCompleto);
|
||||||
|
|
||||||
|
// Processa seções em paralelo com limite de concorrência
|
||||||
|
var semaphore = new SemaphoreSlim(5, 5); // Máximo 5 operações simultâneas
|
||||||
|
var tasks = textoArray.Select(async item =>
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lines = item.Split('\n', 2);
|
||||||
|
var title = lines[0].Replace("**", "").Replace("\r", "").Trim();
|
||||||
|
var content = lines.Length > 1 ? lines[1] : "";
|
||||||
|
|
||||||
|
await SalvarNoMongoDB(title, content, projectId);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
semaphore.Dispose();
|
||||||
|
|
||||||
|
_logger.LogInformation("Texto completo processado: {SectionCount} seções salvas no Qdrant", textoArray.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao processar texto completo no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Usa cache de project IDs quando possível
|
||||||
|
var projectIds = await GetAllProjectIdsOptimized();
|
||||||
|
|
||||||
|
if (!projectIds.Any())
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<TextoComEmbedding>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var allDocuments = new List<VectorSearchResult>();
|
||||||
|
|
||||||
|
// Busca documentos em paralelo por projeto
|
||||||
|
var semaphore = new SemaphoreSlim(3, 3); // Máximo 3 projetos simultâneos
|
||||||
|
var tasks = projectIds.Select(async projectId =>
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Erro ao buscar documentos do projeto {ProjectId}", projectId);
|
||||||
|
return new List<VectorSearchResult>();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
semaphore.Dispose();
|
||||||
|
|
||||||
|
foreach (var projectDocs in results)
|
||||||
|
{
|
||||||
|
allDocuments.AddRange(projectDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDocuments.Select(ConvertToTextoComEmbedding);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar todos os documentos do Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var documents = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
return documents.Select(ConvertToTextoComEmbedding);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} no Qdrant", projectId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TextoComEmbedding> GetById(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var document = await _vectorSearchService.GetDocumentAsync(id);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Documento {id} não encontrado no Qdrant");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertToTextoComEmbedding(document);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS NOVOS DA INTERFACE
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<string> SaveDocumentAsync(DocumentInput document)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var embedding = await GenerateEmbeddingOptimized($"**{document.Title}** \n\n {document.Content}");
|
||||||
|
|
||||||
|
string id;
|
||||||
|
if (!string.IsNullOrEmpty(document.Id))
|
||||||
|
{
|
||||||
|
// Atualizar documento existente
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(
|
||||||
|
document.Id,
|
||||||
|
document.Title,
|
||||||
|
document.Content,
|
||||||
|
document.ProjectId,
|
||||||
|
embedding,
|
||||||
|
document.Metadata);
|
||||||
|
id = document.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Criar novo documento
|
||||||
|
id = await _vectorSearchService.AddDocumentAsync(
|
||||||
|
document.Title,
|
||||||
|
document.Content,
|
||||||
|
document.ProjectId,
|
||||||
|
embedding,
|
||||||
|
document.Metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza cache de project IDs
|
||||||
|
_projectIdCache.TryAdd(document.ProjectId, DateTime.UtcNow);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} salvo no Qdrant via SaveDocumentAsync", id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao salvar documento no Qdrant");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateDocumentAsync(string id, DocumentInput document)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var embedding = await GenerateEmbeddingOptimized($"**{document.Title}** \n\n {document.Content}");
|
||||||
|
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(
|
||||||
|
id,
|
||||||
|
document.Title,
|
||||||
|
document.Content,
|
||||||
|
document.ProjectId,
|
||||||
|
embedding,
|
||||||
|
document.Metadata);
|
||||||
|
|
||||||
|
_logger.LogDebug("Documento {Id} atualizado no Qdrant", id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao atualizar documento {Id} no Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vectorSearchService.DeleteDocumentAsync(id);
|
||||||
|
_logger.LogDebug("Documento {Id} deletado do Qdrant", id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao deletar documento {Id} do Qdrant", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DocumentExistsAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vectorSearchService.DocumentExistsAsync(id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao verificar existência do documento {Id} no Qdrant", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentOutput?> GetDocumentAsync(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _vectorSearchService.GetDocumentAsync(id);
|
||||||
|
if (result == null) return null;
|
||||||
|
|
||||||
|
return new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Title = result.Title,
|
||||||
|
Content = result.Content,
|
||||||
|
ProjectId = result.ProjectId,
|
||||||
|
Embedding = result.Embedding,
|
||||||
|
CreatedAt = result.CreatedAt,
|
||||||
|
UpdatedAt = result.UpdatedAt,
|
||||||
|
Metadata = result.Metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Qdrant", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
|
||||||
|
|
||||||
|
return results.Select(result => new DocumentOutput
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Title = result.Title,
|
||||||
|
Content = result.Content,
|
||||||
|
ProjectId = result.ProjectId,
|
||||||
|
Embedding = result.Embedding,
|
||||||
|
CreatedAt = result.CreatedAt,
|
||||||
|
UpdatedAt = result.UpdatedAt,
|
||||||
|
Metadata = result.Metadata
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} do Qdrant", projectId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetDocumentCountAsync(string? projectId = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _vectorSearchService.GetDocumentCountAsync(projectId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao contar documentos no Qdrant");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// OPERAÇÕES EM LOTE OTIMIZADAS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
|
||||||
|
{
|
||||||
|
var ids = new List<string>();
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
|
||||||
|
// Agrupa documentos por projeto para otimizar embeddings
|
||||||
|
var documentsByProject = documents.GroupBy(d => d.ProjectId).ToList();
|
||||||
|
|
||||||
|
foreach (var projectGroup in documentsByProject)
|
||||||
|
{
|
||||||
|
var projectDocs = projectGroup.ToList();
|
||||||
|
|
||||||
|
// Processa em lotes menores dentro do projeto
|
||||||
|
var batchSize = 5; // Reduzido para evitar timeout
|
||||||
|
for (int i = 0; i < projectDocs.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = projectDocs.Skip(i).Take(batchSize);
|
||||||
|
|
||||||
|
// Gera embeddings em paralelo para o lote
|
||||||
|
var embeddingTasks = batch.Select(async doc =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var embedding = await GenerateEmbeddingOptimized($"**{doc.Title}** \n\n {doc.Content}");
|
||||||
|
return new { Document = doc, Embedding = embedding, Error = (Exception?)null };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new { Document = doc, Embedding = (double[]?)null, Error = ex };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var embeddingResults = await Task.WhenAll(embeddingTasks);
|
||||||
|
|
||||||
|
// Salva documentos com embeddings gerados
|
||||||
|
var saveTasks = embeddingResults.Select(async result =>
|
||||||
|
{
|
||||||
|
if (result.Error != null)
|
||||||
|
{
|
||||||
|
errors.Add(result.Error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string id;
|
||||||
|
if (!string.IsNullOrEmpty(result.Document.Id))
|
||||||
|
{
|
||||||
|
await _vectorSearchService.UpdateDocumentAsync(
|
||||||
|
result.Document.Id,
|
||||||
|
result.Document.Title,
|
||||||
|
result.Document.Content,
|
||||||
|
result.Document.ProjectId,
|
||||||
|
result.Embedding!,
|
||||||
|
result.Document.Metadata);
|
||||||
|
id = result.Document.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
id = await _vectorSearchService.AddDocumentAsync(
|
||||||
|
result.Document.Title,
|
||||||
|
result.Document.Content,
|
||||||
|
result.Document.ProjectId,
|
||||||
|
result.Embedding!,
|
||||||
|
result.Document.Metadata);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add(ex);
|
||||||
|
_logger.LogError(ex, "Erro ao salvar documento '{Title}' em lote", result.Document.Title);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var batchResults = await Task.WhenAll(saveTasks);
|
||||||
|
ids.AddRange(batchResults.Where(id => id != null)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza cache para o projeto
|
||||||
|
_projectIdCache.TryAdd(projectGroup.Key, DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Batch save completado com {ErrorCount} erros de {TotalCount} documentos",
|
||||||
|
errors.Count, documents.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Batch save: {SuccessCount}/{TotalCount} documentos salvos no Qdrant",
|
||||||
|
ids.Count, documents.Count);
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteDocumentsBatchAsync(List<string> ids)
|
||||||
|
{
|
||||||
|
var errors = new List<Exception>();
|
||||||
|
|
||||||
|
// Processa em lotes pequenos para não sobrecarregar
|
||||||
|
var batchSize = 10; // Reduzido para melhor estabilidade
|
||||||
|
for (int i = 0; i < ids.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = ids.Skip(i).Take(batchSize);
|
||||||
|
var tasks = batch.Select(async id =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _vectorSearchService.DeleteDocumentAsync(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add(ex);
|
||||||
|
_logger.LogError(ex, "Erro ao deletar documento {Id} em lote", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Batch delete completado com {ErrorCount} erros de {TotalCount} documentos",
|
||||||
|
errors.Count, ids.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Batch delete: {TotalCount} documentos removidos do Qdrant", ids.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// ESTATÍSTICAS DO PROVIDER
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseStats = await _vectorSearchService.GetStatsAsync();
|
||||||
|
var totalDocs = await GetDocumentCountAsync();
|
||||||
|
|
||||||
|
// Usa cache para project IDs
|
||||||
|
var projectIds = await GetAllProjectIdsOptimized();
|
||||||
|
var projectStats = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
// Busca contadores em paralelo
|
||||||
|
var countTasks = projectIds.Select(async projectId =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var count = await GetDocumentCountAsync(projectId);
|
||||||
|
return new { ProjectId = projectId, Count = count };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new { ProjectId = projectId, Count = 0 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var countResults = await Task.WhenAll(countTasks);
|
||||||
|
foreach (var result in countResults)
|
||||||
|
{
|
||||||
|
projectStats[result.ProjectId] = result.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enhancedStats = new Dictionary<string, object>(baseStats)
|
||||||
|
{
|
||||||
|
["text_service_provider"] = "Qdrant",
|
||||||
|
["total_documents_via_text_service"] = totalDocs,
|
||||||
|
["projects_count"] = projectIds.Count,
|
||||||
|
["documents_by_project"] = projectStats,
|
||||||
|
["supports_batch_operations"] = true,
|
||||||
|
["supports_metadata"] = true,
|
||||||
|
["embedding_auto_generation"] = true,
|
||||||
|
["cache_enabled"] = true,
|
||||||
|
["cached_project_ids"] = _projectIdCache.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
return enhancedStats;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = "Qdrant",
|
||||||
|
["text_service_provider"] = "Qdrant",
|
||||||
|
["health"] = "error",
|
||||||
|
["error"] = ex.Message,
|
||||||
|
["last_check"] = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS AUXILIARES PRIVADOS OTIMIZADOS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
private async Task<double[]> GenerateEmbeddingOptimized(string content)
|
||||||
|
{
|
||||||
|
var embedding = await _embeddingService.GenerateEmbeddingAsync(content);
|
||||||
|
return embedding.ToArray().Select(e => (double)e).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ParseTextIntoSections(string textoCompleto)
|
||||||
|
{
|
||||||
|
var textoArray = new List<string>();
|
||||||
|
string[] textolinhas = textoCompleto.Split(new string[] { "\n" }, StringSplitOptions.None);
|
||||||
|
|
||||||
|
var title = textolinhas[0];
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (string line in textolinhas)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("**") || line.StartsWith("\r**"))
|
||||||
|
{
|
||||||
|
if (builder.Length > 0)
|
||||||
|
{
|
||||||
|
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
|
||||||
|
builder = new StringBuilder();
|
||||||
|
title = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.AppendLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adiciona último bloco se houver
|
||||||
|
if (builder.Length > 0)
|
||||||
|
{
|
||||||
|
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return textoArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<string>> GetAllProjectIdsOptimized()
|
||||||
|
{
|
||||||
|
// Remove entradas expiradas do cache
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var expiredKeys = _projectIdCache
|
||||||
|
.Where(kvp => now - kvp.Value > _cacheTimeout)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var key in expiredKeys)
|
||||||
|
{
|
||||||
|
_projectIdCache.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se temos dados no cache e não estão muito antigos, usa o cache
|
||||||
|
if (_projectIdCache.Any())
|
||||||
|
{
|
||||||
|
return _projectIdCache.Keys.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caso contrário, busca no Qdrant
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Esta busca é custosa, mas só será executada quando o cache estiver vazio
|
||||||
|
var allResults = await _vectorSearchService.SearchSimilarAsync(
|
||||||
|
new double[384], // Vector dummy menor
|
||||||
|
projectId: null,
|
||||||
|
threshold: 0.0,
|
||||||
|
limit: 1000); // Limit menor para melhor performance
|
||||||
|
|
||||||
|
var projectIds = allResults
|
||||||
|
.Select(r => r.ProjectId)
|
||||||
|
.Where(pid => !string.IsNullOrEmpty(pid))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Atualiza cache
|
||||||
|
foreach (var projectId in projectIds)
|
||||||
|
{
|
||||||
|
_projectIdCache.TryAdd(projectId, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectIds;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro ao recuperar IDs de projetos do Qdrant");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextoComEmbedding ConvertToTextoComEmbedding(VectorSearchResult result)
|
||||||
|
{
|
||||||
|
return new TextoComEmbedding
|
||||||
|
{
|
||||||
|
Id = result.Id,
|
||||||
|
Titulo = result.Title,
|
||||||
|
Conteudo = result.Content,
|
||||||
|
ProjetoId = result.ProjectId,
|
||||||
|
Embedding = result.Embedding,
|
||||||
|
// Campos que podem não existir no Qdrant
|
||||||
|
ProjetoNome = result.Metadata?.GetValueOrDefault("project_name")?.ToString() ?? "",
|
||||||
|
TipoDocumento = result.Metadata?.GetValueOrDefault("document_type")?.ToString() ?? "",
|
||||||
|
Categoria = result.Metadata?.GetValueOrDefault("category")?.ToString() ?? "",
|
||||||
|
Tags = result.Metadata?.GetValueOrDefault("tags") as string[] ?? Array.Empty<string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore SKEXP0001
|
||||||
64
Services/VectorDatabaseHealthCheck.cs
Normal file
64
Services/VectorDatabaseHealthCheck.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services
|
||||||
|
{
|
||||||
|
public class VectorDatabaseHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly IVectorDatabaseFactory _factory;
|
||||||
|
private readonly ILogger<VectorDatabaseHealthCheck> _logger;
|
||||||
|
|
||||||
|
public VectorDatabaseHealthCheck(
|
||||||
|
IVectorDatabaseFactory factory,
|
||||||
|
ILogger<VectorDatabaseHealthCheck> logger)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var provider = _factory.GetActiveProvider();
|
||||||
|
var vectorService = _factory.CreateVectorSearchService();
|
||||||
|
var textService = _factory.CreateTextDataService();
|
||||||
|
|
||||||
|
// Testa conectividade básica
|
||||||
|
var isHealthy = await vectorService.IsHealthyAsync();
|
||||||
|
var stats = await vectorService.GetStatsAsync();
|
||||||
|
var providerStats = await textService.GetProviderStatsAsync();
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = provider,
|
||||||
|
["vector_service_healthy"] = isHealthy,
|
||||||
|
["total_documents"] = stats.GetValueOrDefault("total_documents", 0),
|
||||||
|
["provider_stats"] = providerStats
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHealthy)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Vector Database health check passou para provider {Provider}", provider);
|
||||||
|
return HealthCheckResult.Healthy($"Vector Database ({provider}) está saudável", data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Vector Database health check falhou para provider {Provider}", provider);
|
||||||
|
return HealthCheckResult.Unhealthy($"Vector Database ({provider}) não está saudável", data: data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Erro no health check do Vector Database");
|
||||||
|
return HealthCheckResult.Unhealthy("Erro no Vector Database", ex, new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["provider"] = _factory.GetActiveProvider(),
|
||||||
|
["error"] = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
Settings/ConfidenceAwareSettings.cs
Normal file
120
Settings/ConfidenceAwareSettings.cs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
namespace ChatRAG.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configurações específicas para o ConfidenceAwareRAG
|
||||||
|
/// </summary>
|
||||||
|
public class ConfidenceAwareSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Habilita/desabilita verificação de confiança
|
||||||
|
/// true = só responde com confiança, false = sempre responde (como hoje)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableConfidenceCheck { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modo restrito (critérios rigorosos) vs modo relaxado
|
||||||
|
/// </summary>
|
||||||
|
public bool UseStrictMode { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mostra informações de debug na resposta (confiança, tempo, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowDebugInfo { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Domínio padrão quando não conseguir detectar automaticamente
|
||||||
|
/// </summary>
|
||||||
|
public string DefaultDomain { get; set; } = "TI";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mapeamento de palavras-chave para domínios (detecção automática)
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> 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"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuração de idiomas suportados
|
||||||
|
/// </summary>
|
||||||
|
public LanguageSettings Languages { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configurações de cache para prompts
|
||||||
|
/// </summary>
|
||||||
|
public CacheSettings Cache { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configurações de idioma
|
||||||
|
/// </summary>
|
||||||
|
public class LanguageSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Idioma padrão do sistema
|
||||||
|
/// </summary>
|
||||||
|
public string DefaultLanguage { get; set; } = "pt";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idiomas suportados
|
||||||
|
/// </summary>
|
||||||
|
public List<string> SupportedLanguages { get; set; } = new() { "pt", "en" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-detectar idioma da pergunta
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoDetectLanguage { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sempre responder no idioma detectado/solicitado, mesmo que prompts estejam em PT
|
||||||
|
/// </summary>
|
||||||
|
public bool AlwaysRespondInRequestedLanguage { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Palavras-chave para detecção automática de idioma
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, List<string>> 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" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configurações de cache
|
||||||
|
/// </summary>
|
||||||
|
public class CacheSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Habilitar cache de prompts carregados
|
||||||
|
/// </summary>
|
||||||
|
public bool EnablePromptCache { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tempo de cache em minutos
|
||||||
|
/// </summary>
|
||||||
|
public int CacheExpirationMinutes { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recarregar arquivos automaticamente quando modificados
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoReloadOnFileChange { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
Settings/ConfidenceSettings.cs
Normal file
126
Settings/ConfidenceSettings.cs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configurações para verificação de confiança por estratégia
|
||||||
|
/// </summary>
|
||||||
|
public class ConfidenceSettings
|
||||||
|
{
|
||||||
|
public bool StrictModeByDefault { get; set; } = true;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public Dictionary<string, ConfidenceThresholds> 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mensagens de fallback por idioma
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, ConfidenceFallbackMessages> 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?"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thresholds de confiança para uma estratégia específica
|
||||||
|
/// </summary>
|
||||||
|
public class ConfidenceThresholds
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Número mínimo de documentos encontrados
|
||||||
|
/// </summary>
|
||||||
|
public int MinDocuments { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Número mínimo de documentos relevantes (score >= 0.3)
|
||||||
|
/// </summary>
|
||||||
|
public int MinRelevantDocuments { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Número mínimo de documentos de alta qualidade (score >= 0.6)
|
||||||
|
/// </summary>
|
||||||
|
public int MinHighQualityDocuments { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tamanho mínimo do contexto combinado (caracteres)
|
||||||
|
/// </summary>
|
||||||
|
public int MinContextLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score geral mínimo (0.0 a 1.0)
|
||||||
|
/// </summary>
|
||||||
|
public double MinOverallScore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score máximo mínimo entre os documentos encontrados
|
||||||
|
/// </summary>
|
||||||
|
public double MinMaxScore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score médio mínimo entre os documentos encontrados
|
||||||
|
/// </summary>
|
||||||
|
public double MinAverageScore { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mensagens de fallback quando não há confiança suficiente
|
||||||
|
/// </summary>
|
||||||
|
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; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
178
Settings/VectorDatabaseSettings.cs
Normal file
178
Settings/VectorDatabaseSettings.cs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings.ChatRAG.Configuration
|
||||||
|
{
|
||||||
|
public class VectorDatabaseSettings
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = "Qdrant";
|
||||||
|
public MongoDBSettings? MongoDB { get; set; }
|
||||||
|
public QdrantSettings? Qdrant { get; set; }
|
||||||
|
public ChromaSettings? Chroma { get; set; }
|
||||||
|
public EmbeddingSettings Embedding { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retorna erros de validação
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Provider))
|
||||||
|
errors.Add("Provider é obrigatório");
|
||||||
|
|
||||||
|
switch (Provider.ToLower())
|
||||||
|
{
|
||||||
|
case "mongodb":
|
||||||
|
errors.AddRange(MongoDB.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
case "qdrant":
|
||||||
|
errors.AddRange(Qdrant.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
case "chroma":
|
||||||
|
errors.AddRange(Chroma.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.Add($"Provider '{Provider}' não é suportado");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.AddRange(Embedding.GetValidationErrors());
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MongoDBSettings
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "";
|
||||||
|
public string DatabaseName { get; set; } = "";
|
||||||
|
public string TextCollectionName { get; set; } = "Texts";
|
||||||
|
public string ProjectCollectionName { get; set; } = "Groups";
|
||||||
|
public string UserDataName { get; set; } = "UserData";
|
||||||
|
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||||
|
errors.Add("MongoDB ConnectionString é obrigatória");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DatabaseName))
|
||||||
|
errors.Add("MongoDB DatabaseName é obrigatório");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TextCollectionName))
|
||||||
|
errors.Add("MongoDB TextCollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (ConnectionTimeoutSeconds <= 0)
|
||||||
|
errors.Add("MongoDB ConnectionTimeoutSeconds deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 6334;
|
||||||
|
public string CollectionName { get; set; } = "texts";
|
||||||
|
public string GroupsCollectionName { get; set; } = "projects";
|
||||||
|
public int VectorSize { get; set; } = 384;
|
||||||
|
public string Distance { get; set; } = "Cosine";
|
||||||
|
public int HnswM { get; set; } = 16;
|
||||||
|
public int HnswEfConstruct { get; set; } = 200;
|
||||||
|
public bool OnDisk { get; set; } = false;
|
||||||
|
public bool UseTls { get; set; } = false;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Host))
|
||||||
|
errors.Add("Qdrant Host é obrigatório");
|
||||||
|
|
||||||
|
if (Port <= 0)
|
||||||
|
errors.Add("Qdrant Port deve ser maior que 0");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(CollectionName))
|
||||||
|
errors.Add("Qdrant CollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (VectorSize <= 0)
|
||||||
|
errors.Add("Qdrant VectorSize deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswM <= 0)
|
||||||
|
errors.Add("Qdrant HnswM deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswEfConstruct <= 0)
|
||||||
|
errors.Add("Qdrant HnswEfConstruct deve ser maior que 0");
|
||||||
|
|
||||||
|
var validDistances = new[] { "Cosine", "Euclid", "Dot", "Manhattan" };
|
||||||
|
if (!validDistances.Contains(Distance))
|
||||||
|
errors.Add($"Qdrant Distance deve ser um de: {string.Join(", ", validDistances)}");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 8000;
|
||||||
|
public string CollectionName { get; set; } = "rag_documents";
|
||||||
|
public string ApiVersion { get; set; } = "v1";
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmbeddingSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provider de embedding (OpenAI, Ollama, Azure, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public string Provider { get; set; } = "OpenAI";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modelo de embedding
|
||||||
|
/// </summary>
|
||||||
|
public string Model { get; set; } = "text-embedding-ada-002";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tamanho esperado do embedding
|
||||||
|
/// </summary>
|
||||||
|
public int ExpectedSize { get; set; } = 1536;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Batch size para processamento em lote
|
||||||
|
/// </summary>
|
||||||
|
public int BatchSize { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache de embeddings em memória
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableCache { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TTL do cache em minutos
|
||||||
|
/// </summary>
|
||||||
|
public int CacheTtlMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (ExpectedSize <= 0)
|
||||||
|
errors.Add("Embedding ExpectedSize deve ser maior que 0");
|
||||||
|
|
||||||
|
if (BatchSize <= 0)
|
||||||
|
errors.Add("Embedding BatchSize deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Settings/VectorDatabaseSettingsValidator.cs
Normal file
23
Settings/VectorDatabaseSettingsValidator.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validador para VectorDatabaseSettings
|
||||||
|
/// </summary>
|
||||||
|
public class VectorDatabaseSettingsValidator : IValidateOptions<VectorDatabaseSettings>
|
||||||
|
{
|
||||||
|
public ValidateOptionsResult Validate(string name, VectorDatabaseSettings options)
|
||||||
|
{
|
||||||
|
var errors = options.GetValidationErrors();
|
||||||
|
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
return ValidateOptionsResult.Fail(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateOptionsResult.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
Settings/gckijn3t.ir5~
Normal file
128
Settings/gckijn3t.ir5~
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings.ChatRAG.Configuration
|
||||||
|
{
|
||||||
|
public class VectorDatabaseSettings
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = "Qdrant";
|
||||||
|
public MongoDBSettings? MongoDB { get; set; }
|
||||||
|
public QdrantSettings? Qdrant { get; set; }
|
||||||
|
public ChromaSettings? Chroma { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retorna erros de validação
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Provider))
|
||||||
|
errors.Add("Provider é obrigatório");
|
||||||
|
|
||||||
|
switch (Provider.ToLower())
|
||||||
|
{
|
||||||
|
case "mongodb":
|
||||||
|
errors.AddRange(MongoDB.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
case "qdrant":
|
||||||
|
errors.AddRange(Qdrant.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.Add($"Provider '{Provider}' não é suportado");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.AddRange(Embedding.GetValidationErrors());
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MongoDBSettings
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "";
|
||||||
|
public string DatabaseName { get; set; } = "";
|
||||||
|
public string TextCollectionName { get; set; } = "Texts";
|
||||||
|
public string ProjectCollectionName { get; set; } = "Groups";
|
||||||
|
public string UserDataName { get; set; } = "UserData";
|
||||||
|
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||||
|
errors.Add("MongoDB ConnectionString é obrigatória");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DatabaseName))
|
||||||
|
errors.Add("MongoDB DatabaseName é obrigatório");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TextCollectionName))
|
||||||
|
errors.Add("MongoDB TextCollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (ConnectionTimeoutSeconds <= 0)
|
||||||
|
errors.Add("MongoDB ConnectionTimeoutSeconds deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 6334;
|
||||||
|
public string CollectionName { get; set; } = "texts";
|
||||||
|
public int VectorSize { get; set; } = 384;
|
||||||
|
public string Distance { get; set; } = "Cosine";
|
||||||
|
public int HnswM { get; set; } = 16;
|
||||||
|
public int HnswEfConstruct { get; set; } = 200;
|
||||||
|
public bool OnDisk { get; set; } = false;
|
||||||
|
public bool UseTls { get; set; } = false;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Host))
|
||||||
|
errors.Add("Qdrant Host é obrigatório");
|
||||||
|
|
||||||
|
if (Port <= 0)
|
||||||
|
errors.Add("Qdrant Port deve ser maior que 0");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(CollectionName))
|
||||||
|
errors.Add("Qdrant CollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (VectorSize <= 0)
|
||||||
|
errors.Add("Qdrant VectorSize deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswM <= 0)
|
||||||
|
errors.Add("Qdrant HnswM deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswEfConstruct <= 0)
|
||||||
|
errors.Add("Qdrant HnswEfConstruct deve ser maior que 0");
|
||||||
|
|
||||||
|
var validDistances = new[] { "Cosine", "Euclid", "Dot", "Manhattan" };
|
||||||
|
if (!validDistances.Contains(Distance))
|
||||||
|
errors.Add($"Qdrant Distance deve ser um de: {string.Join(", ", validDistances)}");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 8000;
|
||||||
|
public string CollectionName { get; set; } = "rag_documents";
|
||||||
|
public string ApiVersion { get; set; } = "v1";
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
Settings/ghcutjxi.wn3~
Normal file
120
Settings/ghcutjxi.wn3~
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings.ChatRAG.Configuration
|
||||||
|
{
|
||||||
|
public class VectorDatabaseSettings
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = "Qdrant";
|
||||||
|
public MongoDBSettings? MongoDB { get; set; }
|
||||||
|
public QdrantSettings? Qdrant { get; set; }
|
||||||
|
public ChromaSettings? Chroma { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retorna erros de validação
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Provider))
|
||||||
|
errors.Add("Provider é obrigatório");
|
||||||
|
|
||||||
|
switch (Provider.ToLower())
|
||||||
|
{
|
||||||
|
case "mongodb":
|
||||||
|
errors.AddRange(MongoDB.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
case "qdrant":
|
||||||
|
errors.AddRange(Qdrant.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.Add($"Provider '{Provider}' não é suportado");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.AddRange(Embedding.GetValidationErrors());
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MongoDBSettings
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "";
|
||||||
|
public string DatabaseName { get; set; } = "";
|
||||||
|
public string TextCollectionName { get; set; } = "Texts";
|
||||||
|
public string ProjectCollectionName { get; set; } = "Groups";
|
||||||
|
public string UserDataName { get; set; } = "UserData";
|
||||||
|
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||||
|
errors.Add("MongoDB ConnectionString é obrigatória");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DatabaseName))
|
||||||
|
errors.Add("MongoDB DatabaseName é obrigatório");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TextCollectionName))
|
||||||
|
errors.Add("MongoDB TextCollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (ConnectionTimeoutSeconds <= 0)
|
||||||
|
errors.Add("MongoDB ConnectionTimeoutSeconds deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 6334;
|
||||||
|
public string CollectionName { get; set; } = "texts";
|
||||||
|
public int VectorSize { get; set; } = 384;
|
||||||
|
public string Distance { get; set; } = "Cosine";
|
||||||
|
public int HnswM { get; set; } = 16;
|
||||||
|
public int HnswEfConstruct { get; set; } = 200;
|
||||||
|
public bool OnDisk { get; set; } = false;
|
||||||
|
public bool UseTls { get; set; } = false;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Host))
|
||||||
|
errors.Add("Qdrant Host é obrigatório");
|
||||||
|
|
||||||
|
if (Port <= 0)
|
||||||
|
errors.Add("Qdrant Port deve ser maior que 0");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(CollectionName))
|
||||||
|
errors.Add("Qdrant CollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (VectorSize <= 0)
|
||||||
|
errors.Add("Qdrant VectorSize deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswM <= 0)
|
||||||
|
errors.Add("Qdrant HnswM deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswEfConstruct <= 0)
|
||||||
|
errors.Add("Qdrant HnswEfConstruct deve ser maior que 0");
|
||||||
|
|
||||||
|
var validDistances = new[] { "Cosine", "Euclid", "Dot", "Manhattan" };
|
||||||
|
if (!validDistances.Contains(Distance))
|
||||||
|
errors.Add($"Qdrant Distance deve ser um de: {string.Join(", ", validDistances)}");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 8000;
|
||||||
|
public string CollectionName { get; set; } = "rag_documents";
|
||||||
|
public string ApiVersion { get; set; } = "v1";
|
||||||
|
}
|
||||||
|
}
|
||||||
91
Settings/ixb5gark.btp~
Normal file
91
Settings/ixb5gark.btp~
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings.ChatRAG.Configuration
|
||||||
|
{
|
||||||
|
public class VectorDatabaseSettings
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = "Qdrant";
|
||||||
|
public MongoDBSettings? MongoDB { get; set; }
|
||||||
|
public QdrantSettings? Qdrant { get; set; }
|
||||||
|
public ChromaSettings? Chroma { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retorna erros de validação
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Provider))
|
||||||
|
errors.Add("Provider é obrigatório");
|
||||||
|
|
||||||
|
switch (Provider.ToLower())
|
||||||
|
{
|
||||||
|
case "mongodb":
|
||||||
|
errors.AddRange(MongoDB.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
case "qdrant":
|
||||||
|
errors.AddRange(Qdrant.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.Add($"Provider '{Provider}' não é suportado");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.AddRange(Embedding.GetValidationErrors());
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MongoDBSettings
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "";
|
||||||
|
public string DatabaseName { get; set; } = "";
|
||||||
|
public string TextCollectionName { get; set; } = "Texts";
|
||||||
|
public string ProjectCollectionName { get; set; } = "Groups";
|
||||||
|
public string UserDataName { get; set; } = "UserData";
|
||||||
|
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||||
|
errors.Add("MongoDB ConnectionString é obrigatória");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DatabaseName))
|
||||||
|
errors.Add("MongoDB DatabaseName é obrigatório");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TextCollectionName))
|
||||||
|
errors.Add("MongoDB TextCollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (ConnectionTimeoutSeconds <= 0)
|
||||||
|
errors.Add("MongoDB ConnectionTimeoutSeconds deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 6334;
|
||||||
|
public string CollectionName { get; set; } = "texts";
|
||||||
|
public int VectorSize { get; set; } = 384;
|
||||||
|
public string Distance { get; set; } = "Cosine";
|
||||||
|
public int HnswM { get; set; } = 16;
|
||||||
|
public int HnswEfConstruct { get; set; } = 200;
|
||||||
|
public bool OnDisk { get; set; } = false;
|
||||||
|
public bool UseTls { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 8000;
|
||||||
|
public string CollectionName { get; set; } = "rag_documents";
|
||||||
|
public string ApiVersion { get; set; } = "v1";
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Settings/vwuy0ebd.cjy~
Normal file
174
Settings/vwuy0ebd.cjy~
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using Qdrant.Client.Grpc;
|
||||||
|
|
||||||
|
namespace ChatRAG.Settings.ChatRAG.Configuration
|
||||||
|
{
|
||||||
|
public class VectorDatabaseSettings
|
||||||
|
{
|
||||||
|
public string Provider { get; set; } = "Qdrant";
|
||||||
|
public MongoDBSettings? MongoDB { get; set; }
|
||||||
|
public QdrantSettings? Qdrant { get; set; }
|
||||||
|
public ChromaSettings? Chroma { get; set; }
|
||||||
|
public EmbeddingSettings Embedding { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retorna erros de validação
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Provider))
|
||||||
|
errors.Add("Provider é obrigatório");
|
||||||
|
|
||||||
|
switch (Provider.ToLower())
|
||||||
|
{
|
||||||
|
case "mongodb":
|
||||||
|
errors.AddRange(MongoDB.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
case "qdrant":
|
||||||
|
errors.AddRange(Qdrant.GetValidationErrors());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errors.Add($"Provider '{Provider}' não é suportado");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.AddRange(Embedding.GetValidationErrors());
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MongoDBSettings
|
||||||
|
{
|
||||||
|
public string ConnectionString { get; set; } = "";
|
||||||
|
public string DatabaseName { get; set; } = "";
|
||||||
|
public string TextCollectionName { get; set; } = "Texts";
|
||||||
|
public string ProjectCollectionName { get; set; } = "Groups";
|
||||||
|
public string UserDataName { get; set; } = "UserData";
|
||||||
|
public int ConnectionTimeoutSeconds { get; set; } = 30;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ConnectionString))
|
||||||
|
errors.Add("MongoDB ConnectionString é obrigatória");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(DatabaseName))
|
||||||
|
errors.Add("MongoDB DatabaseName é obrigatório");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TextCollectionName))
|
||||||
|
errors.Add("MongoDB TextCollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (ConnectionTimeoutSeconds <= 0)
|
||||||
|
errors.Add("MongoDB ConnectionTimeoutSeconds deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QdrantSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 6334;
|
||||||
|
public string CollectionName { get; set; } = "texts";
|
||||||
|
public int VectorSize { get; set; } = 384;
|
||||||
|
public string Distance { get; set; } = "Cosine";
|
||||||
|
public int HnswM { get; set; } = 16;
|
||||||
|
public int HnswEfConstruct { get; set; } = 200;
|
||||||
|
public bool OnDisk { get; set; } = false;
|
||||||
|
public bool UseTls { get; set; } = false;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Host))
|
||||||
|
errors.Add("Qdrant Host é obrigatório");
|
||||||
|
|
||||||
|
if (Port <= 0)
|
||||||
|
errors.Add("Qdrant Port deve ser maior que 0");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(CollectionName))
|
||||||
|
errors.Add("Qdrant CollectionName é obrigatório");
|
||||||
|
|
||||||
|
if (VectorSize <= 0)
|
||||||
|
errors.Add("Qdrant VectorSize deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswM <= 0)
|
||||||
|
errors.Add("Qdrant HnswM deve ser maior que 0");
|
||||||
|
|
||||||
|
if (HnswEfConstruct <= 0)
|
||||||
|
errors.Add("Qdrant HnswEfConstruct deve ser maior que 0");
|
||||||
|
|
||||||
|
var validDistances = new[] { "Cosine", "Euclid", "Dot", "Manhattan" };
|
||||||
|
if (!validDistances.Contains(Distance))
|
||||||
|
errors.Add($"Qdrant Distance deve ser um de: {string.Join(", ", validDistances)}");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChromaSettings
|
||||||
|
{
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 8000;
|
||||||
|
public string CollectionName { get; set; } = "rag_documents";
|
||||||
|
public string ApiVersion { get; set; } = "v1";
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmbeddingSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provider de embedding (OpenAI, Ollama, Azure, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public string Provider { get; set; } = "OpenAI";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modelo de embedding
|
||||||
|
/// </summary>
|
||||||
|
public string Model { get; set; } = "text-embedding-ada-002";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tamanho esperado do embedding
|
||||||
|
/// </summary>
|
||||||
|
public int ExpectedSize { get; set; } = 1536;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Batch size para processamento em lote
|
||||||
|
/// </summary>
|
||||||
|
public int BatchSize { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache de embeddings em memória
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableCache { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TTL do cache em minutos
|
||||||
|
/// </summary>
|
||||||
|
public int CacheTtlMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
public List<string> GetValidationErrors()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
if (ExpectedSize <= 0)
|
||||||
|
errors.Add("Embedding ExpectedSize deve ser maior que 0");
|
||||||
|
|
||||||
|
if (BatchSize <= 0)
|
||||||
|
errors.Add("Embedding BatchSize deve ser maior que 0");
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
264
Tools/MigrationService.cs
Normal file
264
Tools/MigrationService.cs
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
using ChatRAG.Contracts.VectorSearch;
|
||||||
|
using ChatRAG.Data;
|
||||||
|
using ChatRAG.Models;
|
||||||
|
using ChatRAG.Services.Contracts;
|
||||||
|
using ChatRAG.Services.Migration;
|
||||||
|
using ChatRAG.Services.SearchVectors;
|
||||||
|
using ChatRAG.Settings.ChatRAG.Configuration;
|
||||||
|
using ChatRAG.Settings;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace ChatRAG.Services.Migration
|
||||||
|
{
|
||||||
|
public class MigrationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<MigrationService> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public MigrationService(
|
||||||
|
ILogger<MigrationService> logger,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migra todos os dados do MongoDB para Qdrant
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MigrationResult> MigrateFromMongoToQdrantAsync(
|
||||||
|
bool validateData = true,
|
||||||
|
int batchSize = 50)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var result = new MigrationResult { StartTime = DateTime.UtcNow };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🚀 Iniciando migração MongoDB → Qdrant...");
|
||||||
|
|
||||||
|
// Cria serviços específicos para migração
|
||||||
|
var mongoService = CreateMongoService();
|
||||||
|
var qdrantService = CreateQdrantService();
|
||||||
|
|
||||||
|
// 1. Exporta dados do MongoDB
|
||||||
|
_logger.LogInformation("📤 Exportando dados do MongoDB...");
|
||||||
|
var mongoDocuments = await mongoService.GetAll();
|
||||||
|
var documentsList = mongoDocuments.ToList();
|
||||||
|
|
||||||
|
result.TotalDocuments = documentsList.Count;
|
||||||
|
_logger.LogInformation("✅ {Count} documentos encontrados no MongoDB", result.TotalDocuments);
|
||||||
|
|
||||||
|
if (!documentsList.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("⚠️ Nenhum documento encontrado no MongoDB");
|
||||||
|
result.Success = true;
|
||||||
|
result.Message = "Migração concluída - nenhum documento para migrar";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Agrupa por projeto para migração organizada
|
||||||
|
var documentsByProject = documentsList.GroupBy(d => d.ProjetoId).ToList();
|
||||||
|
_logger.LogInformation("📁 Documentos organizados em {ProjectCount} projetos", documentsByProject.Count);
|
||||||
|
|
||||||
|
// 3. Migra por projeto em lotes
|
||||||
|
foreach (var projectGroup in documentsByProject)
|
||||||
|
{
|
||||||
|
var projectId = projectGroup.Key;
|
||||||
|
var projectDocs = projectGroup.ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("📂 Migrando projeto {ProjectId}: {DocCount} documentos",
|
||||||
|
projectId, projectDocs.Count);
|
||||||
|
|
||||||
|
// Processa em lotes para não sobrecarregar
|
||||||
|
for (int i = 0; i < projectDocs.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = projectDocs.Skip(i).Take(batchSize).ToList();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MigrateBatch(batch, qdrantService);
|
||||||
|
result.MigratedDocuments += batch.Count;
|
||||||
|
|
||||||
|
_logger.LogDebug("✅ Lote {BatchNum}: {BatchCount} documentos migrados",
|
||||||
|
(i / batchSize) + 1, batch.Count);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "❌ Erro no lote {BatchNum} do projeto {ProjectId}",
|
||||||
|
(i / batchSize) + 1, projectId);
|
||||||
|
|
||||||
|
result.Errors.Add($"Projeto {projectId}, lote {(i / batchSize) + 1}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validação (se solicitada)
|
||||||
|
if (validateData)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🔍 Validando dados migrados...");
|
||||||
|
var validationResult = await ValidateMigration(mongoService, qdrantService);
|
||||||
|
result.ValidationResult = validationResult;
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("⚠️ Validação encontrou inconsistências: {Issues}",
|
||||||
|
string.Join(", ", validationResult.Issues));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("✅ Validação passou - dados consistentes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.Duration = stopwatch.Elapsed;
|
||||||
|
result.Success = true;
|
||||||
|
result.Message = $"Migração concluída: {result.MigratedDocuments}/{result.TotalDocuments} documentos";
|
||||||
|
|
||||||
|
_logger.LogInformation("🎉 Migração concluída em {Duration}s: {MigratedCount}/{TotalCount} documentos",
|
||||||
|
result.Duration.TotalSeconds, result.MigratedDocuments, result.TotalDocuments);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
result.Duration = stopwatch.Elapsed;
|
||||||
|
result.Success = false;
|
||||||
|
result.Message = $"Erro na migração: {ex.Message}";
|
||||||
|
result.Errors.Add(ex.ToString());
|
||||||
|
|
||||||
|
_logger.LogError(ex, "💥 Erro fatal na migração");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rollback - remove todos os dados do Qdrant
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RollbackQdrantAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogWarning("🔄 Iniciando rollback - removendo dados do Qdrant...");
|
||||||
|
|
||||||
|
var qdrantService = CreateQdrantService();
|
||||||
|
|
||||||
|
// Busca todos os documentos
|
||||||
|
var allDocuments = await qdrantService.GetAll();
|
||||||
|
var documentIds = allDocuments.Select(d => d.Id).ToList();
|
||||||
|
|
||||||
|
if (!documentIds.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("ℹ️ Nenhum documento encontrado no Qdrant para rollback");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove em lotes
|
||||||
|
var batchSize = 100;
|
||||||
|
for (int i = 0; i < documentIds.Count; i += batchSize)
|
||||||
|
{
|
||||||
|
var batch = documentIds.Skip(i).Take(batchSize).ToList();
|
||||||
|
await qdrantService.DeleteDocumentsBatchAsync(batch);
|
||||||
|
|
||||||
|
_logger.LogDebug("🗑️ Lote {BatchNum}: {BatchCount} documentos removidos",
|
||||||
|
(i / batchSize) + 1, batch.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("✅ Rollback concluído: {Count} documentos removidos do Qdrant", documentIds.Count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "❌ Erro no rollback");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// MÉTODOS AUXILIARES
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
private async Task MigrateBatch(List<ChatRAG.Models.TextoComEmbedding> batch, ITextDataService qdrantService)
|
||||||
|
{
|
||||||
|
var documents = batch.Select(doc => new DocumentInput
|
||||||
|
{
|
||||||
|
Id = doc.Id,
|
||||||
|
Title = doc.Titulo,
|
||||||
|
Content = doc.Conteudo,
|
||||||
|
ProjectId = doc.ProjetoId,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
Metadata = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["migrated_from"] = "mongodb",
|
||||||
|
["migration_date"] = DateTime.UtcNow.ToString("O"),
|
||||||
|
["original_id"] = doc.Id,
|
||||||
|
["project_name"] = doc.ProjetoNome ?? "",
|
||||||
|
["document_type"] = doc.TipoDocumento ?? "",
|
||||||
|
["category"] = doc.Categoria ?? ""
|
||||||
|
}
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
await qdrantService.SaveDocumentsBatchAsync(documents);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ValidationResult> ValidateMigration(ITextDataService mongoService, ITextDataService qdrantService)
|
||||||
|
{
|
||||||
|
var result = new ValidationResult();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Compara contagens
|
||||||
|
var mongoCount = await mongoService.GetDocumentCountAsync();
|
||||||
|
var qdrantCount = await qdrantService.GetDocumentCountAsync();
|
||||||
|
|
||||||
|
if (mongoCount != qdrantCount)
|
||||||
|
{
|
||||||
|
result.Issues.Add($"Contagem divergente: MongoDB({mongoCount}) vs Qdrant({qdrantCount})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valida alguns documentos aleatoriamente
|
||||||
|
var mongoDocuments = await mongoService.GetAll();
|
||||||
|
var sampleDocs = mongoDocuments.Take(10).ToList();
|
||||||
|
|
||||||
|
foreach (var mongoDoc in sampleDocs)
|
||||||
|
{
|
||||||
|
var qdrantDoc = await qdrantService.GetDocumentAsync(mongoDoc.Id);
|
||||||
|
|
||||||
|
if (qdrantDoc == null)
|
||||||
|
{
|
||||||
|
result.Issues.Add($"Documento {mongoDoc.Id} não encontrado no Qdrant");
|
||||||
|
}
|
||||||
|
else if (qdrantDoc.Title != mongoDoc.Titulo || qdrantDoc.Content != mongoDoc.Conteudo)
|
||||||
|
{
|
||||||
|
result.Issues.Add($"Conteúdo divergente no documento {mongoDoc.Id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.IsValid = !result.Issues.Any();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
result.Issues.Add($"Erro na validação: {ex.Message}");
|
||||||
|
result.IsValid = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ITextDataService CreateMongoService()
|
||||||
|
{
|
||||||
|
// Força usar MongoDB independente da configuração
|
||||||
|
return _serviceProvider.GetRequiredService<ChatApi.Data.TextData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ITextDataService CreateQdrantService()
|
||||||
|
{
|
||||||
|
// Força usar Qdrant independente da configuração
|
||||||
|
return _serviceProvider.GetRequiredService<ChatRAG.Services.TextServices.QdrantTextDataService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Tools/PerformanceTester.cs
Normal file
6
Tools/PerformanceTester.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace ChatRAG.Tools
|
||||||
|
{
|
||||||
|
public class PerformanceTester
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,35 @@
|
|||||||
{
|
{
|
||||||
"DomvsDatabase": {
|
"VectorDatabase": {
|
||||||
//"ConnectionString": "mongodb://192.168.0.82:30017/?directConnection=true",
|
"Provider": "Qdrant",
|
||||||
"ConnectionString": "mongodb://localhost:27017/?directConnection=true",
|
"MongoDB": {
|
||||||
"DatabaseName": "DomvsSites",
|
"ConnectionString": "mongodb://admin:c4rn31r0@k3sw2:27017,k3ss1:27017/?authSource=admin",
|
||||||
"SharepointCollectionName": "SharepointSite",
|
"DatabaseName": "RAGProjects-dev-pt",
|
||||||
"ChatBotRHCollectionName": "ChatBotRHData",
|
"TextCollectionName": "Texts",
|
||||||
"ClassifierCollectionName": "ClassifierData"
|
"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": {
|
"Features": {
|
||||||
"Url": "http://localhost:8070/",
|
"UseQdrant": true,
|
||||||
"Create": "/CallRH"
|
"UseHierarchicalRAG": true,
|
||||||
|
"UseConfidenceAwareRAG": true,
|
||||||
|
"EnableConfidenceCheck": false
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
{
|
{
|
||||||
"DomvsDatabase": {
|
|
||||||
"ConnectionString": "mongodb://admin:c4rn31r0@k3sw2:27017,k3ss1:27017/?authSource=admin",
|
|
||||||
"DatabaseName": "RAGProjects-dev",
|
|
||||||
"TextCollectionName": "Texts",
|
|
||||||
"ProjectCollectionName": "Projects",
|
|
||||||
"UserDataName": "UserData"
|
|
||||||
},
|
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
@ -13,7 +6,90 @@
|
|||||||
"Microsoft.AspNetCore.DataProtection": "None"
|
"Microsoft.AspNetCore.DataProtection": "None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Features": {
|
||||||
|
"UseQdrant": 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": "*",
|
"AllowedHosts": "*",
|
||||||
"AppTenantId": "20190830-5fd4-4a72-b8fd-1c1cb35b25bc",
|
"AppTenantId": "20190830-5fd4-4a72-b8fd-1c1cb35b25bc",
|
||||||
"AppClientID": "8f4248fc-ee30-4f54-8793-66edcca3fd20",
|
"AppClientID": "8f4248fc-ee30-4f54-8793-66edcca3fd20"
|
||||||
}
|
}
|
||||||
|
|||||||
343
vvijr2kk.3vs~
Normal file
343
vvijr2kk.3vs~
Normal file
@ -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 serviço CORS
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowSpecificOrigin",
|
||||||
|
builder =>
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.WithOrigins("http://localhost:5094") // Sua origem específica
|
||||||
|
.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<ConfidenceSettings>(
|
||||||
|
builder.Configuration.GetSection("Confidence"));
|
||||||
|
|
||||||
|
builder.Services.Configure<ConfidenceAwareSettings>(
|
||||||
|
builder.Configuration.GetSection("ConfidenceAware"));
|
||||||
|
|
||||||
|
|
||||||
|
//builder.Services.AddScoped<IVectorSearchService, MongoVectorSearchService>();
|
||||||
|
builder.Services.AddScoped<QdrantVectorSearchService>();
|
||||||
|
builder.Services.AddScoped<MongoVectorSearchService>();
|
||||||
|
builder.Services.AddScoped<ChromaVectorSearchService>();
|
||||||
|
|
||||||
|
builder.Services.AddVectorDatabase(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IVectorSearchService>(provider =>
|
||||||
|
{
|
||||||
|
var useQdrant = builder.Configuration["Features:UseQdrant"] == "true";
|
||||||
|
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
|
||||||
|
return factory.CreateVectorSearchService();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<QdrantProjectDataRepository>();
|
||||||
|
builder.Services.AddScoped<MongoProjectDataRepository>();
|
||||||
|
builder.Services.AddScoped<ChromaProjectDataRepository>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IProjectDataRepository>(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<QdrantProjectDataRepository>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("MongoDB", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<MongoProjectDataRepository>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("Chroma", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<ChromaProjectDataRepository>();
|
||||||
|
}
|
||||||
|
return provider.GetRequiredService<MongoProjectDataRepository>();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<QdrantTextDataService>();
|
||||||
|
builder.Services.AddScoped<MongoTextDataService>();
|
||||||
|
builder.Services.AddScoped<ChromaTextDataService>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ITextDataService>(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<QdrantTextDataService>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("MongoDB", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<MongoTextDataService>();
|
||||||
|
}
|
||||||
|
else if (database.Equals("Chroma", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider.GetRequiredService<ChromaTextDataService>();
|
||||||
|
}
|
||||||
|
return provider.GetRequiredService<MongoTextDataService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ChatHistoryService>();
|
||||||
|
builder.Services.AddScoped<TextDataRepository>();
|
||||||
|
builder.Services.AddSingleton<TextFilter>();
|
||||||
|
|
||||||
|
//builder.Services.AddScoped<IResponseService, ResponseRAGService>();
|
||||||
|
builder.Services.AddScoped<ResponseRAGService>();
|
||||||
|
builder.Services.AddScoped<HierarchicalRAGService>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IResponseService>(provider =>
|
||||||
|
{
|
||||||
|
var configuration = provider.GetService<IConfiguration>();
|
||||||
|
var useHierarchical = configuration?.GetValue<bool>("Features:UseHierarchicalRAG") ?? false;
|
||||||
|
var useConfidence = configuration?.GetValue<bool>("Features:UseConfidenceAwareRAG") ?? false;
|
||||||
|
|
||||||
|
return useConfidence && useHierarchical
|
||||||
|
? provider.GetRequiredService<ConfidenceAwareRAGService>()
|
||||||
|
: useHierarchical
|
||||||
|
? provider.GetRequiredService<HierarchicalRAGService>()
|
||||||
|
: provider.GetRequiredService<ResponseRAGService>();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddTransient<UserDataRepository>();
|
||||||
|
builder.Services.AddTransient<TextData>();
|
||||||
|
builder.Services.AddSingleton<CryptUtil>();
|
||||||
|
|
||||||
|
// Registrar serviços de confiança
|
||||||
|
builder.Services.AddScoped<ConfidenceVerifier>();
|
||||||
|
builder.Services.AddSingleton<PromptConfigurationService>();
|
||||||
|
|
||||||
|
// Registrar ConfidenceAwareRAGService
|
||||||
|
builder.Services.AddScoped<ConfidenceAwareRAGService>();
|
||||||
|
|
||||||
|
//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 =>
|
||||||
|
// {
|
||||||
|
// // Configurações anteriores...
|
||||||
|
|
||||||
|
// // Eventos para log e tratamento de erros
|
||||||
|
// options.Events = new JwtBearerEvents
|
||||||
|
// {
|
||||||
|
// OnAuthenticationFailed = context =>
|
||||||
|
// {
|
||||||
|
// // Log de erros de autenticação
|
||||||
|
// Console.WriteLine($"Erro de autenticação: {context.Exception.Message}");
|
||||||
|
// return Task.CompletedTask;
|
||||||
|
// },
|
||||||
|
// OnTokenValidated = context =>
|
||||||
|
// {
|
||||||
|
// // Validações adicionais se necessário
|
||||||
|
// return Task.CompletedTask;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IConfigurationManager>(builder.Configuration);
|
||||||
|
|
||||||
|
builder.Services.Configure<IISServerOptions>(options =>
|
||||||
|
{
|
||||||
|
options.MaxRequestBodySize = int.MaxValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<KestrelServerOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Limits.MaxRequestBodySize = int.MaxValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<FormOptions>(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<ErrorHandlingMiddleware>();
|
||||||
|
|
||||||
|
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.
|
||||||
Loading…
Reference in New Issue
Block a user