using AuthorBuddy.Web.Data; using LiteDB; using System.Security.Cryptography; using System.Text; namespace AuthorBuddy.Web.Services; public class RagService : IRagService { private readonly ILiteDatabase _db; private readonly IOllamaService _ollama; public RagService(ILiteDatabase db, IOllamaService ollama) { _db = db; _ollama = ollama; } public async Task IndexProjectAsync(int projectId, string rootPath, IProgress progress) { var files = Directory.GetFiles(rootPath, "*.md", SearchOption.AllDirectories) .Where(f => f.Contains("00_Drafts") || f.Contains("01_Fakten")); var metaCol = _db.GetCollection("documentMeta"); var vecCol = _db.GetCollection("vectors"); foreach (var file in files) { progress?.Report($"Indexiere: {Path.GetFileName(file)}..."); string content = await File.ReadAllTextAsync(file); string hash = ComputeHash(content); var meta = metaCol.FindOne(m => m.ProjectId == projectId && m.FilePath == file); if (meta != null && meta.Hash == hash) { var existingVec = vecCol.FindOne(v => v.DocumentMetaId == meta.Id); if (existingVec != null) { progress?.Report($" ↳ unverändert, übersprungen"); continue; } } var embedding = await _ollama.GetEmbeddingAsync(content); if (embedding.Length == 0) { progress?.Report($" ↳ Fehler beim Embedding, übersprungen"); continue; } if (meta == null) { meta = new DocumentMetaEntity { ProjectId = projectId, FilePath = file, LastModified = File.GetLastWriteTimeUtc(file), Hash = hash, FileType = ".md" }; metaCol.Insert(meta); } else { meta.LastModified = File.GetLastWriteTimeUtc(file); meta.Hash = hash; metaCol.Update(meta); } var vec = vecCol.FindOne(v => v.DocumentMetaId == meta.Id); if (vec != null) { vec.Text = content; vec.Hash = hash; vec.Embedding = embedding; vecCol.Update(vec); } else { vecCol.Insert(new DocumentVector { DocumentMetaId = meta.Id, ProjectId = projectId, Text = content, Hash = hash, Embedding = embedding }); } progress?.Report($" ↳ ✓ {embedding.Length} Dimensionen"); } progress?.Report("Indexierung abgeschlossen."); } public async Task> SearchAsync(int projectId, string query, int topN = 5) { var queryVec = await _ollama.GetEmbeddingAsync(query); if (queryVec.Length == 0) return new List(); var vecCol = _db.GetCollection("vectors"); var metaCol = _db.GetCollection("documentMeta"); var vectors = vecCol.Find(v => v.ProjectId == projectId).ToList(); var results = new List(); foreach (var vec in vectors) { var sim = CosineSimilarity(queryVec, vec.Embedding); if (sim > 0) { var meta = metaCol.FindById(vec.DocumentMetaId); results.Add(new RagSearchResult { Text = vec.Text.Length > 500 ? vec.Text[..500] + "..." : vec.Text, FilePath = meta?.FilePath ?? "", Similarity = sim }); } } return results.OrderByDescending(r => r.Similarity).Take(topN).ToList(); } private static string ComputeHash(string content) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content)); return Convert.ToHexStringLower(bytes); } private static double CosineSimilarity(float[] a, float[] b) { double dotProduct = 0.0, l2A = 0.0, l2B = 0.0; for (int i = 0; i < a.Length; i++) { dotProduct += a[i] * b[i]; l2A += Math.Pow(a[i], 2); l2B += Math.Pow(b[i], 2); } return dotProduct / (Math.Sqrt(l2A) * Math.Sqrt(l2B)); } }