Compare commits

..

5 Commits

Author SHA1 Message Date
Ricardo Carneiro
13083ffb5d feat:qdrant 2025-06-20 22:21:54 -03:00
Ricardo Carneiro
caf50d9d7f fix: unificar settings 2025-06-15 23:03:45 -03:00
Ricardo Carneiro
9a1d75aaf8 feat: RAG cm qdrant 2025-06-15 21:34:47 -03:00
Ricardo Carneiro
94c0395e68 feat: instruções em inglês 2025-06-13 16:20:04 -03:00
Ricardo Carneiro
63b455dc34 feat: en-version 2025-06-12 10:41:33 -03:00
36 changed files with 4411 additions and 74 deletions

View File

@ -29,7 +29,7 @@ namespace ChatApi
} }
} }
public ChatHistory GetSumarizer(string sessionId) public ChatHistory GetSumarizer(string sessionId, string language = "en")
{ {
if (_keyValues.ContainsKey(sessionId)) if (_keyValues.ContainsKey(sessionId))
{ {
@ -39,7 +39,12 @@ namespace ChatApi
else else
{ {
var msg = new List<ChatMessageContent>(); var msg = new List<ChatMessageContent>();
PromptLiliana(msg);
if (language == "pt")
PromptPt(msg);
else
PromptEn(msg);
string json = JsonSerializer.Serialize(msg); string json = JsonSerializer.Serialize(msg);
var history = new ChatHistory(JsonSerializer.Deserialize<List<ChatMessageContent>>(json)); var history = new ChatHistory(JsonSerializer.Deserialize<List<ChatMessageContent>>(json));
_keyValues[sessionId] = history; _keyValues[sessionId] = history;
@ -58,5 +63,25 @@ namespace ChatApi
msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde sempre em português do Brasil e fala sobre detalhes de projeto, arquitetura e criação de casos de teste.")); msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde sempre em português do Brasil e fala sobre detalhes de projeto, arquitetura e criação de casos de teste."));
msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil.")); msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil."));
} }
public void PromptEn(List<ChatMessageContent> msg)
{
msg.Add(new ChatMessageContent(AuthorRole.System, "You are an expert software analyst and QA professional."));
msg.Add(new ChatMessageContent(AuthorRole.System, "Please provide a comprehensive response in English (US). Consider the project context and requirements above to generate accurate and relevant information."));
msg.Add(new ChatMessageContent(AuthorRole.System, "If you have test case requests: Use Gherkin format (Given-When-Then) with realistic scenarios covering happy path, edge cases, and error handling."));
msg.Add(new ChatMessageContent(AuthorRole.System, "If you have project summaries: Include objectives, key features, technologies, and main challenges."));
msg.Add(new ChatMessageContent(AuthorRole.System, "If you have a task list request for one developer: Organize tasks by priority and estimated effort for a single developer, including technical dependencies."));
//msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil."));
}
public void PromptPt(List<ChatMessageContent> msg)
{
msg.Add(new ChatMessageContent(AuthorRole.System, "Você é um analista de software especialista e profissional de QA."));
msg.Add(new ChatMessageContent(AuthorRole.System, "Por favor, forneça uma resposta abrangente em português do Brasil. Considere o contexto do projeto e os requisitos acima para gerar informações precisas e relevantes."));
msg.Add(new ChatMessageContent(AuthorRole.System, "Se forem solicitados casos de teste: Use o formato Gherkin (Dado-Quando-Então) com cenários realistas cobrindo caminho feliz, casos extremos e tratamento de erros."));
msg.Add(new ChatMessageContent(AuthorRole.System, "Se for solicitado um resumo do projeto: Inclua objetivos, principais funcionalidades, tecnologias e principais desafios."));
msg.Add(new ChatMessageContent(AuthorRole.System, "Se for uma solicitação de lista de tarefas para um(ou mais) desenvolvedor(es): Organize as tarefas por prioridade e esforço estimado para um único desenvolvedor, incluindo dependências técnicas."));
//msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre português do Brasil."));
}
} }
} }

View File

@ -26,6 +26,7 @@
<PackageReference Include="MongoDB.Driver" Version="3.0.0" /> <PackageReference Include="MongoDB.Driver" Version="3.0.0" />
<PackageReference Include="MongoDB.Driver.Core" Version="2.30.0" /> <PackageReference Include="MongoDB.Driver.Core" Version="2.30.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Qdrant.Client" Version="1.14.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup> </ItemGroup>

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

View File

