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 _logger; public ChromaTextDataService( IVectorSearchService vectorSearchService, ITextEmbeddingGenerationService embeddingService, ILogger 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[] 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> GetAll() { try { // Busca todos os projetos e depois todos os documentos var allDocuments = new List(); // 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> 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 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 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 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 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(), // 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> 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(), 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 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> SaveDocumentsBatchAsync(List documents) { var ids = new List(); var errors = new List(); // 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 ids) { var errors = new List(); // 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> 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(); foreach (var projectId in projectIds) { var count = await GetDocumentCountAsync(projectId); projectStats[projectId] = count; } var enhancedStats = new Dictionary(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 { ["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(), // 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() }; } private async Task> 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(); } } 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.