diff --git a/AI/DI/AIExtensions.cs b/AI/DI/AIExtensions.cs index 3b830d8..ebd1eaa 100644 --- a/AI/DI/AIExtensions.cs +++ b/AI/DI/AIExtensions.cs @@ -1,6 +1,8 @@ +using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using xAiApi.AI.Interfaces; +using xAiApi.AI.Mppings; using xAiApi.AI.Services; namespace xAiApi.AI.DI @@ -16,7 +18,17 @@ namespace xAiApi.AI.DI ) { // + // Register AutoMapper Using Mapping Profiles ... + services.AddAutoMapper(typeof(MappingProfiles).Assembly); + + // + // TODO: Remove this ... services.AddSingleton(); + + // + // Since we Used Repositories in this Service, + // we have to Register it Scoped Lifetime ... + services.AddScoped(); } /// diff --git a/AI/Interfaces/IXAiProvider.cs b/AI/Interfaces/IXAiProvider.cs index a96b983..8674221 100644 --- a/AI/Interfaces/IXAiProvider.cs +++ b/AI/Interfaces/IXAiProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -21,7 +20,7 @@ namespace xAiApi.AI.Interfaces /// /// public XAiApiConfiguration Configuration { get; } - + // #region Helpers ... /// @@ -161,6 +160,8 @@ namespace xAiApi.AI.Interfaces ); #endregion + // + #region Actions ... /// /// Ask a Question ... /// @@ -177,5 +178,23 @@ namespace xAiApi.AI.Interfaces XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ); + + /// + /// Ask a Question in Streaming ... + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable AskStream( + string prompt, + string projectId = null, + string conversationId = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + #endregion } } \ No newline at end of file diff --git a/AI/Models/Dtos/XAiRequirementDto.cs b/AI/Models/Dtos/XAiRequirementDto.cs new file mode 100644 index 0000000..4b11e5e --- /dev/null +++ b/AI/Models/Dtos/XAiRequirementDto.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Microsoft.Extensions.AI; +using xAiModels.Models; +using xCommons.Extensions; +using xModels.Base; + +namespace xAiApi.AI.Models.Dtos +{ + public class XAiRequirementDto : XBaseDto + { + /// + /// Prepared Ai Project ... + /// if Exists one Using Project ID, + /// if not Created Default Project ... + /// + public XAiProjectDto AiProject { get; set; } + + /// + /// Prepared Ai Conversation ... + /// if Exists one Using Conversation ID, + /// if not Created new Conversation ... + /// + public XAiConversationDto AiConversation { get; set; } + + /// + /// Prepared Chat History for Asking ... + /// including Asked Prompt ... + /// + public IList ChatHistory { get; set; } = new List(); + + /// + /// Check User has Permission on + /// Project, Conversation and LLM Usages also ... + /// + public bool HasPermission { get; set; } = false; + + /// + /// Check Ai Project is Default Project of Specified User or not ... + /// Default Project is Main Conversations Contains ... + /// + public bool IsDefaultProject { get; set; } = false; + + /// + /// Check Ai Conversations is New Conversation or not ... + /// + public bool IsFirstConversation { get; set; } = false; + + /// + /// Check Model is Validate or not ... + /// + /// + public bool IsValid() + { + // + var result = + HasPermission && + !AiProject.IsNullOrDefault() && + !AiConversation.IsNullOrDefault(); + + // + return result; + } + } +} \ No newline at end of file diff --git a/AI/Mppings/MappingProfiles.cs b/AI/Mppings/MappingProfiles.cs new file mode 100644 index 0000000..b553908 --- /dev/null +++ b/AI/Mppings/MappingProfiles.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using xAiApi.AI.Models.Entities; +using xAiModels.Models; + +namespace xAiApi.AI.Mppings +{ + public class MappingProfiles : Profile + { + public MappingProfiles() + { + // + #region Entity to Dto ... + // + CreateMap() + .ForMember( + x => x.Owner, + opt => opt.MapFrom() + ); + + // + CreateMap() + .ForMember( + x => x.Owner, + opt => opt.MapFrom() + ); + + // + CreateMap() + .ForMember( + x => x.Owner, + opt => opt.MapFrom() + ); + #endregion + + // + #region Dto to Entity ... + CreateMap(); + CreateMap(); + CreateMap(); + #endregion + } + } +} \ No newline at end of file diff --git a/AI/Mppings/XAiConversationOwnerMappingResolver.cs b/AI/Mppings/XAiConversationOwnerMappingResolver.cs new file mode 100644 index 0000000..89a7d59 --- /dev/null +++ b/AI/Mppings/XAiConversationOwnerMappingResolver.cs @@ -0,0 +1,14 @@ +using xAiApi.AI.Models.Entities; +using xAiModels.Models; +using xIdentityService.Interfaces; + +namespace xAiApi.AI.Mppings +{ + public class XAiConversationOwnerMappingResolver : XBaseOwnerMappingResolver + { + public XAiConversationOwnerMappingResolver( + IXIdentityProvider identityProvider + ) : base(identityProvider) + { } + } +} \ No newline at end of file diff --git a/AI/Mppings/XAiMessageOwnerMappingResolver.cs b/AI/Mppings/XAiMessageOwnerMappingResolver.cs new file mode 100644 index 0000000..315949a --- /dev/null +++ b/AI/Mppings/XAiMessageOwnerMappingResolver.cs @@ -0,0 +1,14 @@ +using xAiApi.AI.Models.Entities; +using xAiModels.Models; +using xIdentityService.Interfaces; + +namespace xAiApi.AI.Mppings +{ + public class XAiMessageOwnerMappingResolver : XBaseOwnerMappingResolver + { + public XAiMessageOwnerMappingResolver( + IXIdentityProvider identityProvider + ) : base(identityProvider) + { } + } +} \ No newline at end of file diff --git a/AI/Mppings/XAiProjectOwnerMappingResolver.cs b/AI/Mppings/XAiProjectOwnerMappingResolver.cs new file mode 100644 index 0000000..d06a3fd --- /dev/null +++ b/AI/Mppings/XAiProjectOwnerMappingResolver.cs @@ -0,0 +1,14 @@ +using xAiApi.AI.Models.Entities; +using xAiModels.Models; +using xIdentityService.Interfaces; + +namespace xAiApi.AI.Mppings +{ + public class XAiProjectOwnerMappingResolver : XBaseOwnerMappingResolver + { + public XAiProjectOwnerMappingResolver( + IXIdentityProvider identityProvider + ) : base(identityProvider) + { } + } +} \ No newline at end of file diff --git a/AI/Mppings/XBaseOwnerMappingResolver.cs b/AI/Mppings/XBaseOwnerMappingResolver.cs new file mode 100644 index 0000000..2b77ff9 --- /dev/null +++ b/AI/Mppings/XBaseOwnerMappingResolver.cs @@ -0,0 +1,97 @@ +using AutoMapper; +using xModels.Dtos; +using xCommons.Extensions; +using xIdentityService.Interfaces; +using xIdentityService.Extensions; + +namespace xAiApi.AI.Mppings +{ + /// + /// an AutoMapper Value Resolver for Mapping TEntity to TDto + /// When TEntity Has OwnerId Property and Owner ... + /// + /// + /// + public abstract class XBaseOwnerMappingResolver : IValueResolver + where TDto : class + where TEntity : class + { + // + private string destPropertyName = "Owner"; + private string srcPropertyName = "OwnerId"; + private readonly IXIdentityProvider identityProvider; + + public XBaseOwnerMappingResolver( + IXIdentityProvider identityProvider + ) + { + this.identityProvider = identityProvider; + } + + public XPersonDto Resolve( + TEntity source, + TDto destination, + XPersonDto destMember, + ResolutionContext context + ) + { + // + XPersonDto result = null; + + // + // Check Source is not Null ... + if (source.IsNull()) + { + return result; + } + + // + // Check OwnerId Exists ... + var isOwnerExists = source.HasProperty(srcPropertyName); + if (!isOwnerExists) + { + return result; + } + + // + // Retrieve OwnerId from Source ... + var ownerId = source.GetProperty(srcPropertyName); + isOwnerExists = !ownerId.IsNullOrEmpty(); + if (!isOwnerExists) + { + return result; + } + + // + // Retrieve Private Trusted Device ... + var device = identityProvider.GetDevice(); + + // + // Retrieve Specified OwnerId's UserInfo ... + var userInfo = identityProvider.GetUserInfo( + device: device, + userSelectByParam: ownerId + ).RunTask(); + + // + // Check UserInfo Exists ... + if (!userInfo.IsNullOrDefault()) + { + // + // Converts UserInfo to XPersonDto ... + result = userInfo.ToXPersonDto(); + } + + // + // Setting Dest Member ... + destMember = result; + + // + // Setting Destination Property Value ... + destination.SetProperty(destPropertyName, result); + + // + return result; + } + } +} \ No newline at end of file diff --git a/AI/Services/XAiProvider.cs b/AI/Services/XAiProvider.cs index 6de8c45..d51f2cd 100644 --- a/AI/Services/XAiProvider.cs +++ b/AI/Services/XAiProvider.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using AutoMapper; using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.ChatCompletion; using OllamaSharp; using xAiApi.AI.Configuration; using xAiApi.AI.Constants; -using xAiApi.AI.Extensions; using xAiApi.AI.Interfaces; using xAiApi.AI.Interfaces.Entities; +using xAiApi.AI.Models.Dtos; using xAiApi.AI.Models.Entities; using xAiModels.Constants; using xAiModels.Extensions; @@ -30,25 +31,64 @@ namespace xAiApi.AI.Services /// public class XAiProvider : IXAiProvider { + // + #region Properties ... /// /// Configuration ... /// /// public XAiApiConfiguration Configuration { get; } - // - private readonly IChatClient chatClient; - private readonly ChatOptions chatOptions; - private readonly IXIdentityProvider identityProvider; - private readonly IXAiProjectRepository aiProjectRepository; - private readonly IXAiMessageRepository aiMessagetRepository; - private readonly IXAiConversationRepository aiConversationRepository; + /// + /// Mapper Service ... + /// + private readonly IMapper mapper; + /// + /// LLm Chat Client ... + /// used for Interacting by LLM ... + /// + private readonly IChatClient chatClient; + + /// + /// LLM Configuration Options ... + /// usually used for Enable Function Tools and etc ... + /// + private readonly ChatOptions chatOptions; + + /// + /// Identity Provider Service ... + /// use for Validate User Info ... + /// + private readonly IXIdentityProvider identityProvider; + + /// + /// Ai Project Repository Service ... + /// + /// + private readonly IXAiProjectRepository aiProjectRepository; + + /// + /// Ai Message Repository Service ... + /// + /// + private readonly IXAiMessageRepository aiMessageRepository; + + /// + /// Ai Conversation Repository Service ... + /// + /// + private readonly IXAiConversationRepository aiConversationRepository; + #endregion + + // + #region Constructor ... public XAiProvider( + IMapper mapper, XAiApiConfiguration configuration, IXIdentityProvider identityProvider, IXAiProjectRepository aiProjectRepository, - IXAiMessageRepository aiMessagetRepository, + IXAiMessageRepository aiMessageRepository, IXAiConversationRepository aiConversationRepository ) { @@ -56,9 +96,10 @@ namespace xAiApi.AI.Services Configuration = configuration; // + this.mapper = mapper; this.identityProvider = identityProvider; this.aiProjectRepository = aiProjectRepository; - this.aiMessagetRepository = aiMessagetRepository; + this.aiMessageRepository = aiMessageRepository; this.aiConversationRepository = aiConversationRepository; // @@ -96,6 +137,7 @@ namespace xAiApi.AI.Services // ], }; } + #endregion // #region Helpers ... @@ -112,28 +154,7 @@ namespace xAiApi.AI.Services ) { // - XAiProjectDto result = null; - - // - if (!entity.IsNullOrDefault()) - { - // - result = entity.ToDto(); - if (!entity.OwnerId.IsNullOrEmpty()) - { - // - result.Owner = await GetPerson( - entity.OwnerId, - cancellationToken - ); - } - - // - result.Conversations = await ToDtos( - entity.Conversations, - cancellationToken - ); - } + var result = mapper.Map(entity); // return result; @@ -152,28 +173,7 @@ namespace xAiApi.AI.Services ) { // - XAiMessageDto result = null; - - // - if (!entity.IsNullOrDefault()) - { - // - result = entity.ToDto(); - if (!entity.OwnerId.IsNullOrEmpty()) - { - // - result.Owner = await GetPerson( - entity.OwnerId, - cancellationToken - ); - } - - // - result.Conversation = await ToDto( - entity.Conversation, - cancellationToken - ); - } + var result = mapper.Map(entity); // return result; @@ -192,34 +192,7 @@ namespace xAiApi.AI.Services ) { // - XAiConversationDto result = null; - - // - if (!entity.IsNullOrDefault()) - { - // - result = entity.ToDto(); - if (!entity.OwnerId.IsNullOrEmpty()) - { - // - result.Owner = await GetPerson( - entity.OwnerId, - cancellationToken - ); - } - - // - result.Project = await ToDto( - entity.Project, - cancellationToken - ); - - // - result.Messages = await ToDtos( - entity.Messages, - cancellationToken - ); - } + var result = mapper.Map(entity); // return result; @@ -238,25 +211,10 @@ namespace xAiApi.AI.Services ) { // - IList list = new List(); + var result = mapper.Map>(entities); // - var enumerable = entities.ToAsyncEnumerable(cancellationToken); - await foreach (var entity in enumerable) - { - // - var dto = await ToDto( - entity, - cancellationToken - ); - if (!dto.IsNullOrDefault()) - { - list.Add(dto); - } - } - - // - return list.AsEnumerable(); + return result; } /// @@ -272,25 +230,10 @@ namespace xAiApi.AI.Services ) { // - IList list = new List(); + var result = mapper.Map>(entities); // - var enumerable = entities.ToAsyncEnumerable(cancellationToken); - await foreach (var entity in enumerable) - { - // - var dto = await ToDto( - entity, - cancellationToken - ); - if (!dto.IsNullOrDefault()) - { - list.Add(dto); - } - } - - // - return list.AsEnumerable(); + return result; } /// @@ -306,25 +249,10 @@ namespace xAiApi.AI.Services ) { // - IList list = new List(); + var result = mapper.Map>(entities); // - var enumerable = entities.ToAsyncEnumerable(cancellationToken); - await foreach (var entity in enumerable) - { - // - var dto = await ToDto( - entity, - cancellationToken - ); - if (!dto.IsNullOrDefault()) - { - list.Add(dto); - } - } - - // - return list.AsEnumerable(); + return result; } /// @@ -340,30 +268,7 @@ namespace xAiApi.AI.Services ) { // - var result = new XQueryResult(); - - // - if (!queryResult.IsNullOrDefault()) - { - // - result = result.UpdateData( - updateWith: queryResult, - propertyBlackList: new List - { - nameof(XQueryResult.Items) - } - ); - - // - if (queryResult.Items.HasChild()) - { - // - result.Items = await ToDtos( - queryResult.Items, - cancellationToken - ); - } - } + var result = mapper.Map>(queryResult); // return result; @@ -382,30 +287,7 @@ namespace xAiApi.AI.Services ) { // - var result = new XQueryResult(); - - // - if (!queryResult.IsNullOrDefault()) - { - // - result = result.UpdateData( - updateWith: queryResult, - propertyBlackList: new List - { - nameof(XQueryResult.Items) - } - ); - - // - if (queryResult.Items.HasChild()) - { - // - result.Items = await ToDtos( - queryResult.Items, - cancellationToken - ); - } - } + var result = mapper.Map>(queryResult); // return result; @@ -424,30 +306,7 @@ namespace xAiApi.AI.Services ) { // - var result = new XQueryResult(); - - // - if (!queryResult.IsNullOrDefault()) - { - // - result = result.UpdateData( - updateWith: queryResult, - propertyBlackList: new List - { - nameof(XQueryResult.Items) - } - ); - - // - if (queryResult.Items.HasChild()) - { - // - result.Items = await ToDtos( - queryResult.Items, - cancellationToken - ); - } - } + var result = mapper.Map>(queryResult); // return result; @@ -522,6 +381,8 @@ namespace xAiApi.AI.Services } #endregion + // + #region Actions ... /// /// Ask a Question ... /// @@ -540,124 +401,195 @@ namespace xAiApi.AI.Services ) { // - // Access Project ... - var project = await GetProject( - userInfo: userInfo, - projectId: projectId, - cancellationToken: cancellationToken - ); - var isValid = !project.IsNullOrDefault(); - if (!isValid) - { - XException.NotFound.Throw(); - } - - // - // Check Project is Default Project or not ... - var isDefaultProject = project.Title == XAiProjectEnum.Default.GetStringValue(); - - // - // Access Conversation ... - var conversation = await GetConversation( + // Get All Requirements and also + // Prepare Ai Project / Conversations and also ChatHistory + // which Including Prompt ... + var requirements = await GetRequirements( + prompt: prompt, userInfo: userInfo, projectId: projectId, conversationId: conversationId, cancellationToken: cancellationToken ); - isValid = !conversation.IsNullOrDefault(); - if (!isValid) - { - XException.NotFound.Throw(); - } - - // - // Check First Question or not ... - var isFirstConversation = !conversation.Messages.HasChild(); - - // - // Check Permissions ... - isValid = await HasPermission( - userInfo: userInfo, - projectId: projectId, - cancellationToken: cancellationToken - ); + var isValid = + !requirements.IsNullOrDefault() && + requirements.HasPermission; if (!isValid) { XException.NotAllowed.Throw(); } - // - // Preparing Chat History ... - IList chatHistory = null; - - // - // Check if First Question ... - if (isFirstConversation) - { - // - // Add Introduction Prompt ... - chatHistory.Add( - new ChatMessage( - content: Configuration.Introduction, - role: XAiChatRole.System.ToChatRole() - ) - ); - - // - // Add Project Prompt, if not Default Project ... - if (!isDefaultProject) - { - // - chatHistory.Add( - new ChatMessage( - content: project.Prompt, - role: XAiChatRole.System.ToChatRole() - ) - ); - } - } - else - { - // - // Complete Chat History ... - // TODO: Complete this ... - } - - // - // Prepare Prompt ... - prompt = await PreparePrompt( - prompt: prompt, - userInfo: userInfo, - projectId: projectId, - cancellationToken: cancellationToken - ); - if (chatHistory.IsNull()) - { - chatHistory = new List(); - } - chatHistory.Add( - new ChatMessage( - content: prompt, - role: XAiChatRole.User.ToChatRole() - ) - ); - // // Getting Response Message from LLM ... var chatResult = await chatClient .GetResponseAsync( options: chatOptions, - messages: chatHistory, + messages: requirements.ChatHistory, cancellationToken: cancellationToken ); // - // TODO: Handle Here ... + // Handle Issued Chat Response ... + isValid = + !chatResult.IsNullOrDefault() && + chatResult.FinishReason == ChatFinishReason.Stop; + if (!isValid) + { + XException.ActionFailed.Throw(); + } // - throw new NotImplementedException(); + // Prepare Response Message ... + var responseMessage = new XAiMessage + { + OwnerId = userInfo.UserId, + Content = chatResult.Text, + CreatedOn = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = requirements.AiConversation.Id + }; + responseMessage = await aiMessageRepository.AddAsync( + saveChanges: true, + item: responseMessage + ); + isValid = !responseMessage.IsNullOrDefault(); + if (!isValid) + { + XException.InvalidData.Throw(); + } + + // + // Converts Entity to Dto ... + var result = await ToDto( + entity: responseMessage, + cancellationToken: cancellationToken + ); + + // + return result; } + /// + /// Ask a Question in Streaming ... + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable AskStream( + string prompt, + string projectId = null, + string conversationId = null, + XUserClaimsInfoDto userInfo = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Get All Requirements and also + // Prepare Ai Project / Conversations and also ChatHistory + // which Including Prompt ... + var requirements = await GetRequirements( + prompt: prompt, + userInfo: userInfo, + projectId: projectId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + var isValid = + !requirements.IsNullOrDefault() && + requirements.HasPermission; + if (!isValid) + { + XException.NotAllowed.Throw(); + } + + // + // Getting Response Message from LLM ... + var chatResponseEnumerable = chatClient + .GetStreamingResponseAsync( + options: chatOptions, + messages: requirements.ChatHistory, + cancellationToken: cancellationToken + ); + + // + // Create an Empty Message for ID ... + var responseMessage = new XAiMessage + { + OwnerId = userInfo.UserId, + Role = XAiChatRole.Assistant, + CreatedOn = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + Content = "Streaming Content", + ConversationId = requirements.AiConversation.Id + }; + responseMessage = await aiMessageRepository.AddAsync( + saveChanges: true, + item: responseMessage + ); + + // + // Collecting Data through Enumeration ... + string content = string.Empty; + var sequence = DateTime.UtcNow.ToTimestamp(); + await foreach (var data in chatResponseEnumerable) + { + // + // Crucially, check the cancellation token before continuing + // Throwing OperationCanceledException is the standard way to signal cancellation. + if (cancellationToken.CanBeCanceled) + { + yield break; + } + + // + // Append recieved data to Content ... + content += data; + + // + // Crate Message Update Model ... + var item = new XAiMessageUpdateDto + { + Sequense = sequence, + Content = data.Text, + Id = responseMessage.Id, + OwnerId = userInfo.UserId, + UpdatedAt = DateTime.UtcNow, + Role = XAiChatRole.Assistant, + ConversationId = requirements.AiConversation.Id + }; + + // + sequence++; + + // + yield return item; + } + + // + // Update Response Message Entity ... + responseMessage.Content = content; + responseMessage.UpdatedAt = DateTime.UtcNow; + responseMessage = await aiMessageRepository.UpdateAsync( + saveChanges: true, + item: responseMessage, + id: responseMessage.Id + ); + isValid = !responseMessage.IsNullOrDefault(); + if (!isValid) + { + XException.ActionFailed.Throw(); + } + + // + yield break; + } + #endregion + // #region Private ... /// @@ -893,6 +825,217 @@ namespace xAiApi.AI.Services // return result; } + + /// + /// Prepare Requirements for Asking a Prompt from Model ... + /// + /// + /// + /// + /// + /// + /// + private async Task GetRequirements( + string prompt, + string projectId = null, + string conversationId = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + var result = new XAiRequirementDto(); + + // + // Access Project ... + var project = await GetProject( + userInfo: userInfo, + projectId: projectId, + cancellationToken: cancellationToken + ); + var isValid = !project.IsNullOrDefault(); + if (!isValid) + { + XException.NotFound.Throw(); + } + + // + result.AiProject = project; + + // + // Check Project is Default Project or not ... + var isDefaultProject = project.Title == XAiProjectEnum.Default.GetStringValue(); + + // + result.IsDefaultProject = isDefaultProject; + + // + // Access Conversation ... + var conversation = await GetConversation( + userInfo: userInfo, + projectId: projectId, + conversationId: conversationId, + cancellationToken: cancellationToken + ); + isValid = !conversation.IsNullOrDefault(); + if (!isValid) + { + XException.NotFound.Throw(); + } + + // + // Check First Question or not ... + var isFirstConversation = !conversation.Messages.HasChild(); + + // + result.AiConversation = conversation; + result.IsFirstConversation = isFirstConversation; + + // + // Check Permissions ... + isValid = await HasPermission( + userInfo: userInfo, + projectId: projectId, + cancellationToken: cancellationToken + ); + if (!isValid) + { + XException.NotAllowed.Throw(); + } + + // + result.HasPermission = isValid; + + // + // Check if First Question ... + if (isFirstConversation) + { + // + // Add Introduction Prompt ... + var aiMessage = new XAiMessage + { + OwnerId = userInfo.UserId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + ConversationId = conversation.Id, + Content = Configuration.Introduction, + }; + aiMessage = await aiMessageRepository.AddAsync( + item: aiMessage, + saveChanges: true + ); + isValid = !aiMessage.IsNullOrDefault(); + if (!isValid) + { + XException.ActionFailed.Throw(); + } + + // + result.ChatHistory.Add( + new ChatMessage( + content: Configuration.Introduction, + role: XAiChatRole.System.ToChatRole() + ) + ); + + // + // Add Project Prompt, if not Default Project ... + if (!isDefaultProject) + { + // + aiMessage = new XAiMessage + { + Content = project.Prompt, + OwnerId = userInfo.UserId, + Role = XAiChatRole.System, + CreatedOn = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + ConversationId = conversation.Id, + }; + aiMessage = await aiMessageRepository.AddAsync( + item: aiMessage, + saveChanges: true + ); + isValid = !aiMessage.IsNullOrDefault(); + if (!isValid) + { + XException.ActionFailed.Throw(); + } + + // + result.ChatHistory.Add( + new ChatMessage( + content: project.Prompt, + role: XAiChatRole.System.ToChatRole() + ) + ); + } + } + else + { + // + // Complete Chat History ... + foreach (var msg in conversation.Messages) + { + // + result.ChatHistory.Add( + new ChatMessage( + content: msg.Content, + role: msg.Role.ToChatRole() + ) + ); + } + } + + // + // Prepare Prompt ... + prompt = await PreparePrompt( + prompt: prompt, + userInfo: userInfo, + projectId: projectId, + cancellationToken: cancellationToken + ); + if (result.ChatHistory.IsNull()) + { + result.ChatHistory = new List(); + } + + // + // Prepare Prompt XAiMessage ... + var promptAiMessage = new XAiMessage + { + Content = prompt, + OwnerId = userInfo.UserId, + Role = XAiChatRole.User, + CreatedOn = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + ConversationId = conversation.Id, + }; + + // + // Add Prompt Message to Database ... + promptAiMessage = await aiMessageRepository.AddAsync( + saveChanges: true, + item: promptAiMessage + ); + isValid = !promptAiMessage.IsNullOrDefault(); + if (!isValid) + { + XException.InvalidData.Throw(); + } + + // + result.ChatHistory.Add( + new ChatMessage( + content: prompt, + role: XAiChatRole.User.ToChatRole() + ) + ); + + // + return result; + } #endregion } } \ No newline at end of file diff --git a/Controllers/V1/AIController.cs b/Controllers/V1/AIController.cs index 8ac330f..239c43c 100644 --- a/Controllers/V1/AIController.cs +++ b/Controllers/V1/AIController.cs @@ -6,22 +6,34 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; +using OpenAI.Realtime; using xAiApi.AI.Interfaces; using xAiApi.Base; +using xAiModels.Models; using xCommons.Configurations; using xCommons.Extensions; using xCommons.Providers; using xIdentityHelper; +using xIdentityService.Constants; using xIdentityService.Interfaces; namespace xAiApi.Controllers.V1 { + /// + /// a Controller Which Provides Actions for + /// using Ai Service ... + /// public class AIController : XIBaseV1Controller { - private readonly IXAIService aiService; + /// + /// Ai Provider ... + /// + private readonly IXAiProvider aiService; + // + #region Constructor ... public AIController( - IXAIService aiService, + IXAiProvider aiService, ILogger logger, XAppConfiguration appConfiguration, IXIdentityProvider identityProvider, @@ -35,13 +47,26 @@ namespace xAiApi.Controllers.V1 { this.aiService = aiService; } + #endregion // #region Actions ... + /// + /// As a Message from Ai ... + /// + /// + /// + /// specified prompt + /// active Project Id + /// active Conversation Id + /// + /// Ai Model's answer [HttpGet("AskAI")] [Authorize(Policy = XPolicies.EnabledUser)] - public async Task> AskAI( + public async Task> AskAI( [FromQuery] string prompt, + [FromQuery] string projectId = null, + [FromQuery] string conversationId = null, CancellationToken cancellationToken = default ) { @@ -50,43 +75,18 @@ namespace xAiApi.Controllers.V1 try { // + var userInfo = await GetUserInfo(); var result = await aiService - .GetTextResponseAsync( + .Ask( prompt: prompt, + userInfo: userInfo, + projectId: projectId, + conversationId: conversationId, cancellationToken: cancellationToken ); // - return Ok(result); - } - catch (Exception ex) - { - // - var result = GetExceptionActionResult(ex); - return result; - } - } - - [HttpGet("AskAIAsChatMessage")] - [Authorize(Policy = XPolicies.EnabledUser)] - public async Task> AskAIAsChatMessage( - [FromQuery] string prompt, - CancellationToken cancellationToken = default - ) - { - // - // Do ... - try - { - // - var result = await aiService - .GetResponseAsync( - prompt: prompt, - cancellationToken: cancellationToken - ); - - // - return Ok(result); + return Ok(result.ToDynamicObject()); } catch (Exception ex) { @@ -100,6 +100,8 @@ namespace xAiApi.Controllers.V1 [Authorize(Policy = XPolicies.EnabledUser)] public async Task AskAIStream( [FromQuery] string prompt, + [FromQuery] string projectId = null, + [FromQuery] string conversationId = null, CancellationToken cancellationToken = default ) { @@ -113,69 +115,37 @@ namespace xAiApi.Controllers.V1 .Append("Content-Type", "text/event-stream"); // - // Retrieving Stream ... + var userInfo = await GetUserInfo(); var stream = aiService - .GetTextReponseStreamAsync( + .AskStream( prompt: prompt, + userInfo: userInfo, + projectId: projectId, + conversationId: conversationId, cancellationToken: cancellationToken ); - - // await foreach (var message in stream) { // - // Do What we Have to Do with Message ... + // Converts Model to Json String ... + var json = message.ToJSON(); // - var msgBytes = message.ToBytes(); + // Generates Json byte[] ... + var bytes = json.ToBytes(); + + // + // Write Bytes to Stream ... await Response.Body.WriteAsync( - msgBytes, + bytes, 0, - msgBytes.Length + bytes.Length, + cancellationToken ); - await Response.Body.FlushAsync(); - } - // - // Closing Connection ... - await Response.Body.FlushAsync(); - } - catch (Exception) - { } - } - - [HttpGet("AskAIStreamAsChatMessage")] - [Authorize(Policy = XPolicies.EnabledUser)] - public async Task AskAIStreamAsChatMessage( - [FromQuery] string prompt, - CancellationToken cancellationToken = default - ) - { - // - // Do ... - try - { - // - // Adding Content Type ... - Response.Headers - .Append("Content-Type", "text/event-stream"); - - // - // Retrieving Stream ... - var stream = aiService - .GetReponseStreamAsync( - prompt: prompt, - cancellationToken: cancellationToken - ); - - // - await foreach (var message in stream) - { // - await Response.Body.WriteModelToStream( - model: message, - cancellationToken: cancellationToken - ); + // Flushing Stream ... + await Response.Body.FlushAsync(cancellationToken); } // diff --git a/Controllers/V1/Entities/AiConversationsController.cs b/Controllers/V1/Entities/AiConversationsController.cs new file mode 100644 index 0000000..1501a6a --- /dev/null +++ b/Controllers/V1/Entities/AiConversationsController.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using xAiApi.AI.Interfaces.Entities; +using xAiApi.AI.Models.Entities; +using xAiApi.Base; +using xCommons.Configurations; +using xCommons.Providers; +using xIdentityService.Interfaces; +using xPushService.Base; + +namespace xAiApi.Controllers.V1.Entities +{ + public class AiConversationsController : XIBaseV1EntityHubController + { + public AiConversationsController( + ILogger logger, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider, + IXAiConversationRepository repository, + IHubContext> hub = null + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider, + repository, + hub + ) + { } + } +} \ No newline at end of file diff --git a/Controllers/V1/Entities/AiMessagesController.cs b/Controllers/V1/Entities/AiMessagesController.cs new file mode 100644 index 0000000..1dd5f2e --- /dev/null +++ b/Controllers/V1/Entities/AiMessagesController.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using xAiApi.AI.Interfaces.Entities; +using xAiApi.AI.Models.Entities; +using xAiApi.Base; +using xCommons.Configurations; +using xCommons.Providers; +using xIdentityService.Interfaces; +using xPushService.Base; + +namespace xAiApi.Controllers.V1.Entities +{ + public class AiMessagesController : XIBaseV1EntityHubController + { + public AiMessagesController( + ILogger logger, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider, + IXAiMessageRepository repository, + IHubContext> hub = null + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider, + repository, + hub + ) + { } + } +} \ No newline at end of file diff --git a/Controllers/V1/Entities/AiProjectsController.cs b/Controllers/V1/Entities/AiProjectsController.cs new file mode 100644 index 0000000..4f22e30 --- /dev/null +++ b/Controllers/V1/Entities/AiProjectsController.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using xAiApi.AI.Interfaces.Entities; +using xAiApi.AI.Models.Entities; +using xAiApi.Base; +using xCommons.Configurations; +using xCommons.Providers; +using xIdentityService.Interfaces; +using xPushService.Base; + +namespace xAiApi.Controllers.V1.Entities +{ + public class AiProjectsController : XIBaseV1EntityHubController + { + public AiProjectsController( + ILogger logger, + XAppConfiguration appConfiguration, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider, + IXAiProjectRepository repository, + IHubContext> hub = null + ) : base( + logger, + appConfiguration, + identityProvider, + validationProvider, + repository, + hub + ) + { } + } +} \ No newline at end of file diff --git a/Data/Repositories/Ef/XAiProjectEfRepository.cs b/Data/Repositories/Ef/XAiProjectEfRepository.cs index c61a87b..5ce499b 100644 --- a/Data/Repositories/Ef/XAiProjectEfRepository.cs +++ b/Data/Repositories/Ef/XAiProjectEfRepository.cs @@ -36,7 +36,8 @@ namespace xAiApi.Data.Repositories.Ef { // return dbSet - .Include(x => x.Conversations); + .Include(x => x.Conversations) + .ThenInclude(x => x.Messages); } //