diff --git a/Extensions/XAiModelsExtensions.cs b/Extensions/XAiModelsExtensions.cs new file mode 100644 index 0000000..73d36d1 --- /dev/null +++ b/Extensions/XAiModelsExtensions.cs @@ -0,0 +1,108 @@ +using xAiModels.Models.Dtos; +using xAiModels.Models.Entities; +using xCommons.Extensions; + +namespace xAiModels.Extensions +{ + /// + /// Extensions Methods for XAiModels ... + /// + public static class XAiModelsExtensions + { + /// + /// Validate a Project for Add Or Update ... + /// + /// + /// + public static bool IsValidForAddOrUpdate(this XAiProject source) + { + // + var result = + !source.IsNullOrDefault() && + !source.OwnerId.IsNullOrEmpty() && + !source.Title.IsNullOrEmpty() && + !source.Prompt.IsNullOrEmpty(); + + // + return result; + } + public static bool IsValidForAddOrUpdate(this XAiProjectDto source) + { + // + var result = + !source.IsNullOrDefault() && + !source.OwnerId.IsNullOrEmpty() && + !source.Title.IsNullOrEmpty() && + !source.Prompt.IsNullOrEmpty(); + + // + return result; + } + + /// + /// Validate a Conversation for Add Or Update ... + /// + /// + /// + public static bool IsValidForAddOrUpdate(this XAiConversation source) + { + // + var result = + !source.IsNullOrDefault() && + !source.OwnerId.IsNullOrEmpty() && + !source.Title.IsNullOrEmpty() && + !source.ProjectId.IsNull() && + !source.ProjectId.IsDefaultGuid(); + + // + return result; + } + public static bool IsValidForAddOrUpdate(this XAiConversationDto source) + { + // + var result = + !source.IsNullOrDefault() && + !source.OwnerId.IsNullOrEmpty() && + !source.Title.IsNullOrEmpty() && + !source.ProjectId.IsNull() && + !source.ProjectId.IsDefaultGuid(); + + // + return result; + } + + /// + /// Validate a Message for Add Or Update ... + /// + /// + /// + public static bool IsValidForAddOrUpdate(this XAiMessage source) + { + // + var result = + !source.IsNullOrDefault() && + !source.OwnerId.IsNullOrEmpty() && + source.Role.IsValid() && + !source.ConversationId.IsNull() && + !source.ConversationId.IsDefaultGuid() && + !source.Content.IsNullOrEmpty(); + + // + return result; + } + public static bool IsValidForAddOrUpdate(this XAiMessageDto source) + { + // + var result = + !source.IsNullOrDefault() && + !source.OwnerId.IsNullOrEmpty() && + source.Role.IsValid() && + !source.ConversationId.IsNull() && + !source.ConversationId.IsDefaultGuid() && + !source.Content.IsNullOrEmpty(); + + // + return result; + } + } +} \ No newline at end of file diff --git a/Interfaces/IXAiDataProvider.cs b/Interfaces/IXAiDataProvider.cs index 38ffced..c305df5 100644 --- a/Interfaces/IXAiDataProvider.cs +++ b/Interfaces/IXAiDataProvider.cs @@ -1,3 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using xAiModels.Models.Dtos; + namespace xAiModels.Interfaces { /// @@ -5,6 +11,94 @@ namespace xAiModels.Interfaces /// public interface IXAiDataProvider { - + // + #region Loaders ... + /// + /// Load Projects List of Specified Owner ... + /// + /// + /// + /// + /// + /// + Task> LoadProjects( + string ownerId, + bool fillConversations = false, + bool fillConversationMessages = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load List of Conversations for Specified Project ... + /// + /// + /// + /// + /// + Task> LoadConversations( + Guid projectId, + bool fillMessages = false, + CancellationToken cancellationToken = default + ); + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ); + #endregion + + // + #region Creator/Adders ... + /// + /// Create Project ... + /// + /// + /// + /// + /// + /// + Task CreateProject( + string ownerId, + XAiProjectDto item = null, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Create Conversation ... + /// + /// + /// + /// + /// + /// + Task CreateConversation( + Guid projectId, + XAiConversationDto item = null, + string connectionId = null, + CancellationToken cancellationToken = default + ); + + /// + /// Add Message ... + /// + /// + /// + /// + /// + /// + Task AddMessage( + Guid conversationId, + XAiMessageDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ); + #endregion } } \ No newline at end of file diff --git a/Providers/XAiDataProvider.cs b/Providers/XAiDataProvider.cs index 149f39a..49cf690 100644 --- a/Providers/XAiDataProvider.cs +++ b/Providers/XAiDataProvider.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; +using MongoDB.Driver.Core.Connections; using xAiModels.Constants; +using xAiModels.Extensions; using xAiModels.Interfaces; using xAiModels.Interfaces.Dtos; using xAiModels.Models.Dtos; @@ -21,6 +24,8 @@ namespace xAiModels.Providers /// public class XAiDataProvider : IXAiDataProvider { + // + #region Properties ... private readonly IXStringProvider stringProvider; private readonly XAppConfiguration appConfiguration; private readonly IHubContext hub; @@ -30,6 +35,7 @@ namespace xAiModels.Providers private readonly IXAiConversationRepositoryService conversationRepositoryService; private readonly IXAiConversationTitleResourceProvider conversationTitleResourceProvider; private readonly IXAiProjectDescriptionResourceProvider projectDescriptionResourceProvider; + #endregion public XAiDataProvider( IXStringProvider stringProvider, @@ -55,7 +61,415 @@ namespace xAiModels.Providers } // - #region Actions ... + #region Loaders ... + /// + /// Load Projects List of Specified Owner ... + /// + /// + /// + /// + /// + /// + public async Task> LoadProjects( + string ownerId, + bool fillConversations = false, + bool fillConversationMessages = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Handle Default Project and Conversation ... + bool hasDefault = await HasDefaultProject( + ownerId: ownerId, + cancellationToken: cancellationToken + ); + if (!hasDefault) + { + // + await CreateDefaultProject( + ownerId: ownerId, + connectionId: null, + cancellationToken: cancellationToken + ); + } + + // + // Retrieve Project List ... + var result = await projectRepositoryService.FindManyAsync( + includeBuilder: null, + ignoreSoftDeleteds: true, + fillConversations: fillConversations, + predicate: x => x.OwnerId == ownerId, + cancellationToken: cancellationToken, + orderBuilder: x => x.OrderBy(y => y.CreatedOn), + fillConversationMessages: fillConversationMessages + ); + + // + return result; + } + + /// + /// Load List of Conversations for Specified Project ... + /// + /// + /// + /// + /// + public async Task> LoadConversations( + Guid projectId, + bool fillMessages = false, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsNull() || + projectId.IsDefaultGuid() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Conversation's List ... + var result = await conversationRepositoryService.FindManyAsync( + includeBuilder: null, + ignoreSoftDeleteds: false, + fillMessages: fillMessages, + cancellationToken: cancellationToken, + predicate: x => x.ProjectId == projectId, + orderBuilder: x => x.OrderBy(y => y.CreatedOn) + ); + + // + return result; + } + + /// + /// Load Messages of Specified Conversation ... + /// + /// + /// + /// + public async Task> LoadMessages( + Guid conversationId, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (conversationId.IsNull() || + conversationId.IsDefaultGuid() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Message Lists of Specified Conversation ... + var result = await messageRepositoryService.FindManyAsync( + includeBuilder: null, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken, + orderBuilder: x => x.OrderBy(y => y.CreatedOn), + predicate: x => x.ConversationId == conversationId + ); + + // + return result; + } + #endregion + + // + #region Creator/Adders ... + public async Task CreateProject( + string ownerId, + XAiProjectDto item = null, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Check Project Provided or not ... + var hasProject = !item.IsNullOrDefault(); + + // + // Normalize Provided Project ... + if (hasProject) + { + // + item.CreatedOn = DateTime.UtcNow; + + // + // Validate Item ... + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + } + + // + XAiProjectDto result = null; + + // + // Create Provided Project ... + if (hasProject) + { + // + result = await projectRepositoryService.AddAsync( + item: item, + saveChanges: true, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handling Send Push Notification ... + if (!result.IsNullOrDefault() && + !connectionId.IsNullOrEmpty() + ) + { + // + await SendPush( + connectionId: connectionId, + cancellationToken: cancellationToken, + payLoad: item.ToJSON(camelCase: true), + action: XAiDataProviderHubActions.ProjectCreated.GetStringValue() + ); + } + } + // + // Check Default Project ... + else + { + // + // Check Default Project Exists or not ... + var hasDefault = await HasDefaultProject( + ownerId: ownerId, + cancellationToken: cancellationToken + ); + + // + // If Default Project Exists, Throw Exception ... + if (hasDefault) + { + XException.InvalidArgs.Throw(); + } + + // + // Create and Retrive Default Project ... + result = await GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Validate ... + if (result.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + return result; + } + + public async Task CreateConversation( + Guid projectId, + XAiConversationDto item = null, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (projectId.IsNull() || + projectId.IsDefaultGuid()) + { + XException.InvalidArgs.Throw(); + } + + // + // Handle Proejct ... + XAiProjectDto project = await projectRepositoryService.GetAsync( + id: projectId, + includeBuilder: null, + ignoreSoftDeleteds: true, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check Conversation Provided or not ... + var hasConversation = !item.IsNullOrDefault(); + + // + // Normalize Conversation if Provided ... + if (hasConversation) + { + // + item.ProjectId = project.Id; + item.CreatedOn = DateTime.UtcNow; + + // + // Validate Item ... + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + } + + // + XAiConversationDto result = null; + + // + // Create Provided Conversation ... + if (hasConversation) + { + // + result = await conversationRepositoryService.AddAsync( + item: item, + saveChanges: true, + fillMessages: false, + cancellationToken: cancellationToken + ); + + // + // Handling Send Push Notification ... + if (!result.IsNullOrDefault() && + !connectionId.IsNullOrEmpty() + ) + { + // + await SendPush( + connectionId: connectionId, + cancellationToken: cancellationToken, + payLoad: item.ToJSON(camelCase: true), + action: XAiDataProviderHubActions.ConversationCreated.GetStringValue() + ); + } + } + // + // Check Default Conversation ... + else + { + // + // Check Default Conversation Exists or not ... + var hasDefault = await HasDefaultConversation( + projectId: project.Id, + ownerId: project.OwnerId, + cancellationToken: cancellationToken + ); + + // + // If Default Project Exists, Throw Exception ... + if (hasDefault) + { + XException.InvalidArgs.Throw(); + } + + // + // Create and Retrive Default Conversation ... + result = await GetDefaultConversation( + projectId: project.Id, + ownerId: project.OwnerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + + // + // Validate ... + if (result.IsNullOrDefault()) + { + XException.ActionFailed.Throw(); + } + + // + return result; + } + + public async Task AddMessage( + Guid conversationId, + XAiMessageDto item, + string connectionId = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate Args ... + if (item.IsNullOrDefault() || + conversationId.IsNull() || + conversationId.IsDefaultGuid() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Normalize ... + item.CreatedOn = DateTime.UtcNow; + item.ConversationId = conversationId; + + // + // Validate Item ... + if (!item.IsValidForAddOrUpdate()) + { + XException.InvalidData.Throw(); + } + + // + var result = await messageRepositoryService.AddAsync( + item: item, + saveChanges: true, + cancellationToken: cancellationToken + ); + + // + // Handling Send Push Notification ... + if (!result.IsNullOrDefault() && + !connectionId.IsNullOrEmpty() + ) + { + // + await SendPush( + connectionId: connectionId, + cancellationToken: cancellationToken, + payLoad: item.ToJSON(camelCase: true), + action: XAiDataProviderHubActions.MessageCreated.GetStringValue() + ); + } + + // + return result; + } + #endregion + + // + #region Updaters ... #endregion // @@ -64,10 +478,12 @@ namespace xAiModels.Providers /// Check Default Project Exists or not ... /// /// + /// /// /// private async Task HasDefaultProject( string ownerId, + string connectionId = null, CancellationToken cancellationToken = default ) { @@ -117,6 +533,7 @@ namespace xAiModels.Providers // Check Default Project Exists or noit ... var isExists = await HasDefaultProject( ownerId: ownerId, + connectionId: connectionId, cancellationToken: cancellationToken ); if (!isExists) @@ -273,10 +690,14 @@ namespace xAiModels.Providers /// Check Default Conversation Exists or not ... /// /// + /// + /// /// /// private async Task HasDefaultConversation( string ownerId, + Guid projectId, + string connectionId = null, CancellationToken cancellationToken = default ) { @@ -288,9 +709,40 @@ namespace xAiModels.Providers return result; } + // + // Prepare Project ... + XAiProjectDto project = null; + if (projectId.IsNull() || + projectId.IsDefaultGuid() + ) + { + // + project = await GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + else + { + // + project = await projectRepositoryService.GetAsync( + id: project.Id, + includeBuilder: null, + ignoreSoftDeleteds: true, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + // // Check Default Owner Exists or not ... - result = !(await stringProvider.FindOne( + var resource = await stringProvider.FindOne( cancellationToken: cancellationToken, predicate: x => x.ResourceTitle @@ -303,8 +755,23 @@ namespace xAiModels.Providers x.TranslatedValue .ToNormalString() .Contains(XAiDataProviderConstants.XDefaultAiConversationSuffix) + ); + result = !resource.IsNullOrDefault(); + if (!result) + { + return result; + } + + // + result = !(await conversationRepositoryService.FindOneAsync( + cancellationToken: cancellationToken, + predicate: x => + x.ProjectId == projectId && + x.Title == resource.ResourceTitle )) .IsNullOrDefault(); + + // return result; } @@ -312,11 +779,13 @@ namespace xAiModels.Providers /// Check Default Conversation Exists or not ... /// /// + /// /// /// /// private async Task CreateDefaultConversation( string ownerId, + Guid projectId, string connectionId = null, CancellationToken cancellationToken = default ) @@ -333,17 +802,41 @@ namespace xAiModels.Providers var defaultLang = appConfiguration.DefaultLanguage; // - // Handle Default Project ... - var project = await GetDefaultProject( - ownerId: ownerId, - connectionId: connectionId, - cancellationToken: cancellationToken - ); + // Prepare Project ... + XAiProjectDto project = null; + if (projectId.IsNull() || + projectId.IsDefaultGuid() + ) + { + // + project = await GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + else + { + // + project = await projectRepositoryService.GetAsync( + id: project.Id, + includeBuilder: null, + ignoreSoftDeleteds: true, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } // // Check Default Owner Exists or not ... isValid = await HasDefaultConversation( ownerId: ownerId, + projectId: project.Id, cancellationToken: cancellationToken ); if (isValid) @@ -412,19 +905,61 @@ namespace xAiModels.Providers /// Create it if not Handled ... /// /// + /// /// /// /// private async Task GetDefaultConversation( string ownerId, + Guid projectId, string connectionId = null, CancellationToken cancellationToken = default ) { + // + // Validate Args ... + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Project ... + XAiProjectDto project = null; + if (projectId.IsNull() || + projectId.IsDefaultGuid() + ) + { + // + project = await GetDefaultProject( + ownerId: ownerId, + connectionId: connectionId, + cancellationToken: cancellationToken + ); + } + else + { + // + project = await projectRepositoryService.GetAsync( + id: project.Id, + includeBuilder: null, + ignoreSoftDeleteds: true, + fillConversations: false, + fillConversationMessages: false, + cancellationToken: cancellationToken + ); + } + if (project.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + // // Check Default Conversation Exists or noit ... var isExists = await HasDefaultConversation( ownerId: ownerId, + projectId: project.Id, + connectionId: connectionId, cancellationToken: cancellationToken ); if (!isExists) @@ -432,12 +967,14 @@ namespace xAiModels.Providers // await CreateDefaultConversation( ownerId: ownerId, + projectId: project.Id, connectionId: connectionId, cancellationToken: cancellationToken ); } // + // Detect Conversation Resource ... var resource = await stringProvider.FindOne( cancellationToken: cancellationToken, predicate: x => @@ -458,13 +995,15 @@ namespace xAiModels.Providers } // + // Find Default Conversation ... var result = await conversationRepositoryService.FindOneAsync( cancellationToken: cancellationToken, predicate: x => x.Title .Equals( resource.ResourceTitle, StringComparison.InvariantCultureIgnoreCase - ) + ) && + x.ProjectId == project.Id ); if (result.IsNullOrDefault()) { @@ -475,6 +1014,29 @@ namespace xAiModels.Providers return result; } + /// + /// Normalize and Validate Language ... + /// + /// + /// + private string NormalizeAndValidateLanguage(string language) + { + // + if (language.IsNullOrEmpty()) + { + language = appConfiguration.DefaultLanguage; + } + + // + if (language.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + return language; + } + /// /// Send Custom Push Message ... ///