diff --git a/Configurations/XAiApiConfiguration.cs b/Configurations/XAiApiConfiguration.cs new file mode 100644 index 0000000..90d738e --- /dev/null +++ b/Configurations/XAiApiConfiguration.cs @@ -0,0 +1,12 @@ +using xAiApi.Models; + +namespace xAiApi.Configurations +{ + public class XAiApiConfiguration + { + /// + /// Allowed Models to Use ... + /// + public XAiModelDescriptor[] Models { get; set; } = []; + } +} \ No newline at end of file diff --git a/Constants/XAiProvider.cs b/Constants/XAiProvider.cs new file mode 100644 index 0000000..686caee --- /dev/null +++ b/Constants/XAiProvider.cs @@ -0,0 +1,10 @@ +namespace xAiApi.Constants +{ + public enum XAiProvider + { + None, + Ollama, + DeepSeek, + HuggingFace + } +} \ No newline at end of file diff --git a/Extensions/XModelsExtensions.cs b/Extensions/XModelsExtensions.cs new file mode 100644 index 0000000..45212d7 --- /dev/null +++ b/Extensions/XModelsExtensions.cs @@ -0,0 +1,101 @@ +using System.Linq; +using xAiApi.Configurations; +using xAiApi.Models; +using xCommons.Extensions; + +namespace xAiApi.Extensions +{ + public static class XModelsExtensions + { + /// + /// Validate a Model Descriptor ... + /// + /// + /// + public static bool IsValid(this XAiModelDescriptor source) + { + // + var result = + !source.IsNullOrDefault() && + !source.Name.IsNullOrEmpty() && + !source.Url.IsNullOrEmpty() && + source.Url.IsValidUrl() && + !source.LLM.IsNullOrEmpty() && + source.Provider != Constants.XAiProvider.None; + + // + return result; + } + + /// + /// Validate Configurations ... + /// + /// + /// + public static bool IsValid(this XAiApiConfiguration source) + { + // + var result = + !source.IsNullOrDefault() && + source.Models.HasChild() && + source.Models.All(m => m.IsValid()); + + // + return result; + } + + /// + /// Extract Default Model Descriptor ... + /// + /// + /// + public static XAiModelDescriptor GetDefaultModel(this XAiApiConfiguration source) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + result = source.Models[0]; + } + + // + return result; + } + + /// + /// Retrieve Specified Model from Config ... + /// + /// + /// + /// + public static XAiModelDescriptor GetModel( + this XAiApiConfiguration source, + string name + ) + { + // + XAiModelDescriptor result = null; + + // + if (source.IsValid()) + { + // + if (name.IsNullOrEmpty()) + { + result = source.GetDefaultModel(); + } + else + { + result = source.Models + .FirstOrDefault(m => m.Name + .Equals(name, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + // + return result; + } + } +} \ No newline at end of file diff --git a/Interfaces/IXAIApiServiceBase.cs b/Interfaces/IXAIApiServiceBase.cs index 832d45e..5799066 100644 --- a/Interfaces/IXAIApiServiceBase.cs +++ b/Interfaces/IXAIApiServiceBase.cs @@ -10,7 +10,7 @@ namespace xAiApi.Interfaces /// /// Describe a Base Service Actions of any Ai Based Services ... /// - public interface IAIServiceBase : IDisposable + public interface IXAIServiceBase : IDisposable { /// /// Base Method for Communicate with LLM ... diff --git a/Models/XAiModelDescriptor.cs b/Models/XAiModelDescriptor.cs new file mode 100644 index 0000000..2a1ea4e --- /dev/null +++ b/Models/XAiModelDescriptor.cs @@ -0,0 +1,35 @@ +using xAiApi.Constants; + +namespace xAiApi.Models +{ + /// + /// Describe an Ai Model ... + /// + public class XAiModelDescriptor + { + /// + /// Provider Name ... + /// + public string Name { get; set; } + + /// + /// LLM Provider Url ... + /// + public string Url { get; set; } + + /// + /// Model Name ... + /// + public string LLM { get; set; } + + /// + /// Api Key for Connection ... + /// + public string ApiKey { get; set; } = null; + + /// + /// Model Provider ... + /// + public XAiProvider Provider { get; set; } = XAiProvider.Ollama; + } +} \ No newline at end of file diff --git a/Providers/XAIServiceBase.cs b/Providers/XAIServiceBase.cs new file mode 100644 index 0000000..a1f7cfc --- /dev/null +++ b/Providers/XAIServiceBase.cs @@ -0,0 +1,306 @@ +using System; +using System.Linq; +using System.Threading; +using xAiApi.Interfaces; +using xCommons.Extensions; +using xAiModels.Interfaces; +using xExceptions.Constants; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.AI; +using xAiApi.Models; +using xAiApi.Configurations; +using xAiApi.Extensions; + +namespace xAiApi.Providers +{ + public abstract class XAIServiceBase : IXAIServiceBase + { + private readonly IXAiDataProvider dataProvider; + private readonly ILogger logger; + private readonly XAiApiConfiguration configuration; + + protected XAIServiceBase( + IXAiDataProvider dataProvider, + ILogger logger, + XAiApiConfiguration configuration + ) + { + this.logger = logger; + this.dataProvider = dataProvider; + this.configuration = configuration; + } + + // + #region Actions ... + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async Task AskAsync( + 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; + + // + // Dispose Client ... + client.Dispose(); + + // + return result; + } + + /// + /// Base Method for Communicate with LLM ... + /// + /// + /// + /// + public virtual async IAsyncEnumerable AskAsEnumerable( + 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 + ); + + // + var result = ""; + await foreach (var res in enumerable) + { + // + // Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + result += res.Text; + } + + // + // Dispose Client ... + client.Dispose(); + + // + yield return result; + } + + /// + /// Get LLM Client instance for Communicating with LLM ... + /// + /// + /// + public virtual IChatClient GetClient(string llm = null) + { + // + // Retrieve Model Descriptor ... + var modelDescriptor = configuration.GetModel(llm); + if (modelDescriptor.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + bool isOllamaBased = modelDescriptor.Provider == Constants.XAiProvider.Ollama; + bool isDeepSeekBased = modelDescriptor.Provider == Constants.XAiProvider.DeepSeek; + + // + // Try to Initialize LLm ... + var model = modelDescriptor.LLM; + var apiKey = modelDescriptor.ApiKey; + var url = new Uri(modelDescriptor.Url); + var httpClient = GetHttpClient(modelDescriptor.Url); + + // + IChatClient result = null; + if (isOllamaBased) + { + // + // Instance Ollama Client using Custom HttpCLient ... + var ollamaClient = new OllamaApiClient(httpClient, model); + result = + new ChatClientBuilder(ollamaClient) + .Build(); + } + else if (isDeepSeekBased) + { + // + if (apiKey is null || + string.IsNullOrWhiteSpace(apiKey)) + { + throw new Exception("Inavlid Args ..."); + } + + // + var deepseekClient = new DeepSeekChatClient(apiKey: apiKey); + result = deepseekClient; + } + else + { + // + // TODO: Implement this ... + } + + // + return result ?? throw new NotSupportedException("Chat Client not found ..."); + } + + /// + /// 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 is null + ? 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 + ) + { + // + // User Prompt ... + var promptMessage = new ChatMessage( + ChatRole.User, + prompt + ); + + // + // Preparing Introduction Prompt ... + var introductionPrompt = new ChatMessage( + ChatRole.System, + PromptHelper.GetIntroductionPrompt().Prompt + ); + + // + 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, + PromptHelper.GetIntroductionPrompt().Prompt + ); + + // + List result = + [ + introductionPrompt, + ]; + if (forceMessage && + message is not null) + { + result.Add(message); + } + + // + return result; + } + #endregion + + /// + /// Base Dispose Action ... + /// + /// + public virtual void Dispose() + { } + } +} \ No newline at end of file diff --git a/xAiApi.csproj b/xAiApi.csproj index 790473d..42f3a9d 100644 --- a/xAiApi.csproj +++ b/xAiApi.csproj @@ -47,6 +47,7 @@ +