@ -7,6 +7,7 @@ using ChatRAG.Data;
using ChatRAG.Models; using ChatRAG.Models;
using ChatRAG.Requests; using ChatRAG.Requests;
using BlazMapper; using BlazMapper;
using ChatRAG.Services.Contracts;
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@ -21,23 +22,23 @@ namespace ChatApi.Controllers
private readonly TextFilter _textFilter; private readonly TextFilter _textFilter;
private readonly UserDataRepository _userDataRepository; private readonly UserDataRepository _userDataRepository;
private readonly ProjectDataRepository _projectDataRepository; private readonly ProjectDataRepository _projectDataRepository;
private readonly TextData _textData; private readonly ITextDataService _textDataService;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
public ChatController( public ChatController(
ILogger<ChatController> logger, ILogger<ChatController> logger,
IResponseService responseService, IResponseService responseService,
UserDataRepository userDataRepository, UserDataRepository userDataRepository,
TextData textData, ITextDataService textDataService,
ProjectDataRepository projectDataRepository, ProjectDataRepository projectDataRepository,
IHttpClientFactory httpClientFactory) IHttpClientFactory httpClientFactory)
{ {
_logger = logger; _logger = logger;
_responseService = responseService; _responseService = responseService;
_userDataRepository = userDataRepository; _userDataRepository = userDataRepository;
_textData = textData; _textDataService = textDataService;
_projectDataRepository = projectDataRepository; _projectDataRepository = projectDataRepository;
this._httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
} }
[HttpPost] [HttpPost]
@ -80,7 +81,37 @@ namespace ChatApi.Controllers
{ {
try try
{ {
await _textData.SalvarNoMongoDB(request.Id, request.Title, request.Content, request.ProjectId); await _textDataService.SaveDocumentAsync(new DocumentInput
{
Id = request.Id,
Title = request.Title,
Content = request.Content,
ProjectId = request.ProjectId
});
return Created();
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
[HttpPost]
[Route("savetexts")]
public async Task<IActionResult> SaveTexts([FromBody] List<TextRequest> requests)
{
try
{
foreach(var request in requests)
{
await _textDataService.SaveDocumentAsync(new DocumentInput
{
Id = request.Id,
Title = request.Title,
Content = request.Content,
ProjectId = request.ProjectId
});
}
return Created(); return Created();
} }
catch (Exception ex) catch (Exception ex)
@ -91,9 +122,9 @@ namespace ChatApi.Controllers
[HttpGet] [HttpGet]
[Route("texts")] [Route("texts")]
public async Task<IEnumerable<TextResponse>> GetTexts() public async Task<IEnumerable<TextResponse>> GetTexts(string groupId)
{ {
var texts = await _textData.GetAll(); var texts = await _textDataService.GetByPorjectId(groupId);
return texts.Select(t => { return texts.Select(t => {
return new TextResponse return new TextResponse
{ {
@ -108,7 +139,7 @@ namespace ChatApi.Controllers
[Route("texts/id/{id}")] [Route("texts/id/{id}")]
public async Task<TextResponse> GetText([FromRoute] string id) public async Task<TextResponse> GetText([FromRoute] string id)
{ {
var textItem = await _textData.GetById(id); var textItem = await _textDataService.GetById(id);
return new TextResponse { return new TextResponse {
Id = textItem.Id, Id = textItem.Id,

View File

@ -0,0 +1,419 @@
using ChatApi.Data;
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Data;
using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using ChatRAG.Services.SearchVectors;
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable SKEXP0001
namespace ChatApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class MigrationController : ControllerBase
{
private readonly IVectorDatabaseFactory _factory;
private readonly ILogger<MigrationController> _logger;
private readonly VectorDatabaseSettings _settings;
private readonly ITextEmbeddingGenerationService _embeddingService;
private readonly TextDataRepository _mongoRepository;
public MigrationController(
IVectorDatabaseFactory factory,
ILogger<MigrationController> logger,
IOptions<VectorDatabaseSettings> settings,
ITextEmbeddingGenerationService embeddingService,
TextDataRepository mongoRepository)
{
_factory = factory;
_logger = logger;
_settings = settings.Value;
_embeddingService = embeddingService;
_mongoRepository = mongoRepository;
}
/// <summary>
/// Status da migração e informações dos providers
/// </summary>
[HttpGet("status")]
public async Task<IActionResult> GetMigrationStatus()
{
try
{
var currentProvider = _factory.GetActiveProvider();
var status = new
{
CurrentProvider = currentProvider,
Settings = new
{
Provider = _settings.Provider,
MongoDB = _settings.MongoDB != null ? new
{
DatabaseName = _settings.MongoDB.DatabaseName,
TextCollection = _settings.MongoDB.TextCollectionName
} : null,
Qdrant = _settings.Qdrant != null ? new
{
Host = _settings.Qdrant.Host,
Port = _settings.Qdrant.Port,
Collection = _settings.Qdrant.CollectionName,
VectorSize = _settings.Qdrant.VectorSize
} : null
},
Stats = await GetProvidersStats()
};
return Ok(status);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao obter status da migração");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Migra dados do MongoDB para Qdrant
/// </summary>
[HttpPost("mongo-to-qdrant")]
public async Task<IActionResult> MigrateMongoToQdrant(
[FromQuery] string? projectId = null,
[FromQuery] int batchSize = 50,
[FromQuery] bool dryRun = false)
{
try
{
if (_settings.Provider != "MongoDB")
{
return BadRequest("Migração só funciona quando o provider atual é MongoDB");
}
_logger.LogInformation("Iniciando migração MongoDB → Qdrant. ProjectId: {ProjectId}, DryRun: {DryRun}",
projectId, dryRun);
var result = await PerformMigration(projectId, batchSize, dryRun);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro durante migração MongoDB → Qdrant");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Migra dados do Qdrant para MongoDB
/// </summary>
[HttpPost("qdrant-to-mongo")]
public async Task<IActionResult> MigrateQdrantToMongo(
[FromQuery] string? projectId = null,
[FromQuery] int batchSize = 50,
[FromQuery] bool dryRun = false)
{
try
{
if (_settings.Provider != "Qdrant")
{
return BadRequest("Migração só funciona quando o provider atual é Qdrant");
}
_logger.LogInformation("Iniciando migração Qdrant → MongoDB. ProjectId: {ProjectId}, DryRun: {DryRun}",
projectId, dryRun);
// TODO: Implementar migração reversa se necessário
return BadRequest("Migração Qdrant → MongoDB ainda não implementada");
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro durante migração Qdrant → MongoDB");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Compara dados entre MongoDB e Qdrant
/// </summary>
[HttpPost("compare")]
public async Task<IActionResult> CompareProviders([FromQuery] string? projectId = null)
{
try
{
// Cria serviços para ambos os providers manualmente
var mongoService = CreateMongoService();
var qdrantService = await CreateQdrantService();
var comparison = await CompareData(mongoService, qdrantService, projectId);
return Ok(comparison);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao comparar providers");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Limpa todos os dados do provider atual
/// </summary>
[HttpDelete("clear-current")]
public async Task<IActionResult> ClearCurrentProvider([FromQuery] string? projectId = null)
{
try
{
var vectorSearchService = _factory.CreateVectorSearchService();
if (string.IsNullOrEmpty(projectId))
{
// Limpar tudo - PERIGOSO!
return BadRequest("Limpeza completa requer confirmação. Use /clear-current/confirm");
}
// Limpar apenas um projeto
var documents = await vectorSearchService.GetDocumentsByProjectAsync(projectId);
var ids = documents.Select(d => d.Id).ToList();
foreach (var id in ids)
{
await vectorSearchService.DeleteDocumentAsync(id);
}
return Ok(new
{
provider = _settings.Provider,
projectId,
deletedCount = ids.Count
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao limpar dados do provider");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Testa conectividade dos providers
/// </summary>
[HttpGet("test-connections")]
public async Task<IActionResult> TestConnections()
{
var results = new Dictionary<string, object>();
// Testar MongoDB
try
{
var mongoService = CreateMongoService();
var mongoHealth = await mongoService.IsHealthyAsync();
var mongoStats = await mongoService.GetStatsAsync();
results["MongoDB"] = new
{
healthy = mongoHealth,
stats = mongoStats
};
}
catch (Exception ex)
{
results["MongoDB"] = new { healthy = false, error = ex.Message };
}
// Testar Qdrant
try
{
var qdrantService = await CreateQdrantService();
var qdrantHealth = await qdrantService.IsHealthyAsync();
var qdrantStats = await qdrantService.GetStatsAsync();
results["Qdrant"] = new
{
healthy = qdrantHealth,
stats = qdrantStats
};
}
catch (Exception ex)
{
results["Qdrant"] = new { healthy = false, error = ex.Message };
}
return Ok(results);
}
// ========================================
// MÉTODOS PRIVADOS
// ========================================
private async Task<object> PerformMigration(string? projectId, int batchSize, bool dryRun)
{
var mongoService = CreateMongoService();
var qdrantService = await CreateQdrantService();
// 1. Buscar dados do MongoDB
List<TextoComEmbedding> documents;
if (string.IsNullOrEmpty(projectId))
{
var allDocs = await _mongoRepository.GetAsync();
documents = allDocs.ToList();
}
else
{
var projectDocs = await _mongoRepository.GetByProjectIdAsync(projectId);
documents = projectDocs.ToList();
}
_logger.LogInformation("Encontrados {Count} documentos para migração", documents.Count);
if (dryRun)
{
return new
{
dryRun = true,
documentsFound = documents.Count,
projects = documents.GroupBy(d => d.ProjetoId).Select(g => new {
projectId = g.Key,
count = g.Count()
}).ToList()
};
}
// 2. Migrar em batches
var migrated = 0;
var errors = new List<string>();
for (int i = 0; i < documents.Count; i += batchSize)
{
var batch = documents.Skip(i).Take(batchSize);
foreach (var doc in batch)
{
try
{
// Verificar se já existe no Qdrant
var exists = await qdrantService.DocumentExistsAsync(doc.Id);
if (exists)
{
_logger.LogDebug("Documento {Id} já existe no Qdrant, pulando", doc.Id);
continue;
}
// Migrar documento
await qdrantService.AddDocumentAsync(
title: doc.Titulo,
content: doc.Conteudo,
projectId: doc.ProjetoId,
embedding: doc.Embedding,
metadata: new Dictionary<string, object>
{
["project_name"] = doc.ProjetoNome ?? "",
["document_type"] = doc.TipoDocumento ?? "",
["category"] = doc.Categoria ?? "",
["tags"] = doc.Tags ?? Array.Empty<string>(),
["migrated_from"] = "MongoDB",
["migrated_at"] = DateTime.UtcNow.ToString("O")
}
);
migrated++;
if (migrated % 10 == 0)
{
_logger.LogInformation("Migrados {Migrated}/{Total} documentos", migrated, documents.Count);
}
}
catch (Exception ex)
{
var error = $"Erro ao migrar documento {doc.Id}: {ex.Message}";
errors.Add(error);
_logger.LogError(ex, error);
}
}
}
return new
{
totalDocuments = documents.Count,
migrated,
errors = errors.Count,
errorDetails = errors.Take(10).ToList(), // Primeiros 10 erros
batchSize,
duration = "Completed"
};
}
private async Task<object> CompareData(
IVectorSearchService mongoService,
IVectorSearchService qdrantService,
string? projectId)
{
var mongoStats = await mongoService.GetStatsAsync();
var qdrantStats = await qdrantService.GetStatsAsync();
var mongoCount = await mongoService.GetDocumentCountAsync(projectId);
var qdrantCount = await qdrantService.GetDocumentCountAsync(projectId);
return new
{
projectId,
comparison = new
{
MongoDB = new
{
documentCount = mongoCount,
healthy = await mongoService.IsHealthyAsync(),
stats = mongoStats
},
Qdrant = new
{
documentCount = qdrantCount,
healthy = await qdrantService.IsHealthyAsync(),
stats = qdrantStats
}
},
differences = new
{
documentCountDiff = qdrantCount - mongoCount,
inSync = mongoCount == qdrantCount
}
};
}
private MongoVectorSearchService CreateMongoService()
{
return new MongoVectorSearchService(_mongoRepository, _embeddingService);
}
private async Task<QdrantVectorSearchService> CreateQdrantService()
{
var qdrantSettings = Microsoft.Extensions.Options.Options.Create(_settings);
var logger = HttpContext.RequestServices.GetService<ILogger<QdrantVectorSearchService>>()!;
return new QdrantVectorSearchService(qdrantSettings, logger);
}
private async Task<Dictionary<string, object>> GetProvidersStats()
{
var stats = new Dictionary<string, object>();
try
{
var currentService = _factory.CreateVectorSearchService();
stats["current"] = await currentService.GetStatsAsync();
}
catch (Exception ex)
{
stats["current"] = new { error = ex.Message };
}
return stats;
}
}
}
#pragma warning restore SKEXP0001

View File

@ -0,0 +1,178 @@
using ChatApi.Data;
using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
namespace ChatRAG.Data
{
public class MongoTextDataService : ITextDataService
{
private readonly TextData _textData; // Sua classe existente!
private readonly ITextEmbeddingGenerationService _embeddingService;
public MongoTextDataService(
TextData textData,
ITextEmbeddingGenerationService embeddingService)
{
_textData = textData;
_embeddingService = embeddingService;
}
public string ProviderName => "MongoDB";
// ========================================
// MÉTODOS ORIGINAIS - Delega para TextData
// ========================================
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
{
await _textData.SalvarNoMongoDB(titulo, texto, projectId);
}
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
{
await _textData.SalvarNoMongoDB(id, titulo, texto, projectId);
}
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
{
await _textData.SalvarTextoComEmbeddingNoMongoDB(textoCompleto, projectId);
}
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
{
return await _textData.GetAll();
}
public async Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId)
{
return await _textData.GetByPorjectId(projectId);
}
public async Task<TextoComEmbedding> GetById(string id)
{
return await _textData.GetById(id);
}
// ========================================
// NOVOS MÉTODOS UNIFICADOS
// ========================================
public async Task<string> SaveDocumentAsync(DocumentInput document)
{
var id = document.Id ?? Guid.NewGuid().ToString();
await _textData.SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
return id;
}
public async Task UpdateDocumentAsync(string id, DocumentInput document)
{
await _textData.SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
}
public async Task DeleteDocumentAsync(string id)
{
// Implementar quando necessário ou usar TextDataRepository diretamente
throw new NotImplementedException("Delete será implementado quando migrar para interface");
}
public async Task<bool> DocumentExistsAsync(string id)
{
try
{
var doc = await _textData.GetById(id);
return doc != null;
}
catch
{
return false;
}
}
public async Task<DocumentOutput?> GetDocumentAsync(string id)
{
try
{
var doc = await _textData.GetById(id);
if (doc == null) return null;
return new DocumentOutput
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
Embedding = doc.Embedding,
CreatedAt = DateTime.UtcNow, // MongoDB não tem essa info no modelo atual
UpdatedAt = DateTime.UtcNow
};
}
catch
{
return null;
}
}
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
{
var docs = await _textData.GetByPorjectId(projectId);
return docs.Select(doc => new DocumentOutput
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
Embedding = doc.Embedding,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}).ToList();
}
public async Task<int> GetDocumentCountAsync(string? projectId = null)
{
if (string.IsNullOrEmpty(projectId))
{
var all = await _textData.GetAll();
return all.Count();
}
else
{
var byProject = await _textData.GetByPorjectId(projectId);
return byProject.Count();
}
}
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
{
var ids = new List<string>();
foreach (var doc in documents)
{
var id = await SaveDocumentAsync(doc);
ids.Add(id);
}
return ids;
}
public async Task DeleteDocumentsBatchAsync(List<string> ids)
{
foreach (var id in ids)
{
await DeleteDocumentAsync(id);
}
}
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
{
var totalDocs = await GetDocumentCountAsync();
return new Dictionary<string, object>
{
["provider"] = "MongoDB",
["total_documents"] = totalDocs,
["health"] = "ok",
["last_check"] = DateTime.UtcNow
};
}
}
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

View File

@ -1,5 +1,6 @@
using ChatApi; using ChatApi;
using ChatRAG.Models; using ChatRAG.Models;
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Driver; using MongoDB.Driver;
@ -10,16 +11,16 @@ namespace ChatRAG.Data
private readonly IMongoCollection<Project> _textsCollection; private readonly IMongoCollection<Project> _textsCollection;
public ProjectDataRepository( public ProjectDataRepository(
IOptions<DomvsDatabaseSettings> databaseSettings) IOptions<VectorDatabaseSettings> databaseSettings)
{ {
var mongoClient = new MongoClient( var mongoClient = new MongoClient(
databaseSettings.Value.ConnectionString); databaseSettings.Value.MongoDB.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase( var mongoDatabase = mongoClient.GetDatabase(
databaseSettings.Value.DatabaseName); databaseSettings.Value.MongoDB.DatabaseName);
_textsCollection = mongoDatabase.GetCollection<Project>( _textsCollection = mongoDatabase.GetCollection<Project>(
databaseSettings.Value.ProjectCollectionName); databaseSettings.Value.MongoDB.ProjectCollectionName);
} }
public async Task<List<Project>> GetAsync() => public async Task<List<Project>> GetAsync() =>

View File

@ -1,5 +1,6 @@
using ChatRAG.Data; using ChatRAG.Data;
using ChatRAG.Models; using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Embeddings;
using System.Text; using System.Text;
@ -8,7 +9,7 @@ using System.Text;
namespace ChatApi.Data namespace ChatApi.Data
{ {
public class TextData public class TextData : ITextDataService
{ {
private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService; private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService;
private readonly TextDataRepository _textDataService; private readonly TextDataRepository _textDataService;
@ -19,6 +20,12 @@ namespace ChatApi.Data
_textDataService = textDataService; _textDataService = textDataService;
} }
public string ProviderName => "MongoDB";
// ========================================
// MÉTODOS ORIGINAIS (já implementados)
// ========================================
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId) public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
{ {
var textoArray = new List<string>(); var textoArray = new List<string>();
@ -55,7 +62,7 @@ namespace ChatApi.Data
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId) public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
{ {
await SalvarNoMongoDB(null, titulo, texto); await SalvarNoMongoDB(null, titulo, texto, projectId);
} }
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId) public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
@ -73,6 +80,7 @@ namespace ChatApi.Data
{ {
var documento = new TextoComEmbedding var documento = new TextoComEmbedding
{ {
Id = id ?? Guid.NewGuid().ToString(),
Titulo = titulo, Titulo = titulo,
Conteudo = texto, Conteudo = texto,
ProjetoId = projectId, ProjetoId = projectId,
@ -85,14 +93,14 @@ namespace ChatApi.Data
{ {
var documento = new TextoComEmbedding var documento = new TextoComEmbedding
{ {
Id = id, Id = id!,
Titulo = titulo, Titulo = titulo,
Conteudo = texto, Conteudo = texto,
ProjetoId = projectId, ProjetoId = projectId,
Embedding = embeddingArray Embedding = embeddingArray
}; };
await _textDataService.UpdateAsync(id, documento); await _textDataService.UpdateAsync(id!, documento);
} }
} }
@ -108,8 +116,173 @@ namespace ChatApi.Data
public async Task<TextoComEmbedding> GetById(string id) public async Task<TextoComEmbedding> GetById(string id)
{ {
return await _textDataService.GetAsync(id); return (await _textDataService.GetAsync(id))!;
}
// ========================================
// MÉTODOS NOVOS DA INTERFACE (implementação completa)
// ========================================
public async Task<string> SaveDocumentAsync(DocumentInput document)
{
var id = document.Id ?? Guid.NewGuid().ToString();
await SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
return id;
}
public async Task UpdateDocumentAsync(string id, DocumentInput document)
{
await SalvarNoMongoDB(id, document.Title, document.Content, document.ProjectId);
}
public async Task DeleteDocumentAsync(string id)
{
await _textDataService.RemoveAsync(id);
}
public async Task<bool> DocumentExistsAsync(string id)
{
try
{
var doc = await GetById(id);
return doc != null;
}
catch
{
return false;
}
}
public async Task<DocumentOutput?> GetDocumentAsync(string id)
{
try
{
var doc = await GetById(id);
if (doc == null) return null;
return new DocumentOutput
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
Embedding = doc.Embedding,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Metadata = new Dictionary<string, object>
{
["source"] = "MongoDB",
["has_embedding"] = doc.Embedding != null,
["embedding_size"] = doc.Embedding?.Length ?? 0
}
};
}
catch
{
return null;
}
}
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
{
var docs = await GetByPorjectId(projectId);
return docs.Select(doc => new DocumentOutput
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
Embedding = doc.Embedding,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Metadata = new Dictionary<string, object>
{
["source"] = "MongoDB",
["has_embedding"] = doc.Embedding != null,
["embedding_size"] = doc.Embedding?.Length ?? 0
}
}).ToList();
}
public async Task<int> GetDocumentCountAsync(string? projectId = null)
{
if (string.IsNullOrEmpty(projectId))
{
var all = await GetAll();
return all.Count();
}
else
{
var byProject = await GetByPorjectId(projectId);
return byProject.Count();
}
}
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
{
var ids = new List<string>();
foreach (var doc in documents)
{
var id = await SaveDocumentAsync(doc);
ids.Add(id);
}
return ids;
}
public async Task DeleteDocumentsBatchAsync(List<string> ids)
{
foreach (var id in ids)
{
await DeleteDocumentAsync(id);
}
}
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
{
try
{
var totalDocs = await GetDocumentCountAsync();
var allDocs = await GetAll();
var docsWithEmbedding = allDocs.Count(d => d.Embedding != null && d.Embedding.Length > 0);
var avgContentLength = allDocs.Any() ? allDocs.Average(d => d.Conteudo?.Length ?? 0) : 0;
var projectStats = allDocs
.GroupBy(d => d.ProjetoId)
.ToDictionary(
g => g.Key ?? "unknown",
g => g.Count()
);
return new Dictionary<string, object>
{
["provider"] = "MongoDB",
["total_documents"] = totalDocs,
["documents_with_embedding"] = docsWithEmbedding,
["embedding_coverage"] = totalDocs > 0 ? (double)docsWithEmbedding / totalDocs : 0,
["average_content_length"] = Math.Round(avgContentLength, 1),
["projects_count"] = projectStats.Count,
["documents_by_project"] = projectStats,
["health"] = "ok",
["last_check"] = DateTime.UtcNow,
["connection_status"] = "connected"
};
}
catch (Exception ex)
{
return new Dictionary<string, object>
{
["provider"] = "MongoDB",
["health"] = "error",
["error"] = ex.Message,
["last_check"] = DateTime.UtcNow,
["connection_status"] = "error"
};
} }
} }
} }
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

View File

@ -1,5 +1,6 @@
using ChatApi; using ChatApi;
using ChatRAG.Models; using ChatRAG.Models;
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -11,16 +12,16 @@ namespace ChatRAG.Data
private readonly IMongoCollection<TextoComEmbedding> _textsCollection; private readonly IMongoCollection<TextoComEmbedding> _textsCollection;
public TextDataRepository( public TextDataRepository(
IOptions<DomvsDatabaseSettings> bookStoreDatabaseSettings) IOptions<VectorDatabaseSettings> vectorStoreDatabaseSettings)
{ {
var mongoClient = new MongoClient( var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString); vectorStoreDatabaseSettings.Value.MongoDB.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase( var mongoDatabase = mongoClient.GetDatabase(
bookStoreDatabaseSettings.Value.DatabaseName); vectorStoreDatabaseSettings.Value.MongoDB.DatabaseName);
_textsCollection = mongoDatabase.GetCollection<TextoComEmbedding>( _textsCollection = mongoDatabase.GetCollection<TextoComEmbedding>(
bookStoreDatabaseSettings.Value.TextCollectionName); vectorStoreDatabaseSettings.Value.MongoDB.TextCollectionName);
} }
public IMongoCollection<TextoComEmbedding> GetCollection() public IMongoCollection<TextoComEmbedding> GetCollection()
@ -32,7 +33,7 @@ namespace ChatRAG.Data
await _textsCollection.Find(_ => true).ToListAsync(); await _textsCollection.Find(_ => true).ToListAsync();
public async Task<List<TextoComEmbedding>> GetByProjectIdAsync(string projectId) => public async Task<List<TextoComEmbedding>> GetByProjectIdAsync(string projectId) =>
await _textsCollection.Find(s => s.ProjetoId == ObjectId.Parse(projectId).ToString()).ToListAsync(); await _textsCollection.Find(s => s.ProjetoId == projectId).ToListAsync();
public async Task<TextoComEmbedding?> GetAsync(string id) => public async Task<TextoComEmbedding?> GetAsync(string id) =>
await _textsCollection.Find(x => x.Id == id).FirstOrDefaultAsync(); await _textsCollection.Find(x => x.Id == id).FirstOrDefaultAsync();

View File

@ -1,4 +1,5 @@
using ChatApi.Models; using ChatApi.Models;
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Driver; using MongoDB.Driver;
@ -9,16 +10,16 @@ namespace ChatApi
private readonly IMongoCollection<UserData> _userCollection; private readonly IMongoCollection<UserData> _userCollection;
public UserDataRepository( public UserDataRepository(
IOptions<DomvsDatabaseSettings> bookStoreDatabaseSettings) IOptions<VectorDatabaseSettings> vectorStoreDatabaseSettings)
{ {
var mongoClient = new MongoClient( var mongoClient = new MongoClient(
bookStoreDatabaseSettings.Value.ConnectionString); vectorStoreDatabaseSettings.Value.MongoDB.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase( var mongoDatabase = mongoClient.GetDatabase(
bookStoreDatabaseSettings.Value.DatabaseName); vectorStoreDatabaseSettings.Value.MongoDB.DatabaseName);
_userCollection = mongoDatabase.GetCollection<UserData>( _userCollection = mongoDatabase.GetCollection<UserData>(
bookStoreDatabaseSettings.Value.UserDataName); vectorStoreDatabaseSettings.Value.MongoDB.UserDataName);
} }
public async Task<List<UserData>> GetAsync() => public async Task<List<UserData>> GetAsync() =>

View File

@ -1,16 +0,0 @@
namespace ChatApi
{
public class DomvsDatabaseSettings
{
public string ConnectionString { get; set; } = null!;
public string DatabaseName { get; set; } = null!;
public string TextCollectionName { get; set; } = null!;
public string UserDataName { get; set; } = null!;
public string ProjectCollectionName { get; set; } = null!;
}
}

View File

@ -0,0 +1,100 @@
using ChatApi.Data;
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Data;
using ChatRAG.Services;
using ChatRAG.Services.Contracts;
using ChatRAG.Services.ResponseService;
using ChatRAG.Services.SearchVectors;
using ChatRAG.Services.TextServices;
using ChatRAG.Settings;
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.Extensions.Options;
using Qdrant.Client;
namespace ChatRAG.Extensions
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registra o sistema completo de Vector Database
/// </summary>
public static IServiceCollection AddVectorDatabase(
this IServiceCollection services,
IConfiguration configuration)
{
// Registra e valida configurações
services.Configure<VectorDatabaseSettings>(
configuration.GetSection("VectorDatabase"));
services.AddSingleton<IValidateOptions<VectorDatabaseSettings>,
ChatRAG.Settings.VectorDatabaseSettingsValidator>();
// Registra factory principal
services.AddScoped<IVectorDatabaseFactory, VectorDatabaseFactory>();
// Registra implementações de todos os providers
services.AddMongoDbProvider();
services.AddQdrantProvider(); // 👈 Agora ativo!
// Registra interfaces principais usando factory
services.AddScoped<IVectorSearchService>(provider =>
{
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
return factory.CreateVectorSearchService();
});
services.AddScoped<ITextDataService>(provider =>
{
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
return factory.CreateTextDataService();
});
services.AddScoped<IResponseService>(provider =>
{
var factory = provider.GetRequiredService<IVectorDatabaseFactory>();
return factory.CreateResponseService();
});
return services;
}
/// <summary>
/// Registra implementações MongoDB (suas classes atuais)
/// </summary>
private static IServiceCollection AddMongoDbProvider(this IServiceCollection services)
{
services.AddScoped<TextData>(); // Implementa ITextDataService
services.AddScoped<TextDataRepository>();
services.AddScoped<ResponseRAGService>(); // Implementa IResponseService
services.AddScoped<MongoVectorSearchService>(); // Wrapper para IVectorSearchService
return services;
}
/// <summary>
/// Registra implementações Qdrant
/// </summary>
private static IServiceCollection AddQdrantProvider(this IServiceCollection services)
{
// ✅ Cliente Qdrant configurado
services.AddScoped<QdrantClient>(provider =>
{
var settings = provider.GetRequiredService<IOptions<VectorDatabaseSettings>>();
var qdrantSettings = settings.Value.Qdrant;
return new QdrantClient(
host: qdrantSettings.Host,
port: qdrantSettings.Port,
https: qdrantSettings.UseTls
);
});
// ✅ Serviços Qdrant
services.AddScoped<QdrantVectorSearchService>();
services.AddScoped<QdrantTextDataService>();
services.AddScoped<QdrantResponseService>();
return services;
}
}
}

43
Models/DocumentInput.cs Normal file
View File

@ -0,0 +1,43 @@
namespace ChatRAG.Models
{
/// <summary>
/// Modelo para entrada de dados (CREATE/UPDATE)
/// </summary>
public class DocumentInput
{
public string? Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string ProjectId { get; set; } = string.Empty;
public Dictionary<string, object>? Metadata { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public static DocumentInput FromTextoComEmbedding(TextoComEmbedding texto)
{
return new DocumentInput
{
Id = texto.Id,
Title = texto.Titulo,
Content = texto.Conteudo,
ProjectId = texto.ProjetoId,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
}
public DocumentInput WithMetadata(string key, object value)
{
Metadata ??= new Dictionary<string, object>();
Metadata[key] = value;
return this;
}
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Title) &&
!string.IsNullOrWhiteSpace(Content) &&
!string.IsNullOrWhiteSpace(ProjectId);
}
}
}

70
Models/DocumentOutput.cs Normal file
View File

@ -0,0 +1,70 @@
namespace ChatRAG.Models
{
/// <summary>
/// Modelo para saída de dados (READ)
/// </summary>
public class DocumentOutput
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string ProjectId { get; set; } = string.Empty;
public Dictionary<string, object>? Metadata { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public double[]? Embedding { get; set; }
public TextoComEmbedding ToTextoComEmbedding()
{
return new TextoComEmbedding
{
Id = Id,
Titulo = Title,
Conteudo = Content,
ProjetoId = ProjectId,
Embedding = Embedding
};
}
public static DocumentOutput FromTextoComEmbedding(TextoComEmbedding texto)
{
return new DocumentOutput
{
Id = texto.Id,
Title = texto.Titulo,
Content = texto.Conteudo,
ProjectId = texto.ProjetoId,
Embedding = texto.Embedding,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
}
public string GetContentPreview(int maxLength = 200)
{
if (string.IsNullOrEmpty(Content))
return string.Empty;
if (Content.Length <= maxLength)
return Content;
return Content.Substring(0, maxLength) + "...";
}
public bool HasEmbedding() => Embedding != null && Embedding.Length > 0;
public DocumentInput ToInput()
{
return new DocumentInput
{
Id = Id,
Title = Title,
Content = Content,
ProjectId = ProjectId,
Metadata = Metadata,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt
};
}
}
}

22
Models/MigrationResult.cs Normal file
View File

@ -0,0 +1,22 @@
namespace ChatRAG.Models
{
public class MigrationResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public DateTime StartTime { get; set; }
public TimeSpan Duration { get; set; }
public int TotalDocuments { get; set; }
public int MigratedDocuments { get; set; }
public List<string> Errors { get; set; } = new();
public ValidationResult? ValidationResult { get; set; }
public double SuccessRate => TotalDocuments > 0 ? (double)MigratedDocuments / TotalDocuments : 0;
}
public class ValidationResult
{
public bool IsValid { get; set; }
public List<string> Issues { get; set; } = new();
}
}

47
Models/Models.cs Normal file
View File

@ -0,0 +1,47 @@
namespace ChatRAG.Models
{
public class ResponseOptions
{
public int MaxContextDocuments { get; set; } = 3;
public double SimilarityThreshold { get; set; } = 0.3;
public bool IncludeSourceDetails { get; set; } = false;
public bool IncludeTiming { get; set; } = true;
public Dictionary<string, object>? Filters { get; set; }
}
public class ResponseResult
{
public string Content { get; set; } = string.Empty;
public List<SourceDocument> Sources { get; set; } = new();
public ResponseMetrics Metrics { get; set; } = new();
public string Provider { get; set; } = string.Empty;
}
public class SourceDocument
{
public string Id { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public double Similarity { get; set; }
public Dictionary<string, object>? Metadata { get; set; }
}
public class ResponseMetrics
{
public long TotalTimeMs { get; set; }
public long SearchTimeMs { get; set; }
public long LlmTimeMs { get; set; }
public int DocumentsFound { get; set; }
public int DocumentsUsed { get; set; }
public double AverageSimilarity { get; set; }
}
public class ResponseStats
{
public int TotalRequests { get; set; }
public double AverageResponseTime { get; set; }
public Dictionary<string, int> RequestsByProject { get; set; } = new();
public DateTime LastRequest { get; set; }
public string Provider { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,144 @@
namespace ChatRAG.Models
{
/// <summary>
/// Resultado padronizado de busca vetorial
/// Funciona com qualquer provider (MongoDB, Qdrant, etc.)
/// </summary>
public class VectorSearchResult
{
/// <summary>
/// ID único do documento
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Título do documento
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Conteúdo completo do documento
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// ID do projeto ao qual pertence
/// </summary>
public string ProjectId { get; set; } = string.Empty;
/// <summary>
/// Score de similaridade (0.0 a 1.0, onde 1.0 é idêntico)
/// </summary>
public double Score { get; set; }
/// <summary>
/// Embedding vetorial (opcional - nem sempre retornado por performance)
/// </summary>
public double[]? Embedding { get; set; }
/// <summary>
/// Metadados adicionais (tags, categoria, autor, etc.)
/// </summary>
public Dictionary<string, object>? Metadata { get; set; }
/// <summary>
/// Data de criação do documento
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Data da última atualização
/// </summary>
public DateTime UpdatedAt { get; set; }
// ========================================
// INFORMAÇÕES DO PROVIDER
// ========================================
/// <summary>
/// Nome do provider que retornou este resultado (MongoDB, Qdrant, etc.)
/// </summary>
public string Provider { get; set; } = string.Empty;
/// <summary>
/// Informações específicas do provider (índices, shards, etc.)
/// </summary>
public Dictionary<string, object>? ProviderSpecific { get; set; }
// ========================================
// MÉTODOS DE CONVENIÊNCIA
// ========================================
/// <summary>
/// Preview do conteúdo (primeiros N caracteres)
/// </summary>
public string GetContentPreview(int maxLength = 200)
{
if (string.IsNullOrEmpty(Content))
return string.Empty;
if (Content.Length <= maxLength)
return Content;
return Content.Substring(0, maxLength) + "...";
}
/// <summary>
/// Score formatado como percentual
/// </summary>
public string GetScorePercentage()
{
return $"{Score:P1}"; // Ex: "85.3%"
}
/// <summary>
/// Indica se é um resultado relevante (score alto)
/// </summary>
public bool IsHighRelevance(double threshold = 0.7)
{
return Score >= threshold;
}
/// <summary>
/// Converte para o modelo atual do sistema (compatibilidade)
/// </summary>
public TextoComEmbedding ToTextoComEmbedding()
{
return new TextoComEmbedding
{
Id = Id,
Titulo = Title,
Conteudo = Content,
ProjetoId = ProjectId,
Embedding = Embedding
};
}
/// <summary>
/// Converte do modelo atual do sistema
/// </summary>
public static VectorSearchResult FromTextoComEmbedding(
TextoComEmbedding texto,
double score = 1.0,
string provider = "Unknown")
{
return new VectorSearchResult
{
Id = texto.Id,
Title = texto.Titulo,
Content = texto.Conteudo,
ProjectId = texto.ProjetoId,
Score = score,
Embedding = texto.Embedding,
Provider = provider,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
}
public override string ToString()
{
return $"{Title} (Score: {GetScorePercentage()}, Provider: {Provider})";
}
}
}

View File

@ -3,9 +3,14 @@ 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.Contracts;
using ChatRAG.Services.ResponseService; using ChatRAG.Services.ResponseService;
using ChatRAG.Services.SearchVectors;
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
@ -16,6 +21,7 @@ using Microsoft.SemanticKernel;
using System.Text; using System.Text;
using static OllamaSharp.OllamaApiClient; using static OllamaSharp.OllamaApiClient;
using static System.Net.Mime.MediaTypeNames; using static System.Net.Mime.MediaTypeNames;
using static System.Net.WebRequestMethods;
#pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@ -69,12 +75,20 @@ builder.Services.AddSwaggerGen(c =>
}); });
}); });
builder.Services.Configure<DomvsDatabaseSettings>(
builder.Configuration.GetSection("DomvsDatabase"));
builder.Services.Configure<ChatRHSettings>( builder.Services.Configure<ChatRHSettings>(
builder.Configuration.GetSection("ChatRHSettings")); builder.Configuration.GetSection("ChatRHSettings"));
builder.Services.AddScoped<IVectorSearchService, MongoVectorSearchService>();
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.AddSingleton<ChatHistoryService>(); builder.Services.AddSingleton<ChatHistoryService>();
builder.Services.AddScoped<TextDataRepository>(); builder.Services.AddScoped<TextDataRepository>();
builder.Services.AddScoped<ProjectDataRepository>(); builder.Services.AddScoped<ProjectDataRepository>();
@ -90,7 +104,16 @@ builder.Services.AddSingleton<CryptUtil>();
//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin"); //var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin");
//Olllama //Olllama
builder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:11434")); //Desktop
//builder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:11434"));
//Notebook
var model = "meta-llama/Llama-3.2-3B-Instruct";
var url = "https://api.deepinfra.com/v1/openai"; // Adicione o /v1/openai
builder.Services.AddOpenAIChatCompletion(model, new Uri(url), "HedaR4yPrp9N2XSHfwdZjpZvPIxejPFK");
//builder.Services.AddOllamaChatCompletion("llama3.2:3b", new Uri("http://localhost:11435"));
//builder.Services.AddOllamaChatCompletion("llama3.2:1b", new Uri("http://localhost:11435"));
//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); //builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435"));
//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435")); //builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435"));
@ -103,7 +126,13 @@ builder.Services.AddOllamaChatCompletion("llama3.2", new Uri("http://localhost:1
//builder.Services.AddOllamaChatCompletion("llama3.1:latest", new Uri("http://192.168.0.150:11434")); //builder.Services.AddOllamaChatCompletion("llama3.1:latest", new Uri("http://192.168.0.150:11434"));
//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://192.168.0.150:11434")); //builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://192.168.0.150:11434"));
builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11434"));
//Desktop
//builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11434"));
//Notebook
builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435"));
//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); //builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435"));
//builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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"); //builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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");
//builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8"); //builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8");

View File

@ -1,9 +1,10 @@
using ChatApi.Models; using ChatApi.Models;
namespace ChatRAG.Services.ResponseService namespace ChatRAG.Services.Contracts
{ {
public interface IResponseService public interface IResponseService
{ {
Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question); Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question);
Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt");
} }
} }

View File

@ -0,0 +1,17 @@
using ChatApi.Models;
using ChatRAG.Models;
namespace ChatRAG.Services.Contracts
{
public interface IResponseServiceExtended : IResponseService
{
Task<ResponseResult> GetResponseDetailed(
UserData userData,
string projectId,
string sessionId,
string question,
ResponseOptions? options = null);
Task<ResponseStats> GetStatsAsync();
}
}

View File

@ -0,0 +1,143 @@
using ChatRAG.Models;
namespace ChatRAG.Services.Contracts
{
/// <summary>
/// Interface unificada para operações de documentos de texto.
/// Permite alternar entre MongoDB, Qdrant, ou outros providers sem quebrar código.
/// </summary>
public interface ITextDataService
{
// ========================================
// MÉTODOS ORIGINAIS (compatibilidade com TextData.cs atual)
// ========================================
/// <summary>
/// Salva texto no banco (método original do seu TextData.cs)
/// </summary>
/// <param name="titulo">Título do documento</param>
/// <param name="texto">Conteúdo do documento</param>
/// <param name="projectId">ID do projeto</param>
Task SalvarNoMongoDB(string titulo, string texto, string projectId);
/// <summary>
/// Salva ou atualiza texto com ID específico (método original)
/// </summary>
/// <param name="id">ID do documento (null para criar novo)</param>
/// <param name="titulo">Título do documento</param>
/// <param name="texto">Conteúdo do documento</param>
/// <param name="projectId">ID do projeto</param>
Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId);
/// <summary>
/// Processa texto completo dividindo por seções (método original)
/// </summary>
/// <param name="textoCompleto">Texto com divisões marcadas por **</param>
/// <param name="projectId">ID do projeto</param>
Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId);
/// <summary>
/// Recupera todos os documentos (método original)
/// </summary>
/// <returns>Lista de todos os documentos</returns>
Task<IEnumerable<TextoComEmbedding>> GetAll();
/// <summary>
/// Recupera documentos por projeto (método original)
/// </summary>
/// <param name="projectId">ID do projeto</param>
/// <returns>Lista de documentos do projeto</returns>
Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId);
/// <summary>
/// Recupera documento por ID (método original)
/// </summary>
/// <param name="id">ID do documento</param>
/// <returns>Documento ou null se não encontrado</returns>
Task<TextoComEmbedding> GetById(string id);
// ========================================
// MÉTODOS NOVOS (interface moderna e unificada)
// ========================================
/// <summary>
/// Salva documento usando modelo unificado
/// </summary>
/// <param name="document">Dados do documento</param>
/// <returns>ID do documento criado</returns>
Task<string> SaveDocumentAsync(DocumentInput document);
/// <summary>
/// Atualiza documento existente
/// </summary>
/// <param name="id">ID do documento</param>
/// <param name="document">Novos dados do documento</param>
Task UpdateDocumentAsync(string id, DocumentInput document);
/// <summary>
/// Remove documento
/// </summary>
/// <param name="id">ID do documento</param>
Task DeleteDocumentAsync(string id);
/// <summary>
/// Verifica se documento existe
/// </summary>
/// <param name="id">ID do documento</param>
/// <returns>True se existe, False caso contrário</returns>
Task<bool> DocumentExistsAsync(string id);
/// <summary>
/// Recupera documento por ID (formato moderno)
/// </summary>
/// <param name="id">ID do documento</param>
/// <returns>Documento ou null se não encontrado</returns>
Task<DocumentOutput?> GetDocumentAsync(string id);
/// <summary>
/// Lista documentos por projeto (formato moderno)
/// </summary>
/// <param name="projectId">ID do projeto</param>
/// <returns>Lista de documentos do projeto</returns>
Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId);
/// <summary>
/// Conta documentos
/// </summary>
/// <param name="projectId">Filtrar por projeto (opcional)</param>
/// <returns>Número de documentos</returns>
Task<int> GetDocumentCountAsync(string? projectId = null);
// ========================================
// OPERAÇÕES EM LOTE
// ========================================
/// <summary>
/// Salva múltiplos documentos de uma vez
/// </summary>
/// <param name="documents">Lista de documentos</param>
/// <returns>Lista de IDs dos documentos criados</returns>
Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents);
/// <summary>
/// Remove múltiplos documentos de uma vez
/// </summary>
/// <param name="ids">Lista de IDs para remover</param>
Task DeleteDocumentsBatchAsync(List<string> ids);
// ========================================
// INFORMAÇÕES DO PROVIDER
// ========================================
/// <summary>
/// Nome do provider (MongoDB, Qdrant, etc.)
/// </summary>
string ProviderName { get; }
/// <summary>
/// Estatísticas e métricas do provider
/// </summary>
/// <returns>Informações sobre performance, saúde, etc.</returns>
Task<Dictionary<string, object>> GetProviderStatsAsync();
}
}

View File

@ -0,0 +1,14 @@
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Settings.ChatRAG.Configuration;
namespace ChatRAG.Services.Contracts
{
public interface IVectorDatabaseFactory
{
IVectorSearchService CreateVectorSearchService();
ITextDataService CreateTextDataService();
IResponseService CreateResponseService();
string GetActiveProvider();
VectorDatabaseSettings GetSettings();
}
}

View File

@ -0,0 +1,138 @@
using ChatRAG.Models;
using Microsoft.Extensions.VectorData;
namespace ChatRAG.Contracts.VectorSearch
{
/// <summary>
/// Interface unificada para operações de busca vetorial.
/// Pode ser implementada por MongoDB, Qdrant, Pinecone, etc.
/// </summary>
public interface IVectorSearchService
{
// ========================================
// BUSCA VETORIAL
// ========================================
/// <summary>
/// Busca documentos similares usando embedding vetorial
/// </summary>
/// <param name="queryEmbedding">Embedding da query (ex: 1536 dimensões para OpenAI)</param>
/// <param name="projectId">Filtrar por projeto específico (opcional)</param>
/// <param name="threshold">Score mínimo de similaridade (0.0 a 1.0)</param>
/// <param name="limit">Número máximo de resultados</param>
/// <param name="filters">Filtros adicionais (metadata, data, etc.)</param>
/// <returns>Lista de documentos ordenados por similaridade</returns>
Task<List<VectorSearchResult>> SearchSimilarAsync(
double[] queryEmbedding,
string? projectId = null,
double threshold = 0.3,
int limit = 5,
Dictionary<string, object>? filters = null);
/// <summary>
/// Busca adaptativa - relaxa threshold se não encontrar resultados suficientes
/// (Implementa a mesma lógica do seu ResponseRAGService atual)
/// </summary>
/// <param name="queryEmbedding">Embedding da query</param>
/// <param name="projectId">ID do projeto</param>
/// <param name="minThreshold">Threshold inicial (será reduzido se necessário)</param>
/// <param name="limit">Número máximo de resultados</param>
/// <returns>Lista de documentos com busca adaptativa</returns>
Task<List<VectorSearchResult>> SearchSimilarDynamicAsync(
double[] queryEmbedding,
string projectId,
double minThreshold = 0.5,
int limit = 5);
// ========================================
// CRUD DE DOCUMENTOS
// ========================================
/// <summary>
/// Adiciona um novo documento com embedding
/// </summary>
/// <param name="title">Título do documento</param>
/// <param name="content">Conteúdo do documento</param>
/// <param name="projectId">ID do projeto</param>
/// <param name="embedding">Embedding pré-calculado</param>
/// <param name="metadata">Metadados adicionais (tags, data, autor, etc.)</param>
/// <returns>ID do documento criado</returns>
Task<string> AddDocumentAsync(
string title,
string content,
string projectId,
double[] embedding,
Dictionary<string, object>? metadata = null);
/// <summary>
/// Atualiza um documento existente
/// </summary>
/// <param name="id">ID do documento</param>
/// <param name="title">Novo título</param>
/// <param name="content">Novo conteúdo</param>
/// <param name="projectId">ID do projeto</param>
/// <param name="embedding">Novo embedding</param>
/// <param name="metadata">Novos metadados</param>
Task UpdateDocumentAsync(
string id,
string title,
string content,
string projectId,
double[] embedding,
Dictionary<string, object>? metadata = null);
/// <summary>
/// Remove um documento
/// </summary>
/// <param name="id">ID do documento</param>
Task DeleteDocumentAsync(string id);
// ========================================
// CONSULTAS AUXILIARES
// ========================================
/// <summary>
/// Verifica se um documento existe
/// </summary>
/// <param name="id">ID do documento</param>
/// <returns>True se existe, False caso contrário</returns>
Task<bool> DocumentExistsAsync(string id);
/// <summary>
/// Recupera um documento específico por ID
/// </summary>
/// <param name="id">ID do documento</param>
/// <returns>Documento ou null se não encontrado</returns>
Task<VectorSearchResult?> GetDocumentAsync(string id);
/// <summary>
/// Lista todos os documentos de um projeto
/// </summary>
/// <param name="projectId">ID do projeto</param>
/// <returns>Lista de documentos do projeto</returns>
Task<List<VectorSearchResult>> GetDocumentsByProjectAsync(string projectId);
/// <summary>
/// Conta total de documentos
/// </summary>
/// <param name="projectId">Filtrar por projeto (opcional)</param>
/// <returns>Número de documentos</returns>
Task<int> GetDocumentCountAsync(string? projectId = null);
// ========================================
// HEALTH CHECK E MÉTRICAS
// ========================================
/// <summary>
/// Verifica se o serviço está saudável
/// </summary>
/// <returns>True se está funcionando, False caso contrário</returns>
Task<bool> IsHealthyAsync();
/// <summary>
/// Retorna estatísticas e métricas do provider
/// </summary>
/// <returns>Dicionário com estatísticas (documentos, performance, etc.)</returns>
Task<Dictionary<string, object>> GetStatsAsync();
}
}

View File

@ -0,0 +1,118 @@
using ChatApi.Models;
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
namespace ChatRAG.Services.ResponseService
{
public class MongoResponseService : IResponseService
{
private readonly ResponseRAGService _originalService; // Sua classe atual!
private readonly IVectorSearchService _vectorSearchService;
private readonly ITextEmbeddingGenerationService _embeddingService;
private readonly TextFilter _textFilter;
public MongoResponseService(
ResponseRAGService originalService,
IVectorSearchService vectorSearchService,
ITextEmbeddingGenerationService embeddingService,
TextFilter textFilter)
{
_originalService = originalService;
_vectorSearchService = vectorSearchService;
_embeddingService = embeddingService;
_textFilter = textFilter;
}
public string ProviderName => "MongoDB";
// ========================================
// MÉTODO ORIGINAL - Delega para ResponseRAGService
// ========================================
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language="pt")
{
return await _originalService.GetResponse(userData, projectId, sessionId, question);
}
// ========================================
// MÉTODO ESTENDIDO COM MAIS DETALHES
// ========================================
public async Task<ResponseResult> GetResponseDetailed(
UserData userData,
string projectId,
string sessionId,
string question,
ResponseOptions? options = null)
{
options ??= new ResponseOptions();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Gera embedding da pergunta
var embeddingPergunta = await _embeddingService.GenerateEmbeddingAsync(
_textFilter.ToLowerAndWithoutAccents(question));
var embeddingArray = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
var searchStart = stopwatch.ElapsedMilliseconds;
// Busca documentos similares usando a interface
var documentos = await _vectorSearchService.SearchSimilarDynamicAsync(
embeddingArray,
projectId,
options.SimilarityThreshold,
options.MaxContextDocuments);
var searchTime = stopwatch.ElapsedMilliseconds - searchStart;
var llmStart = stopwatch.ElapsedMilliseconds;
// Chama o método original para gerar resposta
var response = await _originalService.GetResponse(userData, projectId, sessionId, question);
var llmTime = stopwatch.ElapsedMilliseconds - llmStart;
stopwatch.Stop();
// Monta resultado detalhado
return new ResponseResult
{
Content = response,
Provider = "MongoDB",
Sources = documentos.Select(d => new SourceDocument
{
Id = d.Id,
Title = d.Title,
Content = d.Content,
Similarity = d.Score,
Metadata = d.Metadata
}).ToList(),
Metrics = new ResponseMetrics
{
TotalTimeMs = stopwatch.ElapsedMilliseconds,
SearchTimeMs = searchTime,
LlmTimeMs = llmTime,
DocumentsFound = documentos.Count,
DocumentsUsed = documentos.Count,
AverageSimilarity = documentos.Any() ? documentos.Average(d => d.Score) : 0
}
};
}
public async Task<ResponseStats> GetStatsAsync()
{
// Implementação básica - pode ser expandida
return new ResponseStats
{
TotalRequests = 0,
AverageResponseTime = 0,
RequestsByProject = new Dictionary<string, int>(),
LastRequest = DateTime.UtcNow
};
}
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
{
return this.GetResponse(userData, projectId, sessionId, question, "pt");
}
}
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

View File

@ -0,0 +1,212 @@
#pragma warning disable SKEXP0001
using ChatApi.Models;
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Embeddings;
namespace ChatRAG.Services.ResponseService
{
public class QdrantResponseService : IResponseService
{
private readonly IVectorSearchService _vectorSearchService;
private readonly ITextEmbeddingGenerationService _embeddingService;
private readonly IChatCompletionService _chatService;
private readonly ILogger<QdrantResponseService> _logger;
public QdrantResponseService(
IVectorSearchService vectorSearchService,
ITextEmbeddingGenerationService embeddingService,
IChatCompletionService chatService,
ILogger<QdrantResponseService> logger)
{
_vectorSearchService = vectorSearchService;
_embeddingService = embeddingService;
_chatService = chatService;
_logger = logger;
}
public string ProviderName => "Qdrant";
public async Task<string> GetResponse(
UserData userData,
string projectId,
string sessionId,
string userMessage,
string language = "pt")
{
try
{
_logger.LogInformation("Processando consulta RAG com Qdrant para projeto {ProjectId}", projectId);
// 1. Gerar embedding da pergunta do usuário
var questionEmbedding = await _embeddingService.GenerateEmbeddingAsync(userMessage);
var embeddingArray = questionEmbedding.ToArray().Select(e => (double)e).ToArray();
// 2. Buscar documentos similares no Qdrant
var searchResults = await _vectorSearchService.SearchSimilarDynamicAsync(
queryEmbedding: embeddingArray,
projectId: projectId,
minThreshold: 0.5,
limit: 5
);
// 3. Construir contexto a partir dos resultados
var context = BuildContextFromResults(searchResults);
// 4. Criar prompt com contexto
var prompt = BuildRagPrompt(userMessage, context);
// 5. Gerar resposta usando LLM
var response = await _chatService.GetChatMessageContentAsync(prompt);
_logger.LogDebug("Resposta RAG gerada com {ResultCount} documentos do Qdrant",
searchResults.Count);
return response.Content ?? "Desculpe, não foi possível gerar uma resposta.";
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao processar consulta RAG com Qdrant");
return "Ocorreu um erro ao processar sua consulta. Tente novamente.";
}
}
public async Task<string> GetResponseWithHistory(
UserData userData,
string projectId,
string sessionId,
string userMessage,
List<string> conversationHistory)
{
try
{
// Combina histórico com mensagem atual para melhor contexto
var enhancedMessage = BuildEnhancedMessageWithHistory(userMessage, conversationHistory);
return await GetResponse(userData, projectId, sessionId, enhancedMessage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao processar consulta RAG com histórico");
return "Ocorreu um erro ao processar sua consulta. Tente novamente.";
}
}
// ========================================
// MÉTODOS AUXILIARES PRIVADOS
// ========================================
private string BuildContextFromResults(List<VectorSearchResult> results)
{
if (!results.Any())
{
return "Nenhum documento relevante encontrado.";
}
var contextBuilder = new System.Text.StringBuilder();
contextBuilder.AppendLine("=== CONTEXTO DOS DOCUMENTOS ===");
foreach (var result in results.Take(5)) // Limita a 5 documentos
{
contextBuilder.AppendLine($"\n--- Documento: {result.Title} (Relevância: {result.GetScorePercentage()}) ---");
contextBuilder.AppendLine(result.Content);
contextBuilder.AppendLine();
}
return contextBuilder.ToString();
}
private string BuildRagPrompt(string userQuestion, string context)
{
return $@"
Você é um assistente especializado que responde perguntas baseado nos documentos fornecidos.
CONTEXTO DOS DOCUMENTOS:
{context}
PERGUNTA DO USUÁRIO:
{userQuestion}
INSTRUÇÕES:
- Responda baseado APENAS nas informações dos documentos fornecidos
- Se a informação não estiver nos documentos, diga que não encontrou a informação
- Seja preciso e cite trechos relevantes quando possível
- Mantenha um tom profissional e prestativo
- Se houver múltiplas informações relevantes, organize-as de forma clara
RESPOSTA:
";
}
private string BuildEnhancedMessageWithHistory(string currentMessage, List<string> history)
{
if (!history.Any())
return currentMessage;
var enhancedMessage = new System.Text.StringBuilder();
enhancedMessage.AppendLine("HISTÓRICO DA CONVERSA:");
foreach (var message in history.TakeLast(3)) // Últimas 3 mensagens para contexto
{
enhancedMessage.AppendLine($"- {message}");
}
enhancedMessage.AppendLine($"\nPERGUNTA ATUAL: {currentMessage}");
return enhancedMessage.ToString();
}
// ========================================
// MÉTODOS DE ESTATÍSTICAS
// ========================================
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
{
try
{
var vectorStats = await _vectorSearchService.GetStatsAsync();
return new Dictionary<string, object>(vectorStats)
{
["response_service_provider"] = "Qdrant",
["supports_history"] = true,
["supports_dynamic_threshold"] = true,
["last_check"] = DateTime.UtcNow
};
}
catch (Exception ex)
{
return new Dictionary<string, object>
{
["response_service_provider"] = "Qdrant",
["health"] = "error",
["error"] = ex.Message,
["last_check"] = DateTime.UtcNow
};
}
}
public async Task<bool> IsHealthyAsync()
{
try
{
return await _vectorSearchService.IsHealthyAsync();
}
catch
{
return false;
}
}
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
{
return this.GetResponse(userData, projectId, sessionId, question, "pt");
}
}
}
#pragma warning restore SKEXP0001

View File

@ -1,10 +1,13 @@
 
using ChatApi; using ChatApi;
using ChatApi.Models; using ChatApi.Models;
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Data; using ChatRAG.Data;
using ChatRAG.Models; using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using Microsoft.SemanticKernel; using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@ -19,6 +22,7 @@ namespace ChatRAG.Services.ResponseService
private readonly TextDataRepository _textDataRepository; private readonly TextDataRepository _textDataRepository;
private readonly ProjectDataRepository _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,
@ -26,7 +30,9 @@ namespace ChatRAG.Services.ResponseService
TextFilter textFilter, TextFilter textFilter,
TextDataRepository textDataRepository, TextDataRepository textDataRepository,
ProjectDataRepository projectDataRepository, ProjectDataRepository projectDataRepository,
IChatCompletionService chatCompletionService) IChatCompletionService chatCompletionService,
IVectorSearchService vectorSearchService,
ITextDataService textDataService)
{ {
this._chatHistoryService = chatHistoryService; this._chatHistoryService = chatHistoryService;
this._kernel = kernel; this._kernel = kernel;
@ -34,27 +40,71 @@ namespace ChatRAG.Services.ResponseService
this._textDataRepository = textDataRepository; this._textDataRepository = textDataRepository;
this._projectDataRepository = projectDataRepository; this._projectDataRepository = projectDataRepository;
this._chatCompletionService = chatCompletionService; this._chatCompletionService = chatCompletionService;
this._vectorSearchService = vectorSearchService;
} }
public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question) public async Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question, string language = "pt")
{ {
var stopWatch = new System.Diagnostics.Stopwatch(); var stopWatch = new System.Diagnostics.Stopwatch();
stopWatch.Start(); stopWatch.Start();
//var resposta = await BuscarTextoRelacionado(question); var searchStrategy = await ClassificarEstrategiaDeBusca(question, language);
//var resposta = await BuscarTopTextosRelacionados(question, projectId);
var resposta = await BuscarTopTextosRelacionadosDinamico(question, projectId);
var projectData = (await _projectDataRepository.GetAsync()).FirstOrDefault(); string resposta;
switch (searchStrategy)
{
case SearchStrategy.TodosProjeto:
resposta = await BuscarTodosRequisitosDoProjeto(question, projectId);
break;
case SearchStrategy.SimilaridadeComFiltro:
resposta = await BuscarTopTextosRelacionadosComInterface(question, projectId);
break;
case SearchStrategy.SimilaridadeGlobal:
resposta = await BuscarTopTextosRelacionados(question, projectId);
break;
default:
resposta = await BuscarTopTextosRelacionadosComInterface(question, projectId);
break;
}
var projectData = await _projectDataRepository.GetAsync(projectId);
var project = $"Nome: {projectData.Nome} \n\n Descrição:{projectData.Descricao}"; var project = $"Nome: {projectData.Nome} \n\n Descrição:{projectData.Descricao}";
question = $"Para responder à solicitação/pergunta: \"{question }\" por favor, considere o projeto: \"{project}\" e os requisitos: \"{resposta}\""; //question = $"Para responder à solicitação/pergunta: \"{question}\" por favor, considere o projeto: \"{project}\" e os requisitos: \"{resposta}\"";
// Base prompt template
string basePrompt = @"You are a QA professional. Generate ONLY what the user requests.
Project Context: {0}
Requirements: {1}
User Request: ""{2}""
Focus exclusively on the user's request. Do not add summaries, explanations, or additional content unless specifically asked.";
if (language == "pt")
{
basePrompt = @"Você é um profissional de QA. Gere APENAS o que o usuário solicitar.
Contexto do Projeto: {0}
Requisitos: {1}
Solicitação do Usuário: ""{2}""
Foque exclusivamente na solicitação do usuário. Não adicione resumos, explicações ou conteúdo adicional, a menos que especificamente solicitado.";
}
// Usage
question = string.Format(basePrompt, project, resposta, question);
ChatHistory history = _chatHistoryService.GetSumarizer(sessionId); ChatHistory history = _chatHistoryService.GetSumarizer(sessionId);
history.AddUserMessage(question); history.AddUserMessage(question);
var response = await _chatCompletionService.GetChatMessageContentAsync(history); var executionSettings = new OpenAIPromptExecutionSettings
{
Temperature = 0.8,
TopP = 1.0,
FrequencyPenalty = 0,
PresencePenalty = 0
};
var response = await _chatCompletionService.GetChatMessageContentAsync(history, executionSettings);
history.AddMessage(response.Role, response.Content ?? ""); history.AddMessage(response.Role, response.Content ?? "");
_chatHistoryService.UpdateHistory(sessionId, history); _chatHistoryService.UpdateHistory(sessionId, history);
@ -64,6 +114,68 @@ namespace ChatRAG.Services.ResponseService
} }
private async Task<SearchStrategy> ClassificarEstrategiaDeBusca(string question, string language)
{
string prompt = language == "pt" ?
@"TAREFA: Classificar estratégia de busca
ENTRADA: ""{0}""
REGRAS OBRIGATÓRIAS:
- Se menciona ""projeto"" sem especificar módulos/aspectos TODOS_PROJETO
- Se menciona ""todo"", ""todos"", ""completo"", ""geral"" TODOS_PROJETO
- Se menciona aspectos específicos como ""usuário"", ""login"", ""pagamento"" SIMILARIDADE_FILTRADA
- Se pergunta ""como funciona"" algo específico SIMILARIDADE_GLOBAL
EXEMPLOS OBRIGATÓRIOS:
""gere casos de teste para o projeto"" TODOS_PROJETO
""gere resumo do projeto"" TODOS_PROJETO
""gere lista de tarefas para este projeto"" TODOS_PROJETO
""casos de teste para usuários"" SIMILARIDADE_FILTRADA
""como funciona validação CPF"" SIMILARIDADE_GLOBAL
RESPOSTA OBRIGATÓRIA (copie exatamente): TODOS_PROJETO, SIMILARIDADE_FILTRADA ou SIMILARIDADE_GLOBAL" :
@"TASK: Classify search strategy
INPUT: ""{0}""
MANDATORY RULES:
- If mentions ""project"" without specifying modules/aspects ALL_PROJECT
- If mentions ""all"", ""entire"", ""complete"", ""overview"" ALL_PROJECT
- If mentions specific aspects like ""user"", ""login"", ""payment"" FILTERED_SIMILARITY
- If asks ""how does"" something specific work GLOBAL_SIMILARITY
MANDATORY EXAMPLES:
""generate test cases for the project"" ALL_PROJECT
""generate project summary"" ALL_PROJECT
""generate task list for this project"" ALL_PROJECT
""test cases for users"" FILTERED_SIMILARITY
""how does CPF validation work"" GLOBAL_SIMILARITY
MANDATORY RESPONSE (copy exactly): ALL_PROJECT, FILTERED_SIMILARITY or GLOBAL_SIMILARITY";
var classificationPrompt = string.Format(prompt, question);
var executionSettings = new OpenAIPromptExecutionSettings
{
Temperature = 0.0, // Mais determinístico
MaxTokens = 50, // Resposta curta
TopP = 1.0,
FrequencyPenalty = 0,
PresencePenalty = 0
};
// Aqui você faria a chamada para o Ollama
var resp = await _chatCompletionService.GetChatMessageContentAsync(classificationPrompt, executionSettings);
//var classification = await _ollamaService.GetResponse(classificationPrompt);
var classification = resp.Content ?? "";
return classification.ToUpper().Contains("TODOS") || classification.ToUpper().Contains("ALL") ?
SearchStrategy.TodosProjeto :
classification.ToUpper().Contains("FILTRADA") || classification.ToUpper().Contains("FILTERED") ?
SearchStrategy.SimilaridadeComFiltro :
SearchStrategy.SimilaridadeGlobal;
}
async Task<string> BuscarTextoRelacionado(string pergunta) async Task<string> BuscarTextoRelacionado(string pergunta)
{ {
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>(); var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
@ -88,7 +200,49 @@ namespace ChatRAG.Services.ResponseService
return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada."; return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada.";
} }
// Adicione esta nova rotina no seu ResponseRAGService private async Task<string> BuscarTopTextosRelacionadosComInterface(string pergunta, string projectId)
{
var embeddingService = _kernel.GetRequiredService<ITextEmbeddingGenerationService>();
var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(
_textFilter.ToLowerAndWithoutAccents(pergunta));
var embeddingArray = embeddingPergunta.ToArray().Select(e => (double)e).ToArray();
var resultados = await _vectorSearchService.SearchSimilarDynamicAsync(embeddingArray, projectId, 0.5, 3);
if (!resultados.Any())
return "Não encontrei respostas adequadas para a pergunta fornecida.";
var cabecalho = $"Contexto encontrado para: '{pergunta}' ({resultados.Count} resultado(s)):\n\n";
var resultadosFormatados = resultados
.Select((item, index) =>
$"=== CONTEXTO {index + 1} ===\n" +
$"Relevância: {item.Score:P1}\n" +
$"Conteúdo:\n{item.Content}")
.ToList();
return cabecalho + string.Join("\n\n", resultadosFormatados);
}
private async Task<string> BuscarTodosRequisitosDoProjeto(string pergunta, string projectId)
{
var resultados = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
if (!resultados.Any())
return "Não encontrei respostas adequadas para a pergunta fornecida.";
var cabecalho = $"Contexto encontrado para: '{pergunta}' ({resultados.Count} resultado(s)):\n\n";
var resultadosFormatados = resultados
.Select((item, index) =>
$"=== CONTEXTO {index + 1} ===\n" +
$"Relevância: {item.Score:P1}\n" +
$"Conteúdo:\n{item.Content}")
.ToList();
return cabecalho + string.Join("\n\n", resultadosFormatados);
}
async Task<string> BuscarTopTextosRelacionadosDinamico(string pergunta, string projectId, int size = 3) async Task<string> BuscarTopTextosRelacionadosDinamico(string pergunta, string projectId, int size = 3)
{ {
@ -221,6 +375,18 @@ namespace ChatRAG.Services.ResponseService
} }
return dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB)); return dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB));
} }
public Task<string> GetResponse(UserData userData, string projectId, string sessionId, string question)
{
return this.GetResponse(userData, projectId, sessionId, question, "pt");
}
}
public enum SearchStrategy
{
TodosProjeto,
SimilaridadeComFiltro,
SimilaridadeGlobal
} }
} }

View File

@ -0,0 +1,228 @@
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Data;
using ChatRAG.Models;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Embeddings;
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
namespace ChatRAG.Services.SearchVectors
{
public class MongoVectorSearchService : IVectorSearchService
{
private readonly TextDataRepository _textDataRepository;
private readonly ITextEmbeddingGenerationService _embeddingService;
public MongoVectorSearchService(
TextDataRepository textDataRepository,
ITextEmbeddingGenerationService embeddingService)
{
_textDataRepository = textDataRepository;
_embeddingService = embeddingService;
}
// ... resto da implementação permanece igual ...
// (copiar do código anterior)
public async Task<List<VectorSearchResult>> SearchSimilarAsync(
double[] queryEmbedding,
string? projectId = null,
double threshold = 0.3,
int limit = 5,
Dictionary<string, object>? filters = null)
{
List<TextoComEmbedding> textos = null;
try
{
textos = string.IsNullOrEmpty(projectId)
? await _textDataRepository.GetAsync()
: await _textDataRepository.GetByProjectIdAsync(projectId);
}
catch (Exception ex)
{
throw new Exception($"Erro ao buscar documentos: {ex.Message}");
}
var resultados = textos
.Select(texto => new VectorSearchResult
{
Id = texto.Id,
Title = texto.Titulo,
Content = texto.Conteudo,
ProjectId = texto.ProjetoId,
Score = CalcularSimilaridadeCoseno(queryEmbedding, texto.Embedding),
Embedding = texto.Embedding,
Provider = "MongoDB",
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
})
.Where(r => r.Score >= threshold)
.OrderByDescending(r => r.Score)
.Take(limit)
.ToList();
return resultados;
}
public async Task<List<VectorSearchResult>> SearchSimilarDynamicAsync(
double[] queryEmbedding,
string projectId,
double minThreshold = 0.5,
int limit = 5)
{
var resultados = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold, limit);
if (resultados.Count < 3)
{
resultados = await SearchSimilarAsync(queryEmbedding, projectId, minThreshold * 0.7, limit);
}
if (resultados.Count < 3)
{
resultados = await SearchSimilarAsync(queryEmbedding, projectId, 0.2, limit);
}
return resultados.Take(limit).ToList();
}
public async Task<string> AddDocumentAsync(
string title,
string content,
string projectId,
double[] embedding,
Dictionary<string, object>? metadata = null)
{
var documento = new TextoComEmbedding
{
Id = Guid.NewGuid().ToString(),
Titulo = title,
Conteudo = content,
ProjetoId = projectId,
Embedding = embedding
};
await _textDataRepository.CreateAsync(documento);
return documento.Id;
}
public async Task UpdateDocumentAsync(
string id,
string title,
string content,
string projectId,
double[] embedding,
Dictionary<string, object>? metadata = null)
{
var documento = new TextoComEmbedding
{
Id = id,
Titulo = title,
Conteudo = content,
ProjetoId = projectId,
Embedding = embedding
};
await _textDataRepository.UpdateAsync(id, documento);
}
public async Task DeleteDocumentAsync(string id)
{
await _textDataRepository.RemoveAsync(id);
}
public async Task<bool> DocumentExistsAsync(string id)
{
var doc = await _textDataRepository.GetAsync(id);
return doc != null;
}
public async Task<VectorSearchResult?> GetDocumentAsync(string id)
{
var doc = await _textDataRepository.GetAsync(id);
if (doc == null) return null;
return new VectorSearchResult
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
Score = 1.0,
Embedding = doc.Embedding,
Provider = "MongoDB",
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
}
public async Task<List<VectorSearchResult>> GetDocumentsByProjectAsync(string projectId)
{
var docs = await _textDataRepository.GetByProjectIdAsync(projectId);
return docs.Select(doc => new VectorSearchResult
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
Score = 1.0,
Embedding = doc.Embedding,
Provider = "MongoDB",
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}).ToList();
}
public async Task<int> GetDocumentCountAsync(string? projectId = null)
{
if (string.IsNullOrEmpty(projectId))
{
var all = await _textDataRepository.GetAsync();
return all.Count;
}
else
{
var byProject = await _textDataRepository.GetByProjectIdAsync(projectId);
return byProject.Count;
}
}
public async Task<bool> IsHealthyAsync()
{
try
{
var count = await GetDocumentCountAsync();
return true;
}
catch
{
return false;
}
}
public async Task<Dictionary<string, object>> GetStatsAsync()
{
var totalDocs = await GetDocumentCountAsync();
return new Dictionary<string, object>
{
["provider"] = "MongoDB",
["total_documents"] = totalDocs,
["health"] = await IsHealthyAsync(),
["last_check"] = DateTime.UtcNow
};
}
private double CalcularSimilaridadeCoseno(double[] embedding1, double[] embedding2)
{
double dotProduct = 0.0;
double normA = 0.0;
double normB = 0.0;
for (int i = 0; i < embedding1.Length; i++)
{
dotProduct += embedding1[i] * embedding2[i];
normA += Math.Pow(embedding1[i], 2);
normB += Math.Pow(embedding2[i], 2);
}
return dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB));
}
}
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

