Compare commits

..

No commits in common. "feat/rag-hierarquico" and "main" have entirely different histories.

69 changed files with 94 additions and 12350 deletions

View File

@ -29,7 +29,7 @@ namespace ChatApi
} }
} }
public ChatHistory GetSumarizer(string sessionId, string language = "en") public ChatHistory GetSumarizer(string sessionId)
{ {
if (_keyValues.ContainsKey(sessionId)) if (_keyValues.ContainsKey(sessionId))
{ {
@ -39,12 +39,7 @@ 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;
@ -63,25 +58,5 @@ 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."));
}
} }
} }

View File

@ -26,7 +26,6 @@
<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>

View File

@ -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>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID> <Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/MVC/Controller</Controller_SelectedScaffolderCategoryPath> <Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor> <DebuggerFlavor>ProjectDebugger</DebuggerFlavor>

View File

@ -1,11 +0,0 @@
{
"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."
}
}
}

View File

@ -1,11 +0,0 @@
{
"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."
}
}
}

View File

@ -1,11 +0,0 @@
{
"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."
}
}
}

View File

@ -1,74 +0,0 @@
{
"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."
}
}
}

View File

@ -1,11 +0,0 @@
{
"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."
}
}
}

View File

@ -1,18 +0,0 @@
{
"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."
}
}
}

View File

