using System; using OpenAI; using OllamaSharp; using System.Linq; 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 xAiApi.Configurations; using xExceptions.Constants; using xAiModels.Models.Dtos; using xAiService.Extensions; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.Extensions.Logging; 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 IXAiDataProvider dataProvider; private readonly ILogger logger; private readonly XAiApiConfiguration configuration; private readonly XValidationProvider validationProvider; protected XAIServiceBase( IXAiDataProvider dataProvider, ILogger logger, XAiApiConfiguration configuration, XValidationProvider validationProvider, string model = null ) { // this.logger = logger; this.dataProvider = dataProvider; this.configuration = configuration; this.validationProvider = validationProvider; // // 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 = 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 = 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; } } /// /// Get LLM Client instance for Communicating with LLM ... /// /// public virtual IChatClient GetClient() { // // Try to Initialize LLm ... var model = Descriptor.LLM; var apiKey = Descriptor.ApiKey; var url = new Uri(Descriptor.Url); var httpClient = GetHttpClient(Descriptor.Url); // IChatClient result = null; switch (Descriptor.Provider) { // case XAiModelProviderType.Ollama: // // Instance Ollama Client using Custom HttpCLient ... var ollamaClient = new OllamaApiClient(httpClient, model); result = new ChatClientBuilder(ollamaClient) .UseFunctionInvocation() .Build(); break; // case XAiModelProviderType.OpenAI: // // Instance Ollama Client using Custom HttpCLient ... var openAiClient = new OpenAIClient( new ApiKeyCredential(apiKey.IsNullOrEmpty() ? XAiApiConstants.XOpenAINoKey : apiKey), new OpenAIClientOptions { Endpoint = url, Transport = new HttpClientPipelineTransport(httpClient) } ); result = new ChatClientBuilder( openAiClient .GetChatClient(model) .AsIChatClient()) .UseFunctionInvocation() .Build(); break; // case XAiModelProviderType.DeepSeek: break; // case XAiModelProviderType.HuggingFace: break; // default: break; } // if (result.IsNullOrDefault()) { XException.InvalidData.Throw(); } // return result; } /// /// Create Custom HttpClient for Communicating with LLM API ... /// /// /// public virtual HttpClient GetHttpClient(string url = null) { // var result = new HttpClient { // // Disable timeout completely (not recommended for production) Timeout = Timeout.InfiniteTimeSpan, // BaseAddress = url.IsNullOrEmpty() ? null : new Uri(url), // // OR set a very long timeout (e.g., 30 minutes for large file uploads) // Timeout = TimeSpan.FromMinutes(30) }; // return result; } /// /// 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 Data Provider Based Actions ... /// /// Load AI Projects ... /// /// /// /// /// public async Task> LoadProjects( string ownerId, string connectionId = null, 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 ); // // 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]; defaultProjectConversations = [defaultConversation, .. defaultProjectConversations]; result = [defaultProject, .. result]; // return result; } /// /// Ask a Question as Prompt and Retrieve Response ... /// /// /// /// /// /// /// /// /// public async Task AskAsync( string prompt, string ownerId, Guid projectId, Guid conversationId, string connectionId = null, IFormFileCollection files = 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 ); // // 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(); // // 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, Role = XAiChatRole.System, CreatedOn = DateTime.UtcNow, ConversationId = conversationId }; result = await dataProvider.AddMessage( item: result, connectionId: connectionId, conversationId: conversationId, cancellationToken: cancellationToken ); result = await dataProvider.Enrich( source: result, cancellationToken: cancellationToken ); // return result; } /// /// Ask a Question as Prompt and Retrieve Response as Async Enumerable ... /// /// /// /// /// /// /// /// /// public 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 = 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(); // if (messages.HasChild()) { // // if Messages Has Child, this means // Default Messages Handled before ... result = [.. messages.Select(m => m.ToChatMessages())]; } else { // // If Message Has not any Childs, this means // First Asking Questions ... // // 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); } } // 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 = 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 } }