View File

@ -0,0 +1,527 @@
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;
#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 bool _collectionInitialized = false;
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;
try
{
var collectionExists = await _client.CollectionExistsAsync(_settings.CollectionName);
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 opcionais
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
);
_logger.LogInformation("✅ Collection {CollectionName} criada", _settings.CollectionName);
}
_collectionInitialized = true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao inicializar collection {CollectionName}", _settings.CollectionName);
throw;
}
}
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: true
);
return searchResult.Select(point => 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)
}).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 = new Dictionary<string, Value>
{
["title"] = title,
["content"] = content,
["project_id"] = projectId,
["created_at"] = DateTime.UtcNow.ToString("O"),
["updated_at"] = DateTime.UtcNow.ToString("O")
};
if (metadata?.Any() == true)
{
foreach (var kvp in metadata)
{
payload[$"meta_{kvp.Key}"] = ConvertToValue(kvp.Value);
}
}
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 = new Dictionary<string, Value>
{
["title"] = title,
["content"] = content,
["project_id"] = projectId,
["updated_at"] = DateTime.UtcNow.ToString("O")
};
if (metadata?.Any() == true)
{
foreach (var kvp in metadata)
{
payload[$"meta_{kvp.Key}"] = ConvertToValue(kvp.Value);
}
}
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
{
var result = await GetDocumentAsync(id);
return result != null;
}
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();
if (point == null) return null;
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)
};
}
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 results = await _client.ScrollAsync(
collectionName: _settings.CollectionName,
filter: filter,
limit: 10000,
payloadSelector: true,
vectorsSelector: true
);
return results.Result.Select(point => 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)
}).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
};
}
}
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()
{
_client?.Dispose();
}
}
}
#pragma warning restore SKEXP0001