@ -7,7 +7,6 @@ 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.
@ -21,29 +20,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 IProjectDataRepository _projectDataRepository; private readonly ProjectDataRepository _projectDataRepository;
private readonly ITextDataService _textDataService; private readonly TextData _textData;
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,
ITextDataService textDataService, TextData textData,
IProjectDataRepository projectDataRepository, ProjectDataRepository projectDataRepository,
IHttpClientFactory httpClientFactory) IHttpClientFactory httpClientFactory)
{ {
_logger = logger; _logger = logger;
_responseService = responseService; _responseService = responseService;
_userDataRepository = userDataRepository; _userDataRepository = userDataRepository;
_textDataService = textDataService; _textData = textData;
_projectDataRepository = projectDataRepository; _projectDataRepository = projectDataRepository;
_httpClientFactory = httpClientFactory; this._httpClientFactory = httpClientFactory;
} }
[HttpPost] [HttpPost]
[Route("response")] [Route("response")]
public async Task<IActionResult> GetResponse([FromBody] ChatRequest chatRequest) public async Task<IActionResult> GetResponse([FromForm] ChatRequest chatRequest)
{ {
try try
{ {
@ -62,7 +61,7 @@ namespace ChatApi.Controllers
[Route("savegroup")] [Route("savegroup")]
public async Task SaveSingleProject([FromBody] ProjectRequest project) public async Task SaveSingleProject([FromBody] ProjectRequest project)
{ {
var projectSave = project.MapTo<ProjectRequest, Project>(); var projectSave = project.MapTo<ProjectRequest, Project>();
await _projectDataRepository.SaveAsync(projectSave); await _projectDataRepository.SaveAsync(projectSave);
} }
@ -81,37 +80,7 @@ namespace ChatApi.Controllers
{ {
try try
{ {
await _textDataService.SaveDocumentAsync(new DocumentInput await _textData.SalvarNoMongoDB(request.Id, request.Title, request.Content, request.ProjectId);
{
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)
@ -122,9 +91,9 @@ namespace ChatApi.Controllers
[HttpGet] [HttpGet]
[Route("texts")] [Route("texts")]
public async Task<IEnumerable<TextResponse>> GetTexts(string groupId) public async Task<IEnumerable<TextResponse>> GetTexts()
{ {
var texts = await _textDataService.GetByPorjectId(groupId); var texts = await _textData.GetAll();
return texts.Select(t => { return texts.Select(t => {
return new TextResponse return new TextResponse
{ {
@ -139,7 +108,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 _textDataService.GetById(id); var textItem = await _textData.GetById(id);
return new TextResponse { return new TextResponse {
Id = textItem.Id, Id = textItem.Id,

View File

@ -1,419 +0,0 @@
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

View File

@ -1,225 +0,0 @@
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; }
}
}

View File

@ -1,225 +0,0 @@
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; }
}
}

View File

@ -1,255 +0,0 @@
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; }
}
}

View File

@ -1,255 +0,0 @@
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; }
}
}

View File

@ -1,255 +0,0 @@
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; }
}
}

View File

@ -1,331 +0,0 @@
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; }
}
}

View File

@ -1,178 +0,0 @@
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.

View File

@ -1,27 +1,25 @@
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 MongoProjectDataRepository : IProjectDataRepository public class ProjectDataRepository
{ {
private readonly IMongoCollection<Project> _textsCollection; private readonly IMongoCollection<Project> _textsCollection;
public MongoProjectDataRepository( public ProjectDataRepository(
IOptions<VectorDatabaseSettings> databaseSettings) IOptions<DomvsDatabaseSettings> databaseSettings)
{ {
var mongoClient = new MongoClient( var mongoClient = new MongoClient(
databaseSettings.Value.MongoDB.ConnectionString); databaseSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase( var mongoDatabase = mongoClient.GetDatabase(
databaseSettings.Value.MongoDB.DatabaseName); databaseSettings.Value.DatabaseName);
_textsCollection = mongoDatabase.GetCollection<Project>( _textsCollection = mongoDatabase.GetCollection<Project>(
databaseSettings.Value.MongoDB.ProjectCollectionName); databaseSettings.Value.ProjectCollectionName);
} }
public async Task<List<Project>> GetAsync() => public async Task<List<Project>> GetAsync() =>

View File

@ -1,269 +0,0 @@
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; }
}
}

View File

@ -1,6 +1,5 @@
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;
@ -9,23 +8,17 @@ using System.Text;
namespace ChatApi.Data namespace ChatApi.Data
{ {
public class TextData : ITextDataService public class TextData
{ {
private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService; private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService;
private readonly TextDataRepository _textDataService; private readonly TextDataRepository _textDataService;
public TextData(ITextEmbeddingGenerationService textEmbeddingGenerationService, TextDataRepository textDataService) public TextData(ITextEmbeddingGenerationService textEmbeddingGenerationService, TextDataRepository textDataService)
{ {
_textEmbeddingGenerationService = textEmbeddingGenerationService; _textEmbeddingGenerationService = textEmbeddingGenerationService;
_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>();
@ -54,7 +47,7 @@ namespace ChatApi.Data
} }
} }
foreach (var item in textoArray) foreach(var item in textoArray)
{ {
await SalvarNoMongoDB(title, item, projectId); await SalvarNoMongoDB(title, item, projectId);
} }
@ -62,7 +55,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, projectId); await SalvarNoMongoDB(null, titulo, texto);
} }
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId) public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
@ -74,13 +67,12 @@ 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,
@ -93,14 +85,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);
} }
} }
@ -116,173 +108,8 @@ 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.

View File

@ -1,6 +1,5 @@
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;
@ -12,16 +11,16 @@ namespace ChatRAG.Data
private readonly IMongoCollection<TextoComEmbedding> _textsCollection; private readonly IMongoCollection<TextoComEmbedding> _textsCollection;
public TextDataRepository( public TextDataRepository(
IOptions<VectorDatabaseSettings> vectorStoreDatabaseSettings) IOptions<DomvsDatabaseSettings> bookStoreDatabaseSettings)
{ {
var mongoClient = new MongoClient( var mongoClient = new MongoClient(
vectorStoreDatabaseSettings.Value.MongoDB.ConnectionString); bookStoreDatabaseSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase( var mongoDatabase = mongoClient.GetDatabase(
vectorStoreDatabaseSettings.Value.MongoDB.DatabaseName); bookStoreDatabaseSettings.Value.DatabaseName);
_textsCollection = mongoDatabase.GetCollection<TextoComEmbedding>( _textsCollection = mongoDatabase.GetCollection<TextoComEmbedding>(
vectorStoreDatabaseSettings.Value.MongoDB.TextCollectionName); bookStoreDatabaseSettings.Value.TextCollectionName);
} }
public IMongoCollection<TextoComEmbedding> GetCollection() public IMongoCollection<TextoComEmbedding> GetCollection()
@ -33,7 +32,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 == projectId).ToListAsync(); await _textsCollection.Find(s => s.ProjetoId == ObjectId.Parse(projectId).ToString()).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();

