using System; using OpenAI; using OllamaSharp; using System.Linq; using xModels.Dtos; using System.Net.Http; using System.Threading; using xAiApi.Constants; using xAiModels.Models; using xAiApi.Interfaces; using xAiApi.Extensions; using System.ClientModel; using xCommons.Providers; using xCommons.Extensions; using xAiModels.Constants; using xAiModels.Interfaces; using xAiModels.Extensions; using xAiService.Constants; using xAiApi.Configurations; using xExceptions.Constants; using xAiModels.Models.Dtos; using xAiService.Extensions; using System.Threading.Tasks; using xIdentityModels.Models; using xFileService.Interfaces; using Microsoft.Extensions.AI; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.Extensions.Logging; using xAiApi.Interfaces.Extractors; using System.ClientModel.Primitives; using System.Runtime.CompilerServices; namespace xAiApi.Providers { /// /// Base Ai Service ... /// public abstract class XAIServiceBase : IXAiServiceBase { /// /// Chat Options ... /// public ChatOptions Options { get; } /// /// Descriptor of Models which used in Service ... /// public XAiModelDescriptor Descriptor { get; } private readonly IXFileProvider fileProvider; private readonly IXAiDataProvider dataProvider; private readonly ILogger logger; private readonly XAiApiConfiguration configuration; private readonly XValidationProvider validationProvider; private readonly IXFileContentExtractor fileContentExtractor; protected XAIServiceBase( IXFileProvider fileProvider, IXAiDataProvider dataProvider, ILogger logger, XAiApiConfiguration configuration, XValidationProvider validationProvider, IXFileContentExtractor fileContentExtractor, string model = null ) { // this.logger = logger; this.fileProvider = fileProvider; this.dataProvider = dataProvider; this.configuration = configuration; this.validationProvider = validationProvider; this.fileContentExtractor = fileContentExtractor; // // Prepare Embedding Model Descriptor ... Descriptor = configuration.GetModel(model); if (!Descriptor.IsValid()) { XException.InvalidConfiguration.Throw(); } // // Prepare Chat Options based On Tools ... Options = new ChatOptions(); } // #region Actions ... /// /// Base Method for Communicate with LLM ... /// /// /// /// public virtual async Task AskTextAsync( string prompt, CancellationToken cancellationToken = default ) { // // Check Prompt Exists ... if (prompt.IsNullOrEmpty()) { XException.InvalidArgs.Throw(); } // using var client = Descriptor.GetClient(); var history = GetHistory(prompt); var response = await client.GetResponseAsync( messages: history, cancellationToken: cancellationToken ); // // Validate Response ... if (!response.IsValid()) { // // Dispose Client ... client.Dispose(); XException.ActionFailed.Throw(); } // // Retrieve Response Text ... var result = response.Text; // return result; } /// /// Base Method for Communicate with LLM ... /// /// /// /// public virtual async IAsyncEnumerable AskTextAsEnumerable( string prompt, [EnumeratorCancellation] CancellationToken cancellationToken = default ) { // // Check Prompt Exists ... if (prompt.IsNullOrEmpty()) { XException.InvalidArgs.Throw(); } // using var client = Descriptor.GetClient(); var history = GetHistory(prompt); var enumerable = client.GetStreamingResponseAsync( options: null, messages: history ); // await foreach (var res in enumerable) { // // Cancellation Token ... if (cancellationToken.IsCancellationRequested) { yield break; } // yield return res.Text; } } /// /// Prepare a Message History List for LLM Communication based on Prompt ... /// /// /// /// public virtual IList GetHistory( string prompt = null, bool forcePrompt = true ) { // // Validate Args ... if (prompt.IsNullOrEmpty()) { XException.InvalidArgs.Throw(); } // // User Prompt ... var promptMessage = new ChatMessage( ChatRole.User, prompt ); // // Preparing Introduction Prompt ... var introductionPrompt = new ChatMessage( ChatRole.System, configuration.GetPrompt() ); // List result = [ introductionPrompt, ]; if (forcePrompt && prompt is not null && !string.IsNullOrWhiteSpace(prompt)) { result.Add(promptMessage); } // return result; } /// /// Prepare a Message History List for LLM Communication based on Prompt ... /// /// /// /// public virtual IList GetHistory( ChatMessage message = null, bool forceMessage = true ) { // // Preparing Introduction Prompt ... var introductionPrompt = new ChatMessage( ChatRole.System, configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName) ); // List result = [ introductionPrompt, ]; if (forceMessage && !message.IsNullOrDefault()) { result.Add(message); } // return result; } #endregion // #region Loaders ... /// /// Load AI Projects ... /// /// /// /// /// /// public virtual async Task> LoadProjects( string ownerId, string connectionId = null, bool includeDefaults = false, CancellationToken cancellationToken = default ) { // // Validate Args ... if (ownerId.IsNullOrEmpty()) { XException.InvalidArgs.Throw(); } // var result = await dataProvider.LoadProjects( ownerId: ownerId, fillConversations: false, connectionId: connectionId, fillConversationMessages: false, cancellationToken: cancellationToken ); // // Handle Defaults Including ... if (includeDefaults) { // // Here we have to fill Conversations of Default Project ... var defaultProjectId = result.GetDafualtProject(ownerId); var defaultProject = result.FirstOrDefault(x => x.Id == defaultProjectId); result = result.Where(x => x.Id != defaultProjectId); // // Reading Conversations of Specified Default Project ... var defaultProjectConversations = await dataProvider.LoadConversations( fillMessages: false, connectionId: connectionId, projectId: defaultProjectId, cancellationToken: cancellationToken ); var defaultConversationId = defaultProjectConversations.GetDefaultConversation( ownerId: ownerId, projectId: defaultProjectId ); var defaultConversation = defaultProjectConversations.First(x => x.Id == defaultConversationId); defaultProjectConversations = defaultProjectConversations.Where(x => x.Id != defaultConversationId); // // Reading Default Conversations Messages ... var defaultConversationMessages = await dataProvider.LoadMessages( cancellationToken: cancellationToken, conversationId: defaultConversationId ); // defaultConversation.Messages = [.. defaultConversationMessages.OrderByDescending(m => m.CreatedOn)]; defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; defaultProject.Conversations = [.. defaultProjectConversations]; result = [defaultProject, .. result]; } // return result; } /// /// Load Conversations of Specified AI Project ... /// /// /// /// /// public virtual async Task> LoadConversations( Guid projectId, string connectionId = null, CancellationToken cancellationToken = default ) { // // Validate Args ... if (projectId.IsDefaultGuid()) { XException.InvalidArgs.Throw(); } // var result = await dataProvider.LoadConversations( fillMessages: false, projectId: projectId, connectionId: connectionId, cancellationToken: cancellationToken ); // return result; } /// /// Load Messages of Specified Conversation ... /// /// /// /// public virtual async Task> LoadMessages( Guid conversationId, CancellationToken cancellationToken = default ) { // // Validate Args ... if (conversationId.IsDefaultGuid()) { XException.InvalidArgs.Throw(); } // var result = await dataProvider.LoadMessages( conversationId: conversationId, cancellationToken: cancellationToken ); // return result; } #endregion // #region Queries ... /// /// Query Projects ... /// /// /// /// /// public virtual async Task> QueryProjects( string ownerId, XQuery query, CancellationToken cancellationToken = default ) { // // Validate ... if (ownerId.IsNullOrEmpty()) { XException.InvalidArgs.Throw(); } // var result = await dataProvider.QueryProjects( query: query, ownerId: ownerId, cancellationToken: cancellationToken ); // return result; } /// /// Query Conversations ... /// /// /// /// /// /// public virtual async Task> QueryConversations( string ownerId, Guid projectId, XQuery query, CancellationToken cancellationToken = default ) { // // Validate ... if (ownerId.IsNullOrEmpty() || projectId.IsDefaultGuid()) { XException.InvalidArgs.Throw(); } // // Retrieve Project ... var project = await dataProvider.GetProject( id: projectId, fillConversations: false, fillConversationMessages: false, cancellationToken: cancellationToken ); if (project.IsNullOrDefault()) { XException.NotFound.Throw(); } // // Validate Owner ... if (ownerId != project.OwnerId) { XException.NotAllowed.Throw(); } // var result = await dataProvider.QueryConversations( query: query, ownerId: ownerId, projectId: projectId, cancellationToken: cancellationToken ); // return result; } /// /// Query Messages ... /// /// /// /// /// /// public virtual async Task> QueryMessages( string ownerId, Guid conversationId, XQuery query, CancellationToken cancellationToken = default ) { // // Validate ... if (ownerId.IsNullOrEmpty() || conversationId.IsDefaultGuid()) { XException.InvalidArgs.Throw(); } // // Retrieve Conversation ... var conversation = await dataProvider.GetConversation( id: conversationId, fillMessages: false, cancellationToken: cancellationToken ); if (conversation.IsNullOrDefault()) { XException.NotFound.Throw(); } // // Validate Owner ... if (ownerId != conversation.OwnerId) { XException.NotAllowed.Throw(); } // var result = await dataProvider.QueryMessages( query: query, ownerId: ownerId, conversationId: conversationId, cancellationToken: cancellationToken ); // return result; } #endregion // #region Creators ... /// /// Create an AI Project ... /// /// /// /// /// /// public virtual async Task CreateProject( string ownerId, XAiProjectDto item, string connectionId = null, CancellationToken cancellationToken = default ) { // var result = await dataProvider.CreateProject( item: item, ownerId: ownerId, connectionId: connectionId, cancellationToken: cancellationToken ); // return result; } /// /// Create an AI Conversation ... /// /// /// /// /// /// /// public virtual async Task CreateConversation( string ownerId, Guid projectId, XAiConversationDto item, string connectionId = null, CancellationToken cancellationToken = default ) { // var result = await dataProvider.CreateConversation( item: item, ownerId: ownerId, projectId: projectId, connectionId: connectionId, cancellationToken: cancellationToken ); // return result; } #endregion // #region Content Extraction ... /// /// Try to Extract Files Content ... /// /// /// /// /// public async Task ExtractContent( IFormFileCollection files, string[] extractors = null, CancellationToken cancellationToken = default ) { // if (!files.HasChild()) { XException.InvalidArgs.Throw(); } // var extractionResults = new List(); foreach (var file in files) { // if (file.IsNull() || file.Length == 0) { continue; } // var mimeType = file.ContentType?.ToLowerInvariant() ?? string.Empty; if (!fileContentExtractor.CanExtract(mimeType, extractors)) { continue; } // try { // using var stream = file.OpenReadStream(); // var fr = await fileContentExtractor.ExtractRichAsync( fileStream: stream, mimeType: mimeType, extractors: extractors, fileName: file.FileName, cancellationToken: cancellationToken ); // if (fr.IsValid()) { extractionResults.Add(fr); } } catch (OperationCanceledException) { throw; } catch { continue; } } // var result = string.Empty; if (extractionResults.HasChild()) { // extractionResults.ForEach(er => { result += $"File {er.FileName} Content:{Environment.NewLine}{er.Text ?? er.AudioTranscript}{Environment.NewLine}----"; }); } // return result; } #endregion // #region Data Actions ... /// /// Ask a Question as Prompt and Retrieve Response ... /// /// /// /// /// /// /// /// /// /// public virtual async Task AskAsync( string prompt, string ownerId, Guid projectId, Guid conversationId, string connectionId = null, IFormFileCollection files = null, XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ) { // // Validate Args ... validationProvider .GroupValidationBuilder() .AddNotEmpty(prompt) .AddNotEmpty(ownerId) .ValidateGroup(); // // Check Project ... XAiProjectDto project = null; if (projectId.IsDefaultGuid()) { // project = await dataProvider.GetDefaultProject( ownerId: ownerId, connectionId: connectionId, cancellationToken: cancellationToken ); // projectId = project.Id; } else { // project = await dataProvider.GetProject( id: projectId, fillConversations: false, fillConversationMessages: false, cancellationToken: cancellationToken ); } if (project.IsNullOrDefault()) { XException.NotFound.Throw(); } // // Check Conversation ... XAiConversationDto conversation = null; if (conversationId.IsDefaultGuid()) { // conversation = await dataProvider.GetDefaultConversation( ownerId: ownerId, projectId: projectId, connectionId: connectionId, cancellationToken: cancellationToken ); // conversationId = conversation.Id; } else { // conversation = await dataProvider.GetConversation( id: conversationId, fillMessages: false, cancellationToken: cancellationToken ); } if (conversation.IsNullOrDefault()) { XException.NotFound.Throw(); } // // Reading Messages ... var messages = await dataProvider.LoadMessages( conversationId: conversationId, cancellationToken: cancellationToken ); // // Upload Files ... IEnumerable uploadedFiles = null; if (files.HasChild()) { // // Validate User Info ... if (userInfo.IsNullOrDefault()) { XException.InvalidArgs.Throw(); } // // Upload Files ... uploadedFiles = await fileProvider.Upload( files: files, userInfo: userInfo, connectionId: connectionId, cancellationToken: cancellationToken ); } // // Handle Memory ... IList history = await PrepareMemory( project: project, messages: messages, cancellationToken: cancellationToken ); // // Prepare and Add Prompt Message ... var promptMessage = new XAiMessageDto { Content = prompt, OwnerId = ownerId, Role = XAiChatRole.User, CreatedOn = DateTime.UtcNow, ConversationId = conversationId, ConversationTitle = conversation.Title }; // // Attached Uploaded File Id's as Meta Data ... if (uploadedFiles.HasChild()) { // var fileIds = uploadedFiles.Select(f => f.Id).ToList(); promptMessage.MetaDatas = new Dictionary { { XAiServiceConstants.XMetaDataFileIdsIdentifier, fileIds } }.ToJSON(); } // // Save Prompt Message ... promptMessage = await dataProvider.AddMessage( item: promptMessage, connectionId: connectionId, conversationId: conversationId, cancellationToken: cancellationToken ); // // Since Prompt Message Has Id now, we can Attach Uploaded Files to it ... IList fileContents = []; if (uploadedFiles.HasChild()) { // foreach (var uf in uploadedFiles) { // try { // // Add Reference File to Prompt Message ... await fileProvider.AddReference( id: uf.Id, userInfo: userInfo, connectionId: connectionId, forProvidedId: promptMessage.Id, cancellationToken: cancellationToken, providedFor: XAiServiceConstants.XAiMessageFilesIdentifier ); // // Handle File Content Extraction and Attach to Prompt Message ... var fileDescriptor = await fileProvider.GetFileDescriptor( id: uf.Id, cancellationToken: cancellationToken ); if (fileDescriptor == null || fileDescriptor.Stream == null) { continue; } // var extractionResult = await fileContentExtractor.ExtractRichAsync( fileStream: fileDescriptor.Stream, fileName: uf.FileName ?? uf.Name, mimeType: fileDescriptor.MIMEType, cancellationToken: cancellationToken ); if (extractionResult.IsValid()) { // var contents = extractionResult.ToAIContents(); fileContents = [.. fileContents, .. contents]; // // ذخیره Metadata برای ردیابی logger.LogInformation( "File {FileName} extracted successfully. Text: {HasText}, Images: {ImageCount}, Fallback: {UsedFallback}", uf.FileName, !string.IsNullOrEmpty(extractionResult.Text), extractionResult.Images.Count, extractionResult.UsedImageFallback ); } // fileDescriptor.Stream.Dispose(); } catch (Exception ex) { logger.LogError(ex, "Failed to extract content from file: {FileName}", uf.FileName); } } } // // Converts to ChatMessage ... var promptChatMessage = promptMessage .ToChatMessages(fileContents); // // Ask Questions From LLM ... var answer = await AskLLMAsync( history: history, prompt: promptChatMessage, cancellationToken: cancellationToken ); // // Prepare Answer AI Message ... var result = new XAiMessageDto { Content = answer, OwnerId = ownerId, CreatedOn = DateTime.UtcNow, Role = XAiChatRole.Assistant, ConversationId = conversationId, ConversationTitle = conversation.Title }; result = await dataProvider.AddMessage( item: result, connectionId: connectionId, conversationId: conversationId, cancellationToken: cancellationToken ); // return result; } /// /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... /// /// /// /// /// /// /// /// /// public virtual async IAsyncEnumerable AskAsEnumerable( string prompt, string ownerId, Guid projectId, Guid conversationId, string connectionId = null, IFormFileCollection files = null, [EnumeratorCancellation] CancellationToken cancellationToken = default ) { // // Validate Args ... validationProvider .GroupValidationBuilder() .AddNotEmpty(prompt) .AddNotEmpty(ownerId) .ValidateGroup(); // // Check Project ... XAiProjectDto project = null; if (projectId.IsDefaultGuid()) { // project = await dataProvider.GetDefaultProject( ownerId: ownerId, connectionId: connectionId, cancellationToken: cancellationToken ); // projectId = project.Id; } else { // project = await dataProvider.GetProject( id: projectId, fillConversations: false, fillConversationMessages: false, cancellationToken: cancellationToken ); } if (project.IsNullOrDefault()) { XException.NotFound.Throw(); } // // Check Conversation ... XAiConversationDto conversation = null; if (conversationId.IsDefaultGuid()) { // conversation = await dataProvider.GetDefaultConversation( ownerId: ownerId, projectId: projectId, connectionId: connectionId, cancellationToken: cancellationToken ); // conversationId = conversation.Id; } else { // conversation = await dataProvider.GetConversation( id: conversationId, fillMessages: false, cancellationToken: cancellationToken ); } if (conversation.IsNullOrDefault()) { XException.NotFound.Throw(); } // // Reading Messages ... var messages = await dataProvider.LoadMessages( conversationId: conversationId, cancellationToken: cancellationToken ); // // TODO: Parse Files ... // for Attached them into Prompt Message ... // // Handle Memory ... IList history = await PrepareMemory( project: project, messages: messages, cancellationToken: cancellationToken ); // // Prepare and Add Prompt Message ... var promptMessage = new XAiMessageDto { Content = prompt, OwnerId = ownerId, Role = XAiChatRole.User, CreatedOn = DateTime.UtcNow, ConversationId = conversationId, }; promptMessage = await dataProvider.AddMessage( item: promptMessage, connectionId: connectionId, conversationId: conversationId, cancellationToken: cancellationToken ); promptMessage = await dataProvider.Enrich( source: promptMessage, cancellationToken: cancellationToken ); // // Attach Uploaded Files ... var promptChatMessage = promptMessage .ToChatMessages(); history.Add(promptChatMessage); // // Ask Questions From LLM ... using var client = Descriptor.GetClient(); var enumerable = client.GetStreamingResponseAsync( options: null, messages: history ); // long sequence = 1; var answer = string.Empty; await foreach (var res in enumerable) { // // Cancellation Token ... if (cancellationToken.IsCancellationRequested) { yield break; } // var resp = new XAiResonseMessage { OwnerId = ownerId, Content = res.Text, Sequence = sequence, Role = XAiChatRole.System, RespondTo = promptMessage.Id, RespondedOn = DateTime.UtcNow, }; // sequence++; answer += res.Text; // yield return resp; } // // Prepare Answer AI Message ... var result = new XAiMessageDto { Content = answer, OwnerId = ownerId, Role = XAiChatRole.System, CreatedOn = DateTime.UtcNow, ConversationId = conversationId }; result = await dataProvider.AddMessage( item: result, connectionId: connectionId, conversationId: conversationId, cancellationToken: cancellationToken ); } #endregion /// /// Base Dispose Action ... /// /// public virtual void Dispose() { } // #region Private ... /// /// Implementing Memory Strategy here ... /// /// /// /// /// private async Task> PrepareMemory( XAiProjectDto project, IEnumerable messages, CancellationToken cancellationToken = default ) { // var result = new List(); // // Retrieve Introduction Prompt ... var introductionPrompt = configuration.GetPrompt(XAiApiConstants.XAiApiIntroductionPromptName); if (introductionPrompt.IsNullOrEmpty()) { XException.InvalidData.Throw(); } // var introductionMessage = new ChatMessage( role: ChatRole.System, content: introductionPrompt ); // result.Add(introductionMessage); // if (!project.IsNullOrDefault() && !project.Prompt.IsNullOrEmpty()) { // // TODO: Handle Project Files Here ... var projectMessage = new ChatMessage( role: ChatRole.System, contents: [new TextContent(project.Prompt)] ); result.Add(projectMessage); } // if (messages.HasChild()) { // // TODO: Here we can Implement Memory Management based on // Last nth of Messages, or Summarization of Messages, etc ... result = [..result, ..messages .OrderBy(m => m.CreatedOn) .Select(m => m.ToChatMessages())]; } // return result; } /// /// Ask Quetion from LLM ... /// /// /// /// /// private async Task AskLLMAsync( ChatMessage prompt, IList history, CancellationToken cancellationToken = default ) { // if (prompt.IsNullOrDefault()) { XException.InvalidArgs.Throw(); } // history.Add(prompt); using var client = Descriptor.GetClient(); var response = await client.GetResponseAsync( messages: history, cancellationToken: cancellationToken ); // // Validate Response ... if (!response.IsValid()) { // // Dispose Client ... client.Dispose(); XException.ActionFailed.Throw(); } // // Retrieve Response Text ... var result = response.Text; // return result; } #endregion } }