View File

@ -0,0 +1,114 @@
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
{
/// <summary>
/// Factory principal que cria implementações baseadas na configuração
/// </summary>
/// <summary>
/// Factory principal que cria implementações baseadas na configuração
/// </summary>
public class VectorDatabaseFactory : IVectorDatabaseFactory
{
private readonly VectorDatabaseSettings _settings;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<VectorDatabaseFactory> _logger;
public VectorDatabaseFactory(
IOptions<VectorDatabaseSettings> settings,
IServiceProvider serviceProvider,
ILogger<VectorDatabaseFactory> logger)
{
_settings = settings.Value;
_serviceProvider = serviceProvider;
_logger = logger;
// Valida configurações na inicialização
ValidateSettings();
}
public string GetActiveProvider() => _settings.Provider;
public VectorDatabaseSettings GetSettings() => _settings;
public IVectorSearchService CreateVectorSearchService()
{
_logger.LogInformation("Criando VectorSearchService para provider: {Provider}", _settings.Provider);
return _settings.Provider.ToLower() switch
{
"qdrant" => GetService<ChatRAG.Services.SearchVectors.QdrantVectorSearchService>(),
"mongodb" => GetService<ChatRAG.Services.SearchVectors.MongoVectorSearchService>(),
_ => 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
{
// ✅ CORRIGIDO: Usa os namespaces corretos
"qdrant" => GetService<ChatRAG.Services.TextServices.QdrantTextDataService>(),
"mongodb" => GetService<ChatApi.Data.TextData>(), // Sua classe atual!
_ => throw new ArgumentException($"Provider de TextData não suportado: {_settings.Provider}")
};
}
public IResponseService CreateResponseService()
{
_logger.LogInformation("Criando ResponseService para provider: {Provider}", _settings.Provider);
return _settings.Provider.ToLower() switch
{
// ✅ CORRIGIDO: Usa os namespaces corretos
"qdrant" => GetService<ChatRAG.Services.ResponseService.QdrantResponseService>(),
"mongodb" => GetService<ChatRAG.Services.ResponseService.ResponseRAGService>(), // Sua classe atual!
_ => throw new ArgumentException($"Provider de Response não suportado: {_settings.Provider}")
};
}
// ========================================
// MÉTODOS AUXILIARES
// ========================================
private T GetService<T>() where T : class
{
try
{
var service = _serviceProvider.GetRequiredService<T>();
_logger.LogDebug("Serviço {ServiceType} criado com sucesso", typeof(T).Name);
return service;
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "Erro ao criar serviço {ServiceType} para provider {Provider}",
typeof(T).Name, _settings.Provider);
throw new InvalidOperationException(
$"Serviço {typeof(T).Name} não está registrado para provider {_settings.Provider}. " +
$"Certifique-se de chamar services.Add{_settings.Provider}Provider() no DI.", ex);
}
}
private void ValidateSettings()
{
if (!_settings.IsValid())
{
var errors = _settings.GetValidationErrors();
var errorMessage = $"Configurações inválidas para VectorDatabase: {string.Join(", ", errors)}";
_logger.LogError(errorMessage);
throw new InvalidOperationException(errorMessage);
}
_logger.LogInformation("Configurações validadas com sucesso para provider: {Provider}", _settings.Provider);
}
}
}