View File

@ -1,5 +1,4 @@
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;
@ -10,16 +9,16 @@ namespace ChatApi
private readonly IMongoCollection<UserData> _userCollection; private readonly IMongoCollection<UserData> _userCollection;
public UserDataRepository( public UserDataRepository(
IOptions<VectorDatabaseSettings> vectorStoreDatabaseSettings) IOptions<DomvsDatabaseSettings> bookStoreDatabaseSettings)
{ {
var mongoClient = new MongoClient( var mongoClient = new MongoClient(
vectorStoreDatabaseSettings.Value.MongoDB.ConnectionString); bookStoreDatabaseSettings.Value.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase( var mongoDatabase = mongoClient.GetDatabase(
vectorStoreDatabaseSettings.Value.MongoDB.DatabaseName); bookStoreDatabaseSettings.Value.DatabaseName);
_userCollection = mongoDatabase.GetCollection<UserData>( _userCollection = mongoDatabase.GetCollection<UserData>(
vectorStoreDatabaseSettings.Value.MongoDB.UserDataName); bookStoreDatabaseSettings.Value.UserDataName);
} }
public async Task<List<UserData>> GetAsync() => public async Task<List<UserData>> GetAsync() =>

View File

@ -1,263 +0,0 @@
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; }
}
}

View File

@ -1,268 +0,0 @@
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; }
}
}

16
DomvsDatabaseSettings.cs Normal file
View File

@ -0,0 +1,16 @@
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!;
}
}

View File

@ -1,100 +0,0 @@
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;
}
}
}

View File

@ -1,43 +0,0 @@
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);
}
}
}

View File

@ -1,70 +0,0 @@
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
};
}
}
}

View File

@ -1,22 +0,0 @@
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();
}
}

View File

@ -1,47 +0,0 @@
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;
}
}

View File

@ -1,144 +0,0 @@
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})";
}
}
}

View File

@ -3,18 +3,9 @@ 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;
@ -25,7 +16,6 @@ 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.
@ -79,134 +69,28 @@ builder.Services.AddSwaggerGen(c =>
}); });
}); });
builder.Services.Configure<ConfidenceSettings>( builder.Services.Configure<DomvsDatabaseSettings>(
builder.Configuration.GetSection("Confidence")); builder.Configuration.GetSection("DomvsDatabase"));
builder.Services.Configure<ConfidenceAwareSettings>( builder.Services.Configure<ChatRHSettings>(
builder.Configuration.GetSection("ConfidenceAware")); builder.Configuration.GetSection("ChatRHSettings"));
//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
//Desktop builder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:11434"));
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"));
@ -219,13 +103,7 @@ builder.Services.AddOpenAIChatCompletion(model, new Uri(url), key);
//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");

View File

@ -1,389 +0,0 @@
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; }
}
}

View File

@ -1,14 +0,0 @@
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);
}
}

View File

@ -1,17 +0,0 @@
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();
}
}

View File

@ -1,143 +0,0 @@
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();
}
}

View File

@ -1,14 +0,0 @@
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();
}
}

View File

@ -1,138 +0,0 @@
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();
}
}

View File

@ -1,753 +0,0 @@
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();
}
}

View File

@ -1,375 +0,0 @@
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.

View File

@ -1,385 +0,0 @@
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

View File

@ -1,572 +0,0 @@
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.

View File

@ -1,10 +1,9 @@
using ChatApi.Models; using ChatApi.Models;
namespace ChatRAG.Services.Contracts namespace ChatRAG.Services.ResponseService
{ {
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");
} }
} }

View File

@ -1,118 +0,0 @@
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.

View File

@ -1,212 +0,0 @@
#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

View File

