ChatRAG/Services/TextServices/ChromaTextDataService.cs
2025-06-21 14:20:07 -03:00

538 lines
20 KiB
C#

using ChatRAG.Contracts.VectorSearch;
using ChatRAG.Models;
using ChatRAG.Services.Contracts;
using ChatRAG.Data;
using Microsoft.SemanticKernel.Embeddings;
using System.Text;
#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
namespace ChatRAG.Services
{
// ========================================
// CHROMA TEXT DATA SERVICE - IMPLEMENTAÇÃO COMPLETA
// ========================================
public class ChromaTextDataService : ITextDataService
{
private readonly IVectorSearchService _vectorSearchService;
private readonly ITextEmbeddingGenerationService _embeddingService;
private readonly ILogger<ChromaTextDataService> _logger;
public ChromaTextDataService(
IVectorSearchService vectorSearchService,
ITextEmbeddingGenerationService embeddingService,
ILogger<ChromaTextDataService> logger)
{
_vectorSearchService = vectorSearchService;
_embeddingService = embeddingService;
_logger = logger;
}
public string ProviderName => "Chroma";
// ========================================
// MÉTODOS ORIGINAIS (compatibilidade com MongoDB)
// ========================================
public async Task SalvarNoMongoDB(string titulo, string texto, string projectId)
{
await SalvarNoMongoDB(null, titulo, texto, projectId);
}
public async Task SalvarNoMongoDB(string? id, string titulo, string texto, string projectId)
{
try
{
var conteudo = $"**{titulo}** \n\n {texto}";
// Gera embedding
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
if (string.IsNullOrEmpty(id))
{
// Cria novo documento
await _vectorSearchService.AddDocumentAsync(titulo, texto, projectId, embeddingArray);
_logger.LogDebug("Documento '{Title}' criado no Chroma", titulo);
}
else
{
// Atualiza documento existente
await _vectorSearchService.UpdateDocumentAsync(id, titulo, texto, projectId, embeddingArray);
_logger.LogDebug("Documento '{Id}' atualizado no Chroma", id);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao salvar documento '{Title}' no Chroma", titulo);
throw;
}
}
public async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto, string projectId)
{
try
{
var textoArray = new List<string>();
string[] textolinhas = textoCompleto.Split(
new string[] { "\n" },
StringSplitOptions.None
);
var title = textolinhas[0];
var builder = new StringBuilder();
foreach (string line in textolinhas)
{
if (line.StartsWith("**") || line.StartsWith("\r**"))
{
if (builder.Length > 0)
{
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
builder = new StringBuilder();
title = line;
}
}
else
{
builder.AppendLine(line);
}
}
// Adiciona último bloco se houver
if (builder.Length > 0)
{
textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString());
}
// Processa cada seção
foreach (var item in textoArray)
{
await SalvarNoMongoDB(title.Replace("**", "").Replace("\r", ""), item, projectId);
}
_logger.LogInformation("Texto completo processado: {SectionCount} seções salvas no Chroma", textoArray.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao processar texto completo no Chroma");
throw;
}
}
public async Task<IEnumerable<TextoComEmbedding>> GetAll()
{
try
{
// Busca todos os projetos e depois todos os documentos
var allDocuments = new List<VectorSearchResult>();
// Como Chroma não tem um "GetAll" direto, vamos usar scroll
// Isso é uma limitação vs MongoDB, mas é mais eficiente
var projects = await GetAllProjectIds();
foreach (var projectId in projects)
{
var projectDocs = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
allDocuments.AddRange(projectDocs);
}
return allDocuments.Select(ConvertToTextoComEmbedding);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar todos os documentos do Chroma");
throw;
}
}
public async Task<IEnumerable<TextoComEmbedding>> GetByPorjectId(string projectId)
{
try
{
var documents = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
return documents.Select(ConvertToTextoComEmbedding);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} no Chroma", projectId);
throw;
}
}
public async Task<TextoComEmbedding> GetById(string id)
{
try
{
var document = await _vectorSearchService.GetDocumentAsync(id);
if (document == null)
{
throw new ArgumentException($"Documento {id} não encontrado no Chroma");
}
return ConvertToTextoComEmbedding(document);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Chroma", id);
throw;
}
}
// ========================================
// MÉTODOS NOVOS DA INTERFACE
// ========================================
public async Task<string> SaveDocumentAsync(DocumentInput document)
{
try
{
var conteudo = $"**{document.Title}** \n\n {document.Content}";
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
string id;
if (!string.IsNullOrEmpty(document.Id))
{
// Atualizar documento existente
await _vectorSearchService.UpdateDocumentAsync(
document.Id,
document.Title,
document.Content,
document.ProjectId,
embeddingArray,
document.Metadata);
id = document.Id;
}
else
{
// Criar novo documento
id = await _vectorSearchService.AddDocumentAsync(
document.Title,
document.Content,
document.ProjectId,
embeddingArray,
document.Metadata);
}
_logger.LogDebug("Documento {Id} salvo no Chroma via SaveDocumentAsync", id);
return id;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao salvar documento no Chroma");
throw;
}
}
public async Task UpdateDocumentAsync(string id, DocumentInput document)
{
try
{
var conteudo = $"**{document.Title}** \n\n {document.Content}";
var embedding = await _embeddingService.GenerateEmbeddingAsync(conteudo);
var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray();
await _vectorSearchService.UpdateDocumentAsync(
id,
document.Title,
document.Content,
document.ProjectId,
embeddingArray,
document.Metadata);
_logger.LogDebug("Documento {Id} atualizado no Chroma", id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao atualizar documento {Id} no Chroma", id);
throw;
}
}
public async Task DeleteDocumentAsync(string id)
{
try
{
await _vectorSearchService.DeleteDocumentAsync(id);
_logger.LogDebug("Documento {Id} deletado do Chroma", id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao deletar documento {Id} do Chroma", id);
throw;
}
}
public async Task<bool> DocumentExistsAsync(string id)
{
try
{
return await _vectorSearchService.DocumentExistsAsync(id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao verificar existência do documento {Id} no Chroma", id);
return false;
}
}
public async Task<DocumentOutput?> GetDocumentAsync(string id)
{
try
{
var result = await _vectorSearchService.GetDocumentAsync(id);
if (result == null) return null;
return new DocumentOutput
{
Id = result.Id,
Title = result.Metadata?.GetValueOrDefault("title")?.ToString() ?? "",
Content = result.Content,
ProjectId = result.Metadata?.GetValueOrDefault("project_id")?.ToString() ?? "",
Embedding = Array.Empty<double>(), // Chroma não retorna embedding na busca
CreatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("created_at")?.ToString()).Value,
UpdatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("updated_at")?.ToString()).Value,
Metadata = result.Metadata
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar documento {Id} do Chroma", id);
return null;
}
}
public async Task<List<DocumentOutput>> GetDocumentsByProjectAsync(string projectId)
{
try
{
var results = await _vectorSearchService.GetDocumentsByProjectAsync(projectId);
return results.Select(result => new DocumentOutput
{
Id = result.Id,
Title = result.Metadata?.GetValueOrDefault("title")?.ToString() ?? "",
Content = result.Content,
ProjectId = projectId,
Embedding = Array.Empty<double>(),
CreatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("created_at")?.ToString()).Value,
UpdatedAt = ParseDateTime(result.Metadata?.GetValueOrDefault("updated_at")?.ToString()).Value,
Metadata = result.Metadata
}).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar documentos do projeto {ProjectId} do Chroma", projectId);
throw;
}
}
public async Task<int> GetDocumentCountAsync(string? projectId = null)
{
try
{
return await _vectorSearchService.GetDocumentCountAsync(projectId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao contar documentos no Chroma");
return 0;
}
}
// ========================================
// OPERAÇÕES EM LOTE
// ========================================
public async Task<List<string>> SaveDocumentsBatchAsync(List<DocumentInput> documents)
{
var ids = new List<string>();
var errors = new List<Exception>();
// Processa em lotes menores para performance
var batchSize = 10;
for (int i = 0; i < documents.Count; i += batchSize)
{
var batch = documents.Skip(i).Take(batchSize);
var tasks = batch.Select(async doc =>
{
try
{
var id = await SaveDocumentAsync(doc);
return id;
}
catch (Exception ex)
{
errors.Add(ex);
_logger.LogError(ex, "Erro ao salvar documento '{Title}' em lote", doc.Title);
return null;
}
});
var batchResults = await Task.WhenAll(tasks);
ids.AddRange(batchResults.Where(id => id != null)!);
}
if (errors.Any())
{
_logger.LogWarning("Batch save completado com {ErrorCount} erros de {TotalCount} documentos",
errors.Count, documents.Count);
}
_logger.LogInformation("Batch save: {SuccessCount}/{TotalCount} documentos salvos no Chroma",
ids.Count, documents.Count);
return ids;
}
public async Task DeleteDocumentsBatchAsync(List<string> ids)
{
var errors = new List<Exception>();
// Processa em lotes para não sobrecarregar
var batchSize = 20;
for (int i = 0; i < ids.Count; i += batchSize)
{
var batch = ids.Skip(i).Take(batchSize);
var tasks = batch.Select(async id =>
{
try
{
await DeleteDocumentAsync(id);
return true;
}
catch (Exception ex)
{
errors.Add(ex);
_logger.LogError(ex, "Erro ao deletar documento {Id} em lote", id);
return false;
}
});
await Task.WhenAll(tasks);
}
if (errors.Any())
{
_logger.LogWarning("Batch delete completado com {ErrorCount} erros de {TotalCount} documentos",
errors.Count, ids.Count);
}
else
{
_logger.LogInformation("Batch delete: {TotalCount} documentos removidos do Chroma", ids.Count);
}
}
// ========================================
// ESTATÍSTICAS DO PROVIDER
// ========================================
public async Task<Dictionary<string, object>> GetProviderStatsAsync()
{
try
{
var baseStats = await _vectorSearchService.GetStatsAsync();
var totalDocs = await GetDocumentCountAsync();
// Adiciona estatísticas específicas do TextData
var projectIds = await GetAllProjectIds();
var projectStats = new Dictionary<string, int>();
foreach (var projectId in projectIds)
{
var count = await GetDocumentCountAsync(projectId);
projectStats[projectId] = count;
}
var enhancedStats = new Dictionary<string, object>(baseStats)
{
["text_service_provider"] = "Chroma",
["total_documents_via_text_service"] = totalDocs,
["projects_count"] = projectIds.Count,
["documents_by_project"] = projectStats,
["supports_batch_operations"] = true,
["supports_metadata"] = true,
["embedding_auto_generation"] = true
};
return enhancedStats;
}
catch (Exception ex)
{
return new Dictionary<string, object>
{
["provider"] = "Chroma",
["text_service_provider"] = "Chroma",
["health"] = "error",
["error"] = ex.Message,
["last_check"] = DateTime.UtcNow
};
}
}
// ========================================
// MÉTODOS AUXILIARES PRIVADOS
// ========================================
private static TextoComEmbedding ConvertToTextoComEmbedding(VectorSearchResult result)
{
return new TextoComEmbedding
{
Id = result.Id,
Titulo = result.Metadata?.GetValueOrDefault("title")?.ToString() ?? "",
Conteudo = result.Content,
ProjetoId = result.Metadata?.GetValueOrDefault("project_id")?.ToString() ?? "",
Embedding = Array.Empty<double>(), // Chroma não retorna embedding na busca
// Campos que podem não existir no Chroma
ProjetoNome = result.Metadata?.GetValueOrDefault("project_name")?.ToString() ?? "",
TipoDocumento = result.Metadata?.GetValueOrDefault("document_type")?.ToString() ?? "",
Categoria = result.Metadata?.GetValueOrDefault("category")?.ToString() ?? "",
Tags = result.Metadata?.GetValueOrDefault("tags") as string[] ?? Array.Empty<string>()
};
}
private async Task<List<string>> GetAllProjectIds()
{
try
{
// Esta é uma operação custosa no Chroma
// Em produção, seria melhor manter um cache de project IDs
// ou usar uma estrutura de dados separada
// Por agora, vamos usar uma busca com um vetor dummy para pegar todos os documentos
var dummyVector = new double[384]; // Assumindo embeddings padrão
var allResults = await _vectorSearchService.SearchSimilarAsync(
dummyVector,
projectId: null,
threshold: 0.0,
limit: 10000);
return allResults
.Select(r => r.Metadata?.GetValueOrDefault("project_id")?.ToString())
.Where(pid => !string.IsNullOrEmpty(pid))
.Distinct()
.ToList()!;
}
catch (Exception ex)
{
_logger.LogError(ex, "Erro ao recuperar IDs de projetos do Chroma");
return new List<string>();
}
}
private static DateTime? ParseDateTime(string? dateString)
{
if (string.IsNullOrEmpty(dateString))
return null;
if (DateTime.TryParse(dateString, out var date))
return date;
return null;
}
}
}
#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.