View File

@ -0,0 +1,523 @@
#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;
namespace ChatRAG.Services.TextServices
{
public class QdrantTextDataService : ITextDataService
{
private readonly IVectorSearchService _vectorSearchService;
private readonly ITextEmbeddingGenerationService _embeddingService;
private readonly ILogger<QdrantTextDataService> _logger;
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
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 Qdrant", titulo);
}
else
{
// Atualiza documento existente
await _vectorSearchService.UpdateDocumentAsync(id, titulo, texto, projectId, embeddingArray);
_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 = 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 Qdrant", textoArray.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao processar texto completo no Qdrant");
throw;
}
}
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
{
try
{
// Busca todos os projetos e depois todos os documentos
var allDocuments = new List<VectorSearchResult>();
// Como Qdrant 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 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 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 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 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 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
// ========================================
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 Qdrant",
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 Qdrant", 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"] = "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
};
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
// ========================================
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>()
};
}
private async Task<List<string>> GetAllProjectIds()
{
try
{
// Esta é uma operação custosa no Qdrant
// 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[1536]; // Assumindo embeddings OpenAI
var allResults = await _vectorSearchService.SearchSimilarAsync(
dummyVector,
projectId: null,
threshold: 0.0,
limit: 10000);
return allResults
.Select(r => r.ProjectId)
.Where(pid => !string.IsNullOrEmpty(pid))
.Distinct()
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar IDs de projetos do Qdrant");
return new List<string>();
}
}
}
}
#pragma warning restore SKEXP0001