@ -1,13 +1,10 @@
 
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.
@ -20,19 +17,16 @@ 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 IProjectDataRepository _projectDataRepository; private readonly ProjectDataRepository _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,
IProjectDataRepository projectDataRepository, ProjectDataRepository projectDataRepository,
IChatCompletionService chatCompletionService, IChatCompletionService chatCompletionService)
IVectorSearchService vectorSearchService,
ITextDataService textDataService)
{ {
this._chatHistoryService = chatHistoryService; this._chatHistoryService = chatHistoryService;
this._kernel = kernel; this._kernel = kernel;
@ -40,71 +34,27 @@ 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, string language = "pt") public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
{ {
var stopWatch = new System.Diagnostics.Stopwatch(); var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start(); stopWatch.Start();
var searchStrategy = await ClassificarEstrategiaDeBusca(question, language); //var resposta = await BuscarTextoRelacionado(question);
//var resposta = await BuscarTopTextosRelacionados(question, projectId);
var resposta = await BuscarTopTextosRelacionadosDinamico(question, projectId);
string resposta; var projectData = (await _projectDataRepository.GetAsync()).FirstOrDefault();
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 executionSettings = new OpenAIPromptExecutionSettings var response = await _chatCompletionService.GetChatMessageContentAsync(history);
{
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);
@ -114,68 +64,6 @@ 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>();
@ -200,49 +88,7 @@ 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.";
} }
private async Task<string> BuscarTopTextosRelacionadosComInterface(string pergunta, string projectId) // Adicione esta nova rotina no seu ResponseRAGService
{
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)
{ {
@ -375,18 +221,6 @@ 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
} }
} }

View File

@ -1,572 +0,0 @@
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.

View File

@ -1,651 +0,0 @@
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; }
}
}

View File

@ -1,228 +0,0 @@
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.

View File

@ -1,557 +0,0 @@
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

View File

@ -1,107 +0,0 @@
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;
}
}
}

View File

@ -1,55 +0,0 @@
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;
}
}
}

View File

@ -1,537 +0,0 @@
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.

View File

@ -1,674 +0,0 @@
#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

View File

@ -1,64 +0,0 @@
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
});
}
}
}
}

View File

@ -1,120 +0,0 @@
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;
}
}

View File

@ -1,126 +0,0 @@
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; } = "";
}
}

View File

@ -1,178 +0,0 @@
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;
}
}
}

View File

@ -1,23 +0,0 @@
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;
}
}
}

View File

@ -1,128 +0,0 @@
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;
}
}
}

View File

@ -1,120 +0,0 @@
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";
}
}

View File

@ -1,91 +0,0 @@
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";
}
}

View File

@ -1,174 +0,0 @@
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;
}
}
}

View File

@ -1,264 +0,0 @@
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>();
}
}
}

View File

@ -1,6 +0,0 @@
namespace ChatRAG.Tools
{
public class PerformanceTester
{
}
}

View File

@ -1,35 +1,15 @@
{ {
"VectorDatabase": { "DomvsDatabase": {
"Provider": "Qdrant", //"ConnectionString": "mongodb://192.168.0.82:30017/?directConnection=true",
"MongoDB": { "ConnectionString": "mongodb://localhost:27017/?directConnection=true",
"ConnectionString": "mongodb://admin:c4rn31r0@k3sw2:27017,k3ss1:27017/?authSource=admin", "DatabaseName": "DomvsSites",
"DatabaseName": "RAGProjects-dev-pt", "SharepointCollectionName": "SharepointSite",
"TextCollectionName": "Texts", "ChatBotRHCollectionName": "ChatBotRHData",
"ProjectCollectionName": "Groups", "ClassifierCollectionName": "ClassifierData"
"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": { "ChatRHSettings": {
"UseQdrant": true, "Url": "http://localhost:8070/",
"UseHierarchicalRAG": true, "Create": "/CallRH"
"UseConfidenceAwareRAG": true,
"EnableConfidenceCheck": false
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {

View File

@ -1,4 +1,11 @@
{ {
"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",
@ -6,90 +13,7 @@
"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",
} }

View File

@ -1,343 +0,0 @@
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.