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 OllamaSharp; using xAiApi.AI.Configuration; using xAiApi.AI.Constants; 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; using xAiModels.Models; using xCommons.Extensions; using xExceptions.Constants; using xIdentityModels.Models; using xIdentityService.Extensions; using xIdentityService.Interfaces; using xModels.Dtos; namespace xAiApi.AI.Services { /// /// Provide Ai Capabilities Using AI Service ... /// using User Data Info ... /// public class XAiProvider : IXAiProvider { // #region Properties ... /// /// Configuration ... /// /// public XAiApiConfiguration Configuration { get; } /// /// 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 aiMessageRepository, IXAiConversationRepository aiConversationRepository ) { // Configuration = configuration; // this.mapper = mapper; this.identityProvider = identityProvider; this.aiProjectRepository = aiProjectRepository; this.aiMessageRepository = aiMessageRepository; this.aiConversationRepository = aiConversationRepository; // // Prepare Chat Model ... chatClient = new ChatClientBuilder(new OllamaApiClient( new Uri(configuration.Url), configuration.Model )) // .UseFunctionInvocation() .Build(); // // Configuring Chant Options ... chatOptions = new ChatOptions() { // // Preparing Tools ... // Tools = [ // // // // Tempreature Tool ... // AIFunctionFactory.Create((string location, string unit) => { // // // var temp = Random.Shared.Next(5, 20); // var cond = Random.Shared.Next(0, 1) == 0 ? "sunny" : "rainy"; // // // var result = $"The weather is {temp} degrees C and {cond}."; // // // return result; // }, // "get_current_weather", // "Get the current weather in given location" // ) // ], }; } #endregion // #region Helpers ... /// /// Converts Entity to Dto ... /// /// /// /// /// public async Task ToDto( XAiProject entity, CancellationToken cancellationToken = default ) { // var result = mapper.Map(entity); // return result; } /// /// Converts Entity to Dto ... /// /// /// /// /// public async Task ToDto( XAiMessage entity, CancellationToken cancellationToken = default ) { // var result = mapper.Map(entity); // return result; } /// /// Converts Entity to Dto ... /// /// /// /// /// public async Task ToDto( XAiConversation entity, CancellationToken cancellationToken = default ) { // var result = mapper.Map(entity); // return result; } /// /// Converts a List of Entities to Dtos ... /// /// /// /// /// public async Task> ToDtos( IEnumerable entities, CancellationToken cancellationToken = default ) { // var result = mapper.Map>(entities); // return result; } /// /// Converts a List of Entities to Dtos ... /// /// /// /// /// public async Task> ToDtos( IEnumerable entities, CancellationToken cancellationToken = default ) { // var result = mapper.Map>(entities); // return result; } /// /// Converts a List of Entities to Dtos ... /// /// /// /// /// public async Task> ToDtos( IEnumerable entities, CancellationToken cancellationToken = default ) { // var result = mapper.Map>(entities); // return result; } /// /// Converts a QueryResult of Entities to Dtos ... /// /// /// /// /// public async Task> ToDtoQueryResult( XQueryResult queryResult, CancellationToken cancellationToken = default ) { // var result = mapper.Map>(queryResult); // return result; } /// /// Converts a QueryResult of Entities to Dtos ... /// /// /// /// /// public async Task> ToDtoQueryResult( XQueryResult queryResult, CancellationToken cancellationToken = default ) { // var result = mapper.Map>(queryResult); // return result; } /// /// Converts a QueryResult of Entities to Dtos ... /// /// /// /// /// public async Task> ToDtoQueryResult( XQueryResult queryResult, CancellationToken cancellationToken = default ) { // var result = mapper.Map>(queryResult); // return result; } /// /// Check Permission for Ai Actions ... /// /// /// /// /// public async Task HasPermission( string projectId = null, XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ) { // var result = !userInfo.IsNullOrDefault() && !userInfo.UserId.IsNullOrEmpty(); if (!result) { return result; } // try { // var person = await GetPerson( ownerId: userInfo.UserId, cancellationToken: cancellationToken ); // result = !person.IsNullOrDefault(); } catch { result = false; } // return result; } /// /// Prepare Prompt ... /// /// /// /// /// /// public async Task PreparePrompt( string prompt, string projectId = null, XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ) { // var result = prompt; // // TODO: Complete this ... // return await Task.FromResult(result); } #endregion // #region Actions ... /// /// Ask a Question ... /// /// /// /// /// /// /// public async Task Ask( string prompt, string projectId = null, string conversationId = null, XUserClaimsInfoDto userInfo = null, 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 chatResult = await chatClient .GetResponseAsync( options: chatOptions, messages: requirements.ChatHistory, cancellationToken: cancellationToken ); // // Handle Issued Chat Response ... isValid = !chatResult.IsNullOrDefault() && chatResult.FinishReason == ChatFinishReason.Stop; if (!isValid) { XException.ActionFailed.Throw(); } // // 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 chatStream = chatClient .GetStreamingResponseAsync( options: chatOptions, messages: requirements.ChatHistory, cancellationToken: cancellationToken ); isValid = !chatStream.IsNull(); if (!isValid) { yield break; } // // 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 ); // // Loop through Async Enumerable ... string content = string.Empty; var sequence = DateTime.UtcNow.ToTimestamp(); await foreach(var msg in chatStream) { // // Crucially, check the cancellation token before continuing // Throwing OperationCanceledException is the standard way to signal cancellation. if (cancellationToken.IsCancellationRequested) { yield break; } // // Append recieved data to Content ... content += msg.Text; // // Crate Message Update Model ... var item = new XAiMessageUpdateDto { Sequense = sequence, Content = msg.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(); } // // End Async Enumerable ... yield break; } #endregion // #region Private ... /// /// Access Person class based on OwnerId ... /// /// /// /// /// private async Task GetPerson( string ownerId, CancellationToken cancellationToken = default ) { // XPersonDto result = null; // if (!ownerId.IsNullOrEmpty()) { // var device = identityProvider.GetDevice(); var userInfo = await identityProvider.GetUserInfo( device: device, userSelectByParam: ownerId ); if (userInfo.IsNullOrDefault()) { XException.NotFound.Throw(); } // result = userInfo.ToXPersonDto(); } // return result; } /// /// Retrieve Specified Project or Default ... /// /// /// /// /// private async Task GetProject( string projectId = null, XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ) { // // Validate UserInfo ... var isValid = !userInfo.IsNullOrDefault() && !userInfo.UserId.IsNullOrEmpty(); if (!isValid) { XException.NotAllowed.Throw(); } // // Check UserInfo ... var personDto = await GetPerson( ownerId: userInfo.UserId, cancellationToken: cancellationToken ); isValid = !personDto.IsNullOrDefault(); if (!isValid) { XException.NotFound.Throw(); } // XAiProject project = null; // // Check Default Project is Exists or not ... var defaultProject = await aiProjectRepository.FindOneAsync( containsDetail: true, ignoreSoftDeleteds: true, whereClause: x => x.OwnerId == userInfo.UserId && x.Title == XAiProjectEnum.Default.GetStringValue() ); var isExists = !defaultProject.IsNullOrDefault(); if (!isExists) { // // Create Default Project ... defaultProject = new XAiProject { Prompt = string.Empty, OwnerId = userInfo.UserId, Description = string.Empty, CreatedOn = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, Title = XAiProjectEnum.Default.GetStringValue(), }; // defaultProject = await aiProjectRepository.AddAsync( saveChanges: true, item: defaultProject ); // isExists = !defaultProject.IsNullOrDefault(); } // // Validate Project ID ... if (projectId.IsNullOrEmpty()) { // // Default Project ... project = defaultProject; projectId = defaultProject.Id.ToString(); } else { // // Special Project ... project = await aiProjectRepository.FindOneAsync( containsDetail: false, ignoreSoftDeleteds: true, whereClause: x => x.Id.ToString() == projectId && x.OwnerId == userInfo.UserId ); isExists = !project.IsNullOrDefault(); if (!isExists) { // // If Not Exists Use Default ... project = defaultProject; } } // // Converts to Dto ... var result = await ToDto( entity: project, cancellationToken: cancellationToken ); // return result; } /// /// Retrieve Conversation for Asking ... /// /// /// /// /// /// private async Task GetConversation( string projectId = null, string conversationId = null, XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ) { // // Validate User Info ... var isValid = await HasPermission( userInfo: userInfo, projectId: projectId, cancellationToken: cancellationToken ); if (!isValid) { XException.NotAllowed.Throw(); } // var project = await GetProject( userInfo: userInfo, projectId: projectId, cancellationToken: cancellationToken ); isValid = !project.IsNullOrDefault(); if (!isValid) { XException.NotFound.Throw(); } // // Check Conversation Exists ... var result = project.Conversations .FirstOrDefault(x => x.OwnerId == userInfo.UserId && x.Id.ToString() == conversationId ); var isExists = !result.IsNullOrDefault() && !conversationId.IsNullOrEmpty(); if (!isExists) { // var title = $"Conversation {Guid.NewGuid()}"; var conversationEntity = new XAiConversation { Title = title, ProjectId = project.Id, OwnerId = userInfo.UserId, CreatedOn = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }; // conversationEntity = await aiConversationRepository.AddAsync( saveChanges: true, item: conversationEntity ); isExists = !conversationEntity.IsNullOrDefault(); if (!isExists) { XException.NotFound.Throw(); } // result = await ToDto( entity: conversationEntity, cancellationToken: cancellationToken ); } // 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 ... var orderedByTimeMessage = conversation.Messages .OrderBy(x => x.CreatedOn) .AsEnumerable(); foreach (var msg in orderedByTimeMessage) { // 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(); } // // Update Conversation ... var conversationEntity = await aiConversationRepository .GetAsync(id: result.AiConversation.Id); conversation = mapper.Map(conversationEntity); result.AiConversation = conversation; // result.ChatHistory.Add( new ChatMessage( content: promptAiMessage.Content, role: promptAiMessage.Role.ToChatRole() ) ); // return result; } #endregion } }