View File

@ -0,0 +1,64 @@
using ChatRAG.Services.Contracts;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace ChatRAG.Services
{
public class VectorDatabaseHealthCheck : IHealthCheck
{
private readonly IVectorDatabaseFactory _factory;
private readonly ILogger<VectorDatabaseHealthCheck> _logger;
public VectorDatabaseHealthCheck(
IVectorDatabaseFactory factory,
ILogger<VectorDatabaseHealthCheck> logger)
{
_factory = factory;
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var provider = _factory.GetActiveProvider();
var vectorService = _factory.CreateVectorSearchService();
var textService = _factory.CreateTextDataService();
// Testa conectividade básica
var isHealthy = await vectorService.IsHealthyAsync();
var stats = await vectorService.GetStatsAsync();
var providerStats = await textService.GetProviderStatsAsync();
var data = new Dictionary<string, object>
{
["provider"] = provider,
["vector_service_healthy"] = isHealthy,
["total_documents"] = stats.GetValueOrDefault("total_documents", 0),
["provider_stats"] = providerStats
};
if (isHealthy)
{
_logger.LogDebug("Vector Database health check passou para provider {Provider}", provider);
return HealthCheckResult.Healthy($"Vector Database ({provider}) está saudável", data);
}
else
{
_logger.LogWarning("Vector Database health check falhou para provider {Provider}", provider);
return HealthCheckResult.Unhealthy($"Vector Database ({provider}) não está saudável", data: data);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro no health check do Vector Database");
return HealthCheckResult.Unhealthy("Erro no Vector Database", ex, new Dictionary<string, object>
{
["provider"] = _factory.GetActiveProvider(),
["error"] = ex.Message
});
}
}
}
}

