using BLAIzor.Data; using BLAIzor.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Identity.Client; using BLAIzor.Helpers; using Qdrant.Client.Grpc; using System.Collections; namespace BLAIzor.Services { public class ContentEditorService { #region Private Fields private readonly OpenAIApiService _openAIApiService; private readonly QDrantService _qDrantService; private readonly HtmlSnippetProcessor _htmlSnippetProcessor; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ScopedContentService _scopedContentService; private readonly OpenAIEmbeddingService _openAIEmbeddingService; private readonly LocalEmbeddingService _localEmbeddingService; private readonly ISimpleLogger _logger; public static IConfiguration? _configuration; #endregion #region Constructor /// /// Initializes a new instance of the class. /// /// The OpenAI API service. /// The Qdrant service. /// The OpenAI embedding service. /// The local embedding service. /// The HTML snippet processor. /// The service scope factory for creating database contexts. /// The scoped content service. /// The application configuration. public ContentEditorService(OpenAIApiService openAIApiService, QDrantService qDrantService, OpenAIEmbeddingService openAIEmbeddingService, LocalEmbeddingService localEmbeddingService, HtmlSnippetProcessor htmlSnippetProcessor, IServiceScopeFactory serviceScopeFactory, ScopedContentService scopedContentService, IConfiguration? configuration, ISimpleLogger logger) { _openAIApiService = openAIApiService; _qDrantService = qDrantService; _openAIEmbeddingService = openAIEmbeddingService; _localEmbeddingService = localEmbeddingService; _htmlSnippetProcessor = htmlSnippetProcessor; _serviceScopeFactory = serviceScopeFactory; _scopedContentService = scopedContentService; _configuration = configuration; _logger = logger; } #endregion #region Private Helper Methods /// /// Retrieves the AI embedding service setting from the configuration. /// /// The name of the embedding service (e.g., "local", "openai"). private string GetAiEmbeddingSettings() => _configuration?.GetSection("AiSettings")?.GetValue("EmbeddingService") ?? string.Empty; /// /// Removes content chunks and their corresponding Qdrant entries by their IDs. /// /// A list of content chunk IDs to remove. /// The name of the Qdrant collection. /// A representing the asynchronous operation. private async Task RemoveChunksAndQdrantEntriesByIdsAsync(List chunkIds, string collectionName) { using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var chunks = await db.ContentChunks .Where(c => chunkIds.Contains(c.Id)) .ToListAsync(); var pointIds = chunks.Select(c => Guid.Parse(c.QdrantPointId)).ToArray(); await _qDrantService.DeletePointsAsync(pointIds, collectionName); db.ContentChunks.RemoveRange(chunks); await db.SaveChangesAsync(); } /// /// Generates a unique vector collection name for a given site. /// /// The site information. /// A generated vector collection name. public string GetGeneratedVectorCollectionName(SiteInfo siteInfo) { var safeName = siteInfo.SiteName?.ToLower().Replace(" ", "_").Replace("-", "_"); return $"site_{safeName}_{Guid.NewGuid().ToString().Substring(0, 8)}"; } #endregion #region MenuItem CRUD Operations /// /// Adds a new menu item to the database. /// /// The menu item to add. /// The added menu item with its generated ID. public async Task AddMenuItemAsync(MenuItem menuItem) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.MenuItems.AddAsync(menuItem); await _context.SaveChangesAsync(); return result.Entity; } } /// /// Adds multiple menu items to the database. /// /// A list of menu items to add. /// The number of state entries written to the database. public async Task AddMenuItemsAsync(List menuItems) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); _context.MenuItems.AddRange(menuItems); var result = await _context.SaveChangesAsync(); return result; } } /// /// Retrieves all menu items for a specific site. /// /// The ID of the site. /// A list of menu items belonging to the specified site. public async Task> GetMenuItemsBySiteIdAsync(int siteInfoId) { await _logger.InfoAsync($"GetMenuItemsBySiteIdAsync: method called", siteInfoId.ToString()); using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.MenuItems .Where(m => m.SiteInfoId == siteInfoId) .ToListAsync(); return result; } } /// /// Retrieves all top-level menu items for a specific site, including their children. /// /// The ID of the site. /// A list of top-level menu items with their associated children. public async Task> GetMenuItemsBySiteIdWithChildrenAsync(int siteInfoId) { await _logger.InfoAsync($"GetMenuItemsBySiteIdWithChildrenAsync: method called", siteInfoId.ToString()); using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.MenuItems .Where(m => m.SiteInfoId == siteInfoId && m.ParentId == null) .Include(m => m.Children) .ToListAsync(); return result; } } /// /// Updates an existing menu item in the database. /// /// The menu item with updated information. /// The updated menu item. /// Thrown if the menu item is not found. public async Task UpdateMenuItemAsync(MenuItem menuItem) { using var scope = _serviceScopeFactory.CreateScope(); var _context = scope.ServiceProvider.GetRequiredService(); var existingMenuItem = await _context.MenuItems.FindAsync(menuItem.Id); if (existingMenuItem == null) { throw new Exception("MenuItem not found."); } existingMenuItem.Name = menuItem.Name; existingMenuItem.Slug = menuItem.Slug; existingMenuItem.ParentId = menuItem.ParentId; existingMenuItem.StoredHtml = menuItem.StoredHtml; existingMenuItem.SortOrder = menuItem.SortOrder; existingMenuItem.ShowInMainMenu = menuItem.ShowInMainMenu; await _context.SaveChangesAsync(); return existingMenuItem; } /// /// Deletes a menu item from the database. /// /// The ID of the menu item to delete. /// A representing the asynchronous operation. /// Thrown if the menu item is not found. public async Task DeleteMenuItemAsync(int menuItemId) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var menuItem = await _context.MenuItems.FindAsync(menuItemId); if (menuItem == null) { throw new Exception("MenuItem not found."); } _context.MenuItems.Remove(menuItem); await _context.SaveChangesAsync(); } } #endregion #region ContentGroup Operations /// /// Retrieves the first ContentGroup associated with the given SiteInfoId. /// Returns null if not found. /// /// The ID of the site information. /// The first ContentGroup found, or null if none exists. public async Task GetContentGroupBySiteInfoIdAsync(int siteInfoId) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.ContentGroups .Where(cg => cg.SiteInfoId == siteInfoId) .OrderBy(cg => cg.Id) .FirstOrDefaultAsync(); return result; } } /// /// Returns a list of ContentGroups for a site, with optional filtering by type or slug. /// /// The ID of the site information. /// Optional. The type of the content group to filter by. /// Optional. The slug of the content group to filter by. /// A list of matching ContentGroups. public async Task> GetContentGroupsBySiteInfoIdAsync(int siteInfoId, string? type = null, string? slug = null) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var query = _context.ContentGroups .Include(cg => cg.SiteInfo) .Include(cg => cg.Items) .ThenInclude(ci => ci.Chunks) .AsQueryable(); query = query.Where(cg => cg.SiteInfoId == siteInfoId); if (!string.IsNullOrWhiteSpace(type)) query = query.Where(cg => cg.Type == type); if (!string.IsNullOrWhiteSpace(slug)) query = query.Where(cg => cg.Slug == slug); return query.ToList(); } } /// /// Updates an existing content group in the database. /// /// The content group with updated information. /// The updated content group. /// Thrown if the content group is not found. public async Task UpdateContentGroupByIdAsync(ContentGroup updatedGroup) { using var scope = _serviceScopeFactory.CreateScope(); var _context = scope.ServiceProvider.GetRequiredService(); var existingGroup = await _context.ContentGroups.FindAsync(updatedGroup.Id); if (existingGroup == null) throw new Exception("ContentGroup not found."); existingGroup.Name = updatedGroup.Name; existingGroup.Slug = updatedGroup.Slug; existingGroup.Type = updatedGroup.Type; existingGroup.VectorSize = updatedGroup.VectorSize; existingGroup.EmbeddingModel = updatedGroup.EmbeddingModel; existingGroup.LastUpdated = DateTime.UtcNow; existingGroup.Version = updatedGroup.Version; _context.ContentGroups.Update(existingGroup); await _context.SaveChangesAsync(); return existingGroup; } /// /// Deletes a content group by its ID. /// /// The ID of the content group to delete. /// true if the content group was deleted, false otherwise. public async Task DeleteContentGroupByIdAsync(int contentGroupId) { using var scope = _serviceScopeFactory.CreateScope(); var _context = scope.ServiceProvider.GetRequiredService(); var group = await _context.ContentGroups.FindAsync(contentGroupId); if (group == null) return false; _context.ContentGroups.Remove(group); await _context.SaveChangesAsync(); return true; } /// /// Creates a new content group in the database. /// /// The new content group to create. /// The newly created content group with its generated ID and timestamps. public async Task CreateContentGroupAsync(ContentGroup newGroup) { using var scope = _serviceScopeFactory.CreateScope(); var _context = scope.ServiceProvider.GetRequiredService(); newGroup.CreatedAt = DateTime.UtcNow; newGroup.LastUpdated = DateTime.UtcNow; _context.ContentGroups.Add(newGroup); await _context.SaveChangesAsync(); return newGroup; } /// /// Returns all ContentGroups for a site, with optional filtering by type. /// /// The ID of the site information. /// Optional. The type of the content group to filter by. /// A list of matching ContentGroups. public async Task> GetAllContentGroupsBySiteInfoIdAsync(int siteInfoId, string? type = null) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var query = _context.ContentGroups .Where(cg => cg.SiteInfoId == siteInfoId); if (!string.IsNullOrWhiteSpace(type)) query = query.Where(cg => cg.Type == type); return await query .OrderBy(cg => cg.Name) .ToListAsync(); } } #endregion #region ContentItem Operations /// /// Retrieves a content item by its ID, optionally filtered by type. /// /// The ID of the content item. /// Optional. The type of the content item to filter by. /// The matching content item with its chunks, or null if not found. public async Task GetContentItemByIdAsync(int contentItemId, string? type = null) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var query = _context.ContentItems .Where(cg => cg.Id == contentItemId).Include(ci => ci.Chunks); return query.FirstOrDefault(); } } /// /// Retrieves a content item by its ID, including related ContentGroup, Chunks, and SiteInfo. /// /// The ID of the content item. /// The matching content item, or null if not found. public async Task GetContentItemByIdAsync(int id) { using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var query = db.ContentItems.Where(x => x.Id == id).Include(ci => ci.ContentGroup).Include(ci => ci.Chunks).Include(ci => ci.ContentGroup.SiteInfo); return query.FirstOrDefault(); } /// /// Creates a new content item, chunks its content, generates embeddings, and inserts them into Qdrant. /// /// The content item to create. /// The name of the Qdrant collection to insert into. /// The created content item. /// Thrown if the saved ContentItem cannot be retrieved. public async Task CreateContentItemAsync(ContentItem item, string collectionName) { await _logger.InfoAsync($"CreateContentItemAsync: method called", item.Id.ToString()); using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); item.CreatedAt = DateTime.UtcNow; item.LastUpdated = DateTime.UtcNow; db.ContentItems.Add(item); await db.SaveChangesAsync(); var fullItem = await db.ContentItems .Include(ci => ci.ContentGroup) .FirstOrDefaultAsync(ci => ci.Id == item.Id); if (fullItem == null) throw new Exception("Failed to retrieve saved ContentItem."); if (!string.IsNullOrEmpty(item.Content)) { var chunks = ChunkingHelper.SplitStructuredText(item.Content, 3000); var vectorizedChunks = await VectorizeChunksWithGuidsAsync(chunks, fullItem, collectionName); var chunkEntities = vectorizedChunks.Select((chunk, index) => new ContentChunk { ContentItemId = item.Id, QdrantPointId = chunk.UId, ChunkIndex = index, CreatedAt = DateTime.UtcNow }).ToList(); db.ContentChunks.AddRange(chunkEntities); } await db.SaveChangesAsync(); return item; } /// /// Vectorizes a list of content chunks, generates unique GUIDs for each, and inserts them into Qdrant. /// /// The list of text chunks to vectorize. /// The content item associated with these chunks. /// The name of the Qdrant collection. /// A list of representing the vectorized chunks. public async Task> VectorizeChunksWithGuidsAsync(List chunks, ContentItem item, string collectionName) { var result = new List(); foreach (var (chunk, index) in chunks.Select((c, i) => (c, i))) { var combinedText = chunk; float[] embedding = []; var embeddingServiceProvider = GetAiEmbeddingSettings(); if (embeddingServiceProvider == "local") { embedding = await _localEmbeddingService.GenerateEmbeddingAsync(combinedText); } else { embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText); } var uid = Guid.NewGuid().ToString(); result.Add(new WebPageContent { Id = Guid.Parse(uid), UId = uid, SiteId = item.ContentGroup.SiteInfoId, Type = "content-item", Name = item.Title, Description = item.Description, Content = chunk, Vectors = embedding, LastUpdated = DateTime.UtcNow }); } await _qDrantService.QDrantInsertManyAsync(result, collectionName); return result; } /// /// Vectorizes a list of content chunks and inserts them into Qdrant. /// /// The list of text chunks to vectorize. /// The content item associated with these chunks. /// The name of the Qdrant collection. /// A list of representing the vectorized chunks. public async Task> VectorizeChunksAsync(List chunks, ContentItem item, string collectionName) { var result = new List(); foreach (var (chunk, index) in chunks.Select((c, i) => (c, i))) { var combinedText = $"{chunk}"; float[] embedding = []; var embeddingServiceProvider = GetAiEmbeddingSettings(); if (embeddingServiceProvider == "local") { embedding = await _localEmbeddingService.GenerateEmbeddingAsync(combinedText); } else { embedding = await _openAIEmbeddingService.GenerateEmbeddingAsync(combinedText); } var uid = Guid.NewGuid(); var webChunk = new WebPageContent { Id = uid, UId = uid.ToString(), SiteId = item.ContentGroup.SiteInfoId, Type = "content-item", Name = item.Title, Description = item.Description, Content = chunk, Vectors = embedding, LastUpdated = DateTime.UtcNow }; result.Add(webChunk); } await _qDrantService.QDrantInsertManyAsync(result, collectionName); return result; } /// /// Forces a re-chunking and re-embedding of all content items within a specified content group. /// /// The ID of the content group to re-chunk. /// The name of the Qdrant collection. If null, the existing collection name will be used. /// true if the operation was successful, false otherwise. public async Task ForceRechunkContentGroupAsync(int contentGroupId, string collectionName = null) { using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var group = await db.ContentGroups .Include(g => g.Items) .ThenInclude(ci => ci.Chunks) .Include(g => g.SiteInfo) .FirstOrDefaultAsync(g => g.Id == contentGroupId); if (group == null) return false; foreach (var item in group.Items) { await SaveAndSyncContentItemAsync(item, collectionName, forceRechunk: true); } return true; } /// /// Forces the recreation of a Qdrant collection for a given site, including re-chunking and re-embedding all content. /// /// The ID of the site for which to recreate the collection. /// true if the collection was successfully recreated and content re-synced, false otherwise. /// Thrown if the new Qdrant collection cannot be created. public async Task ForceRecreateQdrantCollectionAsync(int siteInfoId) { using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var site = await db.SiteInfos .Include(s => s.ContentGroups) .ThenInclude(g => g.Items) .ThenInclude(i => i.Chunks) .FirstOrDefaultAsync(s => s.Id == siteInfoId); if (site == null) return false; string oldCollectionName = site.VectorCollectionName; await _qDrantService.DeleteCollectionAsync(oldCollectionName); string newCollectionName = GetGeneratedVectorCollectionName(site); bool created = await _qDrantService.CreateQdrantCollectionAsync(newCollectionName); if (!created) throw new Exception($"Failed to create Qdrant collection '{newCollectionName}'."); site.VectorCollectionName = newCollectionName; await UpdateSiteInfoAsync(site); foreach (var group in site.ContentGroups) { foreach (var item in group.Items.ToList()) { var dto = new ContentItem { Id = item.Id, Title = item.Title, Description = item.Description, Content = item.Content, Language = item.Language, Tags = item.Tags, IsPublished = item.IsPublished, Version = item.Version, LastUpdated = item.LastUpdated, ContentGroupId = item.ContentGroupId }; await SaveAndSyncContentItemAsync(dto, newCollectionName, forceRechunk: true); } } return true; } /// /// Retrieves all content items belonging to a specific content group. /// /// The ID of the content group. /// A list of content items in the specified group, ordered by last updated date descending. public async Task> GetContentItemsByGroupIdAsync(int contentGroupId) { using (var scope = _serviceScopeFactory.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); return await context.ContentItems .Where(ci => ci.ContentGroupId == contentGroupId) .OrderByDescending(ci => ci.LastUpdated) .ToListAsync(); } } /// /// Retrieves all content items belonging to a specific content group. /// /// The ID of the content group. /// A list of content items in the specified group, ordered by last updated date descending. public async Task> GetAllContentItemsBySiteIdAsync(int siteId) { using (var scope = _serviceScopeFactory.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); //var site = await GetSiteInfoByIdAsync(siteId); //List contentList = new List(); //foreach (var item in site.ContentGroups) //{ // var contentItems = await GetContentItemsByGroupIdAsync(item.Id); // contentList.AddRange(contentItems); //} //return contentList; return await context.ContentItems .Where(ci => ci.ContentGroup.SiteInfoId == siteId) .Include(ci => ci.Chunks) .ToListAsync(); } } /// /// Creates a new content item in the database. /// /// The new content item to create. /// The newly created content item with its generated ID and timestamps. public async Task CreateContentItemAsync(ContentItem newItem) { using var scope = _serviceScopeFactory.CreateScope(); var _context = scope.ServiceProvider.GetRequiredService(); newItem.CreatedAt = DateTime.UtcNow; newItem.LastUpdated = DateTime.UtcNow; _context.ContentItems.Add(newItem); await _context.SaveChangesAsync(); return newItem; } /// /// Saves changes to a content item and synchronizes its chunks and embeddings with Qdrant. /// If content has changed or re-chunking is forced, old chunks are removed and new ones are created and embedded. /// /// The content item data transfer object with updated information. /// The name of the Qdrant collection to sync with. /// If set to true, forces re-chunking and re-embedding even if content hasn't changed. /// The updated and synchronized content item. /// Thrown if the content item is not found. public async Task SaveAndSyncContentItemAsync(ContentItem dto, string collectionName, bool forceRechunk = false) { using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var existing = await db.ContentItems .Include(ci => ci.Chunks) .Include(ci => ci.ContentGroup) .FirstOrDefaultAsync(ci => ci.Id == dto.Id); if (existing == null) throw new Exception("ContentItem not found."); bool contentChanged = existing.Content?.Trim() != dto.Content?.Trim(); existing.Title = dto.Title; existing.Description = dto.Description; existing.Content = dto.Content; existing.Language = dto.Language; existing.Tags = dto.Tags; existing.IsPublished = dto.IsPublished; existing.LastUpdated = dto.LastUpdated; if (contentChanged || forceRechunk) { existing.Version++; var chunkIds = existing.Chunks.Select(c => c.Id).ToList(); if (chunkIds.Any()) { await RemoveChunksAndQdrantEntriesByIdsAsync(chunkIds, collectionName); foreach (var chunk in existing.Chunks.ToList()) { db.Entry(chunk).State = EntityState.Detached; } existing.Chunks.Clear(); } var newChunks = ChunkingHelper.SplitStructuredText(existing.Content, 3000); var webChunks = await VectorizeChunksAsync(newChunks, existing, collectionName); var chunkEntities = webChunks.Select((w, i) => new ContentChunk { ContentItemId = existing.Id, ChunkIndex = i, QdrantPointId = w.Id.Uuid, CreatedAt = DateTime.UtcNow }).ToList(); db.ContentChunks.AddRange(chunkEntities); } await db.SaveChangesAsync(); return existing; } /// /// Groups content items by their content group type. /// /// The website content model containing content items. /// A dictionary where the key is the content group type and the value is a list of content item models. public static Dictionary> GroupContentItemsByType(WebsiteContentModel model) { return model.ContentItems .GroupBy(ci => ci.ContentItem.ContentGroup.Type) .ToDictionary(g => g.Key, g => g.ToList()); } /// /// Deletes a content item by its ID. /// /// The ID of the content item to delete. /// true if the content item was deleted, false otherwise. public async Task DeleteContentItemByIdAsync(int id) { using var scope = _serviceScopeFactory.CreateScope(); var _context = scope.ServiceProvider.GetRequiredService(); var item = await _context.ContentItems.FindAsync(id); if (item != null) { _context.ContentItems.Remove(item); await _context.SaveChangesAsync(); return true; } return false; } /// /// Retrieves the Qdrant point IDs associated with content chunks belonging to a specific content group. /// /// The ID of the content group. /// An array of Qdrant point IDs (represented as chunk indices). public async Task GetPointIdsByContentGroupIdAsync(int contentGroupId) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); return await _context.ContentChunks .Where(chunk => chunk.ContentItem.ContentGroupId == contentGroupId) .Select(chunk => chunk.ChunkIndex) .ToArrayAsync(); } } #endregion #region SiteInfo Operations /// /// Retrieves site information by its ID. /// /// The ID of the site information. /// The matching site information, or the first site information if not found. public async Task GetSiteInfoByIdAsync(int SiteInfoId) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.SiteInfos.Where(x => x.Id == SiteInfoId).FirstOrDefaultAsync(); if (result == null) { return await _context.SiteInfos.FirstOrDefaultAsync(); } else { return result; } } } /// /// Retrieves site information by its ID, including associated form definitions. /// /// The ID of the site. /// The matching site information with form definitions, or a new if not found. public async Task GetSiteInfoWithFormsByIdAsync(int siteId) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.SiteInfos.Include(s => s.FormDefinitions).FirstOrDefaultAsync(s => s.Id == siteId); if (result == null) { return new SiteInfo(); } else { return result; } } } /// /// Retrieves site information by its name. /// /// The name of the site. /// The matching site information, or the first site information if not found. public async Task GetSiteInfoByNameAsync(string siteName) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.SiteInfos.Where(x => x.SiteName == siteName).FirstOrDefaultAsync(); if (result == null) { return await _context.SiteInfos.FirstOrDefaultAsync(); } else { return result; } } } /// /// Retrieves site information by its default URL. /// /// The default URL of the site. /// The matching site information, or the first site information if not found. public async Task GetSiteInfoByUrlAsync(string url) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); var result = await _context.SiteInfos.Where(x => x.DefaultUrl == url).FirstOrDefaultAsync(); if (result == null) { return await _context.SiteInfos.FirstOrDefaultAsync(); } else { return result; } } } /// /// Updates existing site information in the database. /// /// The site information with updated details. /// A representing the asynchronous operation. public async Task UpdateSiteInfoAsync(SiteInfo siteInfo) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); _context.SiteInfos.Update(siteInfo); await _context.SaveChangesAsync(); } } /// /// Retrieves all sites associated with a specific user. /// /// The ID of the user. /// A list of sites belonging to the specified user. public async Task> GetUserSitesAsync(string userId) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); return await _context.SiteInfos .Where(s => s.UserId == userId) .ToListAsync(); } } /// /// Retrieves all site information records from the database. /// /// A list of all site information records. public async Task> GetAllSitesAsync() { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); return await _context.SiteInfos.ToListAsync(); } } /// /// Adds new site information to the database and creates a corresponding Qdrant collection. /// /// The site information to add. /// The added site information. /// Thrown if is null. /// Thrown if an error occurs during the site creation or Qdrant collection creation. public async Task AddSiteInfoAsync(SiteInfo siteInfo) { using (var scope = _serviceScopeFactory.CreateScope()) { var _context = scope.ServiceProvider.GetRequiredService(); if (siteInfo == null) throw new ArgumentNullException(nameof(siteInfo), "SiteInfo cannot be null."); try { if (string.IsNullOrEmpty(siteInfo.VectorCollectionName)) { siteInfo.VectorCollectionName = GetGeneratedVectorCollectionName(siteInfo); } await _context.SiteInfos.AddAsync(siteInfo); var result = await _context.SaveChangesAsync(); if (result > 0) { bool checkResult = await _qDrantService.CollectionExistsAsync(siteInfo.VectorCollectionName); if (checkResult) { siteInfo.VectorCollectionName = GetGeneratedVectorCollectionName(siteInfo); _context.SiteInfos.Update(siteInfo); await _context.SaveChangesAsync(); bool qresult = await _qDrantService.CreateQdrantCollectionAsync(siteInfo.VectorCollectionName); if (!qresult) { throw new Exception("Failed to create Qdrant collection for the site."); } } else { bool qresult = await _qDrantService.CreateQdrantCollectionAsync(siteInfo.VectorCollectionName); if (!qresult) { throw new Exception("Failed to create Qdrant collection for the site."); } } } return siteInfo; } catch (Exception ex) { throw new InvalidOperationException("An error occurred while adding the site info.", ex); } } } #endregion ///TEMPORARY /// public async Task MigrateQdrantToContentItemsAsync(int siteId, string collectionName) { using var scope = _serviceScopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var site = await GetSiteInfoByIdAsync(siteId); // 1. Get menu items var menuItems = await GetMenuItemsBySiteIdAsync(siteId); PointId[] pointIds = new PointId[menuItems.Count]; for (int i = 0; i < menuItems.Count; i++) { // Ensure SortOrder is set to i+1 (1-based index) pointIds[i] = Convert.ToUInt64(menuItems[i].SortOrder); } if (!pointIds.Any()) return false; // 2. Get points from Qdrant var qdrantPoints = await _qDrantService.GetPointsFromQdrantAsyncByIntegerPointIds(collectionName, pointIds); // 3. Create new content group var contentGroup = new ContentGroup { Name = "Pages", Type = "page", SiteInfoId = siteId, CreatedAt = DateTime.UtcNow, EmbeddingModel = "openai", Slug = $"group-{DateTime.UtcNow.Ticks}", Version = 0, LastUpdated = DateTime.UtcNow }; db.ContentGroups.Add(contentGroup); await db.SaveChangesAsync(); // Save to get ID // 4. Create ContentItems from Qdrant points foreach (var point in qdrantPoints) { var item = new ContentItem { ContentGroupId = contentGroup.Id, Title = point.Name, Description = point.Description, Content = point.Content, Language = "en", Tags = "", IsPublished = true, Version = 1, CreatedAt = DateTime.UtcNow, LastUpdated = DateTime.UtcNow }; db.ContentItems.Add(item); } await db.SaveChangesAsync(); return true; } } }