View File

@ -0,0 +1,494 @@
namespace ChatRAG.Settings
{
// ============================================================================
// 📁 Configuration/VectorDatabaseSettings.cs
// Settings unificados para todos os providers (MongoDB, Qdrant, etc.)
// ============================================================================
namespace ChatRAG.Configuration
{
/// <summary>
/// Configurações principais do sistema de Vector Database
/// </summary>
public class VectorDatabaseSettings
{
/// <summary>
/// Provider ativo (MongoDB, Qdrant, Pinecone, etc.)
/// </summary>
public string Provider { get; set; } = "MongoDB";
/// <summary>
/// Configurações específicas do MongoDB
/// </summary>
public MongoDbSettings MongoDB { get; set; } = new();
/// <summary>
/// Configurações específicas do Qdrant
/// </summary>
public QdrantSettings Qdrant { get; set; } = new();
/// <summary>
/// Configurações globais de embedding
/// </summary>
public EmbeddingSettings Embedding { get; set; } = new();
/// <summary>
/// Configurações de performance e cache
/// </summary>
public PerformanceSettings Performance { get; set; } = new();
/// <summary>
/// Configurações de logging e monitoramento
/// </summary>
public MonitoringSettings Monitoring { get; set; } = new();
/// <summary>
/// Valida se as configurações estão corretas
/// </summary>
public bool IsValid()
{
if (string.IsNullOrWhiteSpace(Provider))
return false;
return Provider.ToLower() switch
{
"mongodb" => MongoDB.IsValid(),
"qdrant" => Qdrant.IsValid(),
_ => false
};
}
/// <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;
}
}
/// <summary>
/// Configurações específicas do MongoDB
/// </summary>
public class MongoDbSettings
{
/// <summary>
/// String de conexão do MongoDB
/// </summary>
public string ConnectionString { get; set; } = string.Empty;
/// <summary>
/// Nome do banco de dados
/// </summary>
public string DatabaseName { get; set; } = string.Empty;
/// <summary>
/// Nome da coleção de textos/documentos
/// </summary>
public string TextCollectionName { get; set; } = "texts";
/// <summary>
/// Nome da coleção de projetos
/// </summary>
public string ProjectCollectionName { get; set; } = "projects";
/// <summary>
/// Nome da coleção de dados de usuário
/// </summary>
public string UserDataName { get; set; } = "users";
/// <summary>
/// Timeout de conexão em segundos
/// </summary>
public int ConnectionTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Timeout de operação em segundos
/// </summary>
public int OperationTimeoutSeconds { get; set; } = 60;
/// <summary>
/// Tamanho máximo do pool de conexões
/// </summary>
public int MaxConnectionPoolSize { get; set; } = 100;
/// <summary>
/// Habilitar índices de busca vetorial
/// </summary>
public bool EnableVectorSearch { get; set; } = true;
/// <summary>
/// Configurações específicas do Atlas Search
/// </summary>
public MongoAtlasSearchSettings AtlasSearch { get; set; } = new();
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(ConnectionString) &&
!string.IsNullOrWhiteSpace(DatabaseName) &&
!string.IsNullOrWhiteSpace(TextCollectionName);
}
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;
}
}
/// <summary>
/// Configurações do MongoDB Atlas Search
/// </summary>
public class MongoAtlasSearchSettings
{
/// <summary>
/// Nome do índice de busca vetorial
/// </summary>
public string VectorIndexName { get; set; } = "vector_index";
/// <summary>
/// Número de candidatos para busca aproximada
/// </summary>
public int NumCandidates { get; set; } = 200;
/// <summary>
/// Limite de resultados do Atlas Search
/// </summary>
public int SearchLimit { get; set; } = 100;
}
/// <summary>
/// Configurações específicas do Qdrant
/// </summary>
public class QdrantSettings
{
/// <summary>
/// Host do servidor Qdrant
/// </summary>
public string Host { get; set; } = "localhost";
/// <summary>
/// Porta do servidor Qdrant (REST API)
/// </summary>
public int Port { get; set; } = 6333;
/// <summary>
/// Porta gRPC (opcional, para performance)
/// </summary>
public int? GrpcPort { get; set; } = 6334;
/// <summary>
/// Chave de API (se autenticação estiver habilitada)
/// </summary>
public string? ApiKey { get; set; }
/// <summary>
/// Usar TLS/SSL
/// </summary>
public bool UseTls { get; set; } = false;
/// <summary>
/// Nome da coleção principal
/// </summary>
public string CollectionName { get; set; } = "documents";
/// <summary>
/// Tamanho do vetor de embedding
/// </summary>
public int VectorSize { get; set; } = 1536; // OpenAI embedding size
/// <summary>
/// Métrica de distância (Cosine, Euclid, Dot, Manhattan)
/// </summary>
public string Distance { get; set; } = "Cosine";
// ========================================
// CONFIGURAÇÕES DE PERFORMANCE
// ========================================
/// <summary>
/// Parâmetro M do algoritmo HNSW (conectividade)
/// Valores típicos: 16-48, maior = melhor recall, mais memória
/// </summary>
public int HnswM { get; set; } = 16;
/// <summary>
/// Parâmetro ef_construct do HNSW (construção do índice)
/// Valores típicos: 100-800, maior = melhor qualidade, mais lento
/// </summary>
public int HnswEfConstruct { get; set; } = 200;
/// <summary>
/// Parâmetro ef do HNSW (busca)
/// Valores típicos: igual ou maior que o número de resultados desejados
/// </summary>
public int HnswEf { get; set; } = 128;
/// <summary>
/// Armazenar vetores em disco (economiza RAM)
/// </summary>
public bool OnDisk { get; set; } = false;
/// <summary>
/// Fator de replicação (para clusters)
/// </summary>
public int ReplicationFactor { get; set; } = 1;
/// <summary>
/// Número de shards (para distribuição)
/// </summary>
public int ShardNumber { get; set; } = 1;
/// <summary>
/// Usar quantização para reduzir uso de memória
/// </summary>
public bool UseQuantization { get; set; } = false;
/// <summary>
/// Configurações de quantização
/// </summary>
public QuantizationSettings Quantization { get; set; } = new();
/// <summary>
/// Timeout de conexão em segundos
/// </summary>
public int ConnectionTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Timeout de operação em segundos
/// </summary>
public int OperationTimeoutSeconds { get; set; } = 60;
/// <summary>
/// URL completa calculada
/// </summary>
public string GetConnectionUrl()
{
var protocol = UseTls ? "https" : "http";
return $"{protocol}://{Host}:{Port}";
}
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Host) &&
Port > 0 &&
!string.IsNullOrWhiteSpace(CollectionName) &&
VectorSize > 0;
}
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;
}
}
/// <summary>
/// Configurações de quantização para Qdrant
/// </summary>
public class QuantizationSettings
{
/// <summary>
/// Tipo de quantização (scalar, product, binary)
/// </summary>
public string Type { get; set; } = "scalar";
/// <summary>
/// Quantil para quantização scalar
/// </summary>
public double Quantile { get; set; } = 0.99;
/// <summary>
/// Sempre usar RAM para quantização
/// </summary>
public bool AlwaysRam { get; set; } = false;
}
/// <summary>
/// Configurações globais de embedding
/// </summary>
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;
}
}
/// <summary>
/// Configurações de performance e otimização
/// </summary>
public class PerformanceSettings
{
/// <summary>
/// Habilitar paralelização em operações de lote
/// </summary>
public bool EnableParallelization { get; set; } = true;
/// <summary>
/// Número máximo de threads paralelas
/// </summary>
public int MaxParallelism { get; set; } = Environment.ProcessorCount;
/// <summary>
/// Tamanho do batch para operações em lote
/// </summary>
public int BatchSize { get; set; } = 100;
/// <summary>
/// Timeout padrão para operações em segundos
/// </summary>
public int DefaultTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Habilitar retry automático
/// </summary>
public bool EnableRetry { get; set; } = true;
/// <summary>
/// Número máximo de tentativas
/// </summary>
public int MaxRetryAttempts { get; set; } = 3;
/// <summary>
/// Delay entre tentativas em segundos
/// </summary>
public int RetryDelaySeconds { get; set; } = 2;
}
/// <summary>
/// Configurações de monitoramento e logging
/// </summary>
public class MonitoringSettings
{
/// <summary>
/// Habilitar logging detalhado
/// </summary>
public bool EnableDetailedLogging { get; set; } = false;
/// <summary>
/// Logar tempos de operação
/// </summary>
public bool LogOperationTimes { get; set; } = true;
/// <summary>
/// Threshold para log de operações lentas (ms)
/// </summary>
public int SlowOperationThresholdMs { get; set; } = 1000;
/// <summary>
/// Habilitar métricas de performance
/// </summary>
public bool EnableMetrics { get; set; } = true;
/// <summary>
/// Intervalo de coleta de métricas em segundos
/// </summary>
public int MetricsIntervalSeconds { get; set; } = 60;
/// <summary>
/// Habilitar health checks
/// </summary>
public bool EnableHealthChecks { get; set; } = true;
/// <summary>
/// Intervalo de health check em segundos
/// </summary>
public int HealthCheckIntervalSeconds { get; set; } = 30;
}
}
}

View File

@ -0,0 +1,23 @@
using ChatRAG.Settings.ChatRAG.Configuration;
using Microsoft.Extensions.Options;
namespace ChatRAG.Settings
{
/// <summary>
/// Validador para VectorDatabaseSettings
/// </summary>
public class VectorDatabaseSettingsValidator : IValidateOptions<VectorDatabaseSettings>
{
public ValidateOptionsResult Validate(string name, VectorDatabaseSettings options)
{
var errors = options.GetValidationErrors();
if (errors.Any())
{
return ValidateOptionsResult.Fail(errors);
}
return ValidateOptionsResult.Success;
}
}
}

264
Tools/MigrationService.cs Normal file
View File

@ -0,0 +1,264 @@
using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Data;
using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using ChatRAG.Services.Migration;
using ChatRAG.Services.SearchVectors;
using ChatRAG.Settings.ChatRAG.Configuration;
using ChatRAG.Settings;
using Microsoft.Extensions.Options;
using System.Diagnostics;
namespace ChatRAG.Services.Migration
{
public class MigrationService
{
private readonly ILogger<MigrationService> _logger;
private readonly IServiceProvider _serviceProvider;
public MigrationService(
ILogger<MigrationService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
/// <summary>
/// Migra todos os dados do MongoDB para Qdrant
/// </summary>
public async Task<MigrationResult> MigrateFromMongoToQdrantAsync(
bool validateData = true,
int batchSize = 50)
{
var stopwatch = Stopwatch.StartNew();
var result = new MigrationResult { StartTime = DateTime.UtcNow };
try
{
_logger.LogInformation("🚀 Iniciando migração MongoDB → Qdrant...");
// Cria serviços específicos para migração
var mongoService = CreateMongoService();
var qdrantService = CreateQdrantService();
// 1. Exporta dados do MongoDB
_logger.LogInformation("📤 Exportando dados do MongoDB...");
var mongoDocuments = await mongoService.GetAll();
var documentsList = mongoDocuments.ToList();
result.TotalDocuments = documentsList.Count;
_logger.LogInformation("✅ {Count} documentos encontrados no MongoDB", result.TotalDocuments);
if (!documentsList.Any())
{
_logger.LogWarning("⚠️ Nenhum documento encontrado no MongoDB");
result.Success = true;
result.Message = "Migração concluída - nenhum documento para migrar";
return result;
}
// 2. Agrupa por projeto para migração organizada
var documentsByProject = documentsList.GroupBy(d => d.ProjetoId).ToList();
_logger.LogInformation("📁 Documentos organizados em {ProjectCount} projetos", documentsByProject.Count);
// 3. Migra por projeto em lotes
foreach (var projectGroup in documentsByProject)
{
var projectId = projectGroup.Key;
var projectDocs = projectGroup.ToList();
_logger.LogInformation("📂 Migrando projeto {ProjectId}: {DocCount} documentos",
projectId, projectDocs.Count);
// Processa em lotes para não sobrecarregar
for (int i = 0; i < projectDocs.Count; i += batchSize)
{
var batch = projectDocs.Skip(i).Take(batchSize).ToList();
try
{
await MigrateBatch(batch, qdrantService);
result.MigratedDocuments += batch.Count;
_logger.LogDebug("✅ Lote {BatchNum}: {BatchCount} documentos migrados",
(i / batchSize) + 1, batch.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ Erro no lote {BatchNum} do projeto {ProjectId}",
(i / batchSize) + 1, projectId);
result.Errors.Add($"Projeto {projectId}, lote {(i / batchSize) + 1}: {ex.Message}");
}
}
}
// 4. Validação (se solicitada)
if (validateData)
{
_logger.LogInformation("🔍 Validando dados migrados...");
var validationResult = await ValidateMigration(mongoService, qdrantService);
result.ValidationResult = validationResult;
if (!validationResult.IsValid)
{
_logger.LogWarning("⚠️ Validação encontrou inconsistências: {Issues}",
string.Join(", ", validationResult.Issues));
}
else
{
_logger.LogInformation("✅ Validação passou - dados consistentes");
}
}
stopwatch.Stop();
result.Duration = stopwatch.Elapsed;
result.Success = true;
result.Message = $"Migração concluída: {result.MigratedDocuments}/{result.TotalDocuments} documentos";
_logger.LogInformation("🎉 Migração concluída em {Duration}s: {MigratedCount}/{TotalCount} documentos",
result.Duration.TotalSeconds, result.MigratedDocuments, result.TotalDocuments);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
result.Duration = stopwatch.Elapsed;
result.Success = false;
result.Message = $"Erro na migração: {ex.Message}";
result.Errors.Add(ex.ToString());
_logger.LogError(ex, "💥 Erro fatal na migração");
return result;
}
}
/// <summary>
/// Rollback - remove todos os dados do Qdrant
/// </summary>
public async Task<bool> RollbackQdrantAsync()
{
try
{
_logger.LogWarning("🔄 Iniciando rollback - removendo dados do Qdrant...");
var qdrantService = CreateQdrantService();
// Busca todos os documentos
var allDocuments = await qdrantService.GetAll();
var documentIds = allDocuments.Select(d => d.Id).ToList();
if (!documentIds.Any())
{
_logger.LogInformation(" Nenhum documento encontrado no Qdrant para rollback");
return true;
}
// Remove em lotes
var batchSize = 100;
for (int i = 0; i < documentIds.Count; i += batchSize)
{
var batch = documentIds.Skip(i).Take(batchSize).ToList();
await qdrantService.DeleteDocumentsBatchAsync(batch);
_logger.LogDebug("🗑️ Lote {BatchNum}: {BatchCount} documentos removidos",
(i / batchSize) + 1, batch.Count);
}
_logger.LogInformation("✅ Rollback concluído: {Count} documentos removidos do Qdrant", documentIds.Count);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "❌ Erro no rollback");
return false;
}
}
// ========================================
// MÉTODOS AUXILIARES
// ========================================
private async Task MigrateBatch(List<ChatRAG.Models.TextoComEmbedding> batch, ITextDataService qdrantService)
{
var documents = batch.Select(doc => new DocumentInput
{
Id = doc.Id,
Title = doc.Titulo,
Content = doc.Conteudo,
ProjectId = doc.ProjetoId,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Metadata = new Dictionary<string, object>
{
["migrated_from"] = "mongodb",
["migration_date"] = DateTime.UtcNow.ToString("O"),
["original_id"] = doc.Id,
["project_name"] = doc.ProjetoNome ?? "",
["document_type"] = doc.TipoDocumento ?? "",
["category"] = doc.Categoria ?? ""
}
}).ToList();
await qdrantService.SaveDocumentsBatchAsync(documents);
}
private async Task<ValidationResult> ValidateMigration(ITextDataService mongoService, ITextDataService qdrantService)
{
var result = new ValidationResult();
try
{
// Compara contagens
var mongoCount = await mongoService.GetDocumentCountAsync();
var qdrantCount = await qdrantService.GetDocumentCountAsync();
if (mongoCount != qdrantCount)
{
result.Issues.Add($"Contagem divergente: MongoDB({mongoCount}) vs Qdrant({qdrantCount})");
}
// Valida alguns documentos aleatoriamente
var mongoDocuments = await mongoService.GetAll();
var sampleDocs = mongoDocuments.Take(10).ToList();
foreach (var mongoDoc in sampleDocs)
{
var qdrantDoc = await qdrantService.GetDocumentAsync(mongoDoc.Id);
if (qdrantDoc == null)
{
result.Issues.Add($"Documento {mongoDoc.Id} não encontrado no Qdrant");
}
else if (qdrantDoc.Title != mongoDoc.Titulo || qdrantDoc.Content != mongoDoc.Conteudo)
{
result.Issues.Add($"Conteúdo divergente no documento {mongoDoc.Id}");
}
}
result.IsValid = !result.Issues.Any();
return result;
}
catch (Exception ex)
{
result.Issues.Add($"Erro na validação: {ex.Message}");
result.IsValid = false;
return result;
}
}
private ITextDataService CreateMongoService()
{
// Força usar MongoDB independente da configuração
return _serviceProvider.GetRequiredService<ChatApi.Data.TextData>();
}
private ITextDataService CreateQdrantService()
{
// Força usar Qdrant independente da configuração
return _serviceProvider.GetRequiredService<ChatRAG.Services.TextServices.QdrantTextDataService>();
}
}
}

View File

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

View File

@ -1,11 +1,4 @@
{ {
"DomvsDatabase": {
"ConnectionString": "mongodb://admin:c4rn31r0@k3sw2:27017,k3ss1:27017/?authSource=admin",
"DatabaseName": "RAGProjects-dev",
"TextCollectionName": "Texts",
"ProjectCollectionName": "Projects",
"UserDataName": "UserData"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
@ -13,7 +6,30 @@
"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": "localhost",
"Port": 6334,
"CollectionName": "texts",
"VectorSize": 384,
"Distance": "Cosine",
"HnswM": 16,
"HnswEfConstruct": 200,
"OnDisk": false
}
},
"Features": {
"UseQdrant": true
},
"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"
} }