From aa16fd1dabc8d9bfa442277bf1f2e251891fa36b Mon Sep 17 00:00:00 2001 From: Magdalena Majta Date: Tue, 23 Sep 2025 12:34:14 +0200 Subject: [PATCH 1/5] setup --- Examples/Examples/Chat/ChatExampleXai.cs | 18 +++++++ Examples/Examples/Program.cs | 2 + Examples/Examples/Utils/XaiExample.cs | 16 +++++++ src/MaIN.Domain/Configuration/MaINSettings.cs | 2 + .../Constants/ServiceConstants.cs | 5 ++ .../LLMService/Factory/LLMServiceFactory.cs | 7 +++ .../Services/LLMService/OpenAiService.cs | 2 - .../Services/LLMService/XaiService.cs | 48 +++++++++++++++++++ 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 Examples/Examples/Chat/ChatExampleXai.cs create mode 100644 Examples/Examples/Utils/XaiExample.cs create mode 100644 src/MaIN.Services/Services/LLMService/XaiService.cs diff --git a/Examples/Examples/Chat/ChatExampleXai.cs b/Examples/Examples/Chat/ChatExampleXai.cs new file mode 100644 index 00000000..9f0e7581 --- /dev/null +++ b/Examples/Examples/Chat/ChatExampleXai.cs @@ -0,0 +1,18 @@ +using Examples.Utils; +using MaIN.Core.Hub; + +namespace Examples; + +public class ChatExampleXai : IExample +{ + public async Task Start() + { + XaiExample.Setup(); //We need to provide xAI API key + Console.WriteLine("(xAI) ChatExample is running!"); + + await AIHub.Chat() + .WithModel("grok-3-beta") + .WithMessage("Is the killer whale cute?") + .CompleteAsync(interactive: true); + } +} \ No newline at end of file diff --git a/Examples/Examples/Program.cs b/Examples/Examples/Program.cs index eddabc6e..cc46d0f2 100644 --- a/Examples/Examples/Program.cs +++ b/Examples/Examples/Program.cs @@ -74,6 +74,7 @@ static void RegisterExamples(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } async Task RunSelectedExample(IServiceProvider serviceProvider) @@ -165,6 +166,7 @@ public class ExampleRegistry(IServiceProvider serviceProvider) ("\u25a0 DeepSeek Chat with reasoning", serviceProvider.GetRequiredService()), ("\u25a0 GroqCloud Chat", serviceProvider.GetRequiredService()), ("\u25a0 Anthropic Chat", serviceProvider.GetRequiredService()), + ("\u25a0 xAI Chat", serviceProvider.GetRequiredService()), ("\u25a0 McpClient example", serviceProvider.GetRequiredService()), ("\u25a0 McpAgent example", serviceProvider.GetRequiredService()), ("\u25a0 Chat with TTS example", serviceProvider.GetRequiredService()), diff --git a/Examples/Examples/Utils/XaiExample.cs b/Examples/Examples/Utils/XaiExample.cs new file mode 100644 index 00000000..537fec6b --- /dev/null +++ b/Examples/Examples/Utils/XaiExample.cs @@ -0,0 +1,16 @@ +using MaIN.Core; +using MaIN.Domain.Configuration; + +namespace Examples.Utils; + +public class XaiExample +{ + public static void Setup() + { + MaINBootstrapper.Initialize(configureSettings: (options) => + { + options.BackendType = BackendType.Xai; + options.XaiKey = ""; + }); + } +} \ No newline at end of file diff --git a/src/MaIN.Domain/Configuration/MaINSettings.cs b/src/MaIN.Domain/Configuration/MaINSettings.cs index 96e6d452..17f7e062 100644 --- a/src/MaIN.Domain/Configuration/MaINSettings.cs +++ b/src/MaIN.Domain/Configuration/MaINSettings.cs @@ -13,6 +13,7 @@ public class MaINSettings public string? DeepSeekKey { get; set; } public string? AnthropicKey { get; set; } public string? GroqCloudKey { get; set; } + public string? XaiKey { get; set; } public MongoDbSettings? MongoDbSettings { get; set; } public FileSystemSettings? FileSystemSettings { get; set; } public SqliteSettings? SqliteSettings { get; set; } @@ -28,4 +29,5 @@ public enum BackendType DeepSeek = 3, GroqCloud = 4, Anthropic = 5, + Xai = 6, } \ No newline at end of file diff --git a/src/MaIN.Services/Constants/ServiceConstants.cs b/src/MaIN.Services/Constants/ServiceConstants.cs index 3b0eae18..1d7bea71 100644 --- a/src/MaIN.Services/Constants/ServiceConstants.cs +++ b/src/MaIN.Services/Constants/ServiceConstants.cs @@ -10,6 +10,7 @@ public static class HttpClients public const string DeepSeekClient = "DeepSeekClient"; public const string GroqCloudClient = "GroqCloudClient"; public const string AnthropicClient = "AnthropicClient"; + public const string XaiClient = "XaiClient"; public const string ImageDownloadClient = "ImageDownloadClient"; public const string ModelContextDownloadClient = "ModelContextDownloadClient"; } @@ -36,6 +37,10 @@ public static class ApiUrls public const string AnthropicChatMessages = "https://api.anthropic.com/v1/messages"; public const string AnthropicModels = "https://api.anthropic.com/v1/models"; + + public const string XaiImageGenerations = "https://api.x.ai/v1/images/generations"; + public const string XaiOpenAiChatCompletions = "https://api.x.ai/v1/chat/completions"; + public const string XaiModels = "https://api.x.ai/v1/models"; } public static class Messages diff --git a/src/MaIN.Services/Services/LLMService/Factory/LLMServiceFactory.cs b/src/MaIN.Services/Services/LLMService/Factory/LLMServiceFactory.cs index 132dfb07..bff97bbb 100644 --- a/src/MaIN.Services/Services/LLMService/Factory/LLMServiceFactory.cs +++ b/src/MaIN.Services/Services/LLMService/Factory/LLMServiceFactory.cs @@ -39,6 +39,13 @@ public ILLMService CreateService(BackendType backendType) serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()), + BackendType.Xai => new XaiService( + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()), + BackendType.Anthropic => new AnthropicService( serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), diff --git a/src/MaIN.Services/Services/LLMService/OpenAiService.cs b/src/MaIN.Services/Services/LLMService/OpenAiService.cs index 4edbdd58..8bd68b34 100644 --- a/src/MaIN.Services/Services/LLMService/OpenAiService.cs +++ b/src/MaIN.Services/Services/LLMService/OpenAiService.cs @@ -2,8 +2,6 @@ using MaIN.Services.Services.Abstract; using Microsoft.Extensions.Logging; using MaIN.Services.Services.LLMService.Memory; -using System.Net.Http.Headers; -using System.Text.Json; namespace MaIN.Services.Services.LLMService; diff --git a/src/MaIN.Services/Services/LLMService/XaiService.cs b/src/MaIN.Services/Services/LLMService/XaiService.cs new file mode 100644 index 00000000..bd70fc4e --- /dev/null +++ b/src/MaIN.Services/Services/LLMService/XaiService.cs @@ -0,0 +1,48 @@ +using MaIN.Domain.Configuration; +using MaIN.Services.Constants; +using MaIN.Services.Services.Models; +using MaIN.Domain.Entities; +using MaIN.Services.Services.Abstract; +using MaIN.Services.Services.LLMService.Memory; +using Microsoft.Extensions.Logging; + +namespace MaIN.Services.Services.LLMService; + +public sealed class XaiService( + MaINSettings settings, + INotificationService notificationService, + IHttpClientFactory httpClientFactory, + IMemoryFactory memoryFactory, + IMemoryService memoryService, + ILogger? logger = null) + : OpenAiCompatibleService(notificationService, httpClientFactory, memoryFactory, memoryService, logger) +{ + private readonly MaINSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + + protected override string HttpClientName => ServiceConstants.HttpClients.XaiClient; + protected override string ChatCompletionsUrl => ServiceConstants.ApiUrls.XaiOpenAiChatCompletions; + protected override string ModelsUrl => ServiceConstants.ApiUrls.XaiModels; + + protected override string GetApiKey() + { + return _settings.XaiKey ?? Environment.GetEnvironmentVariable("XAI_API_KEY") ?? + throw new InvalidOperationException("xAI Key not configured"); + } + + protected override void ValidateApiKey() + { + if (string.IsNullOrEmpty(_settings.XaiKey) && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("XAI_API_KEY"))) + { + throw new InvalidOperationException("xAI Key not configured"); + } + } + + public override Task AskMemory( + Chat chat, + ChatMemoryOptions memoryOptions, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); // todo + } +} \ No newline at end of file From b10ebcbf72c195d1ac5716346b6c99ba9f758f0a Mon Sep 17 00:00:00 2001 From: Magdalena Majta Date: Wed, 24 Sep 2025 01:27:17 +0200 Subject: [PATCH 2/5] AskMemory in XaiService --- .../Services/LLMService/GroqCloudService.cs | 1 - .../Services/LLMService/XaiService.cs | 35 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/MaIN.Services/Services/LLMService/GroqCloudService.cs b/src/MaIN.Services/Services/LLMService/GroqCloudService.cs index 724f2518..12ca5d2a 100644 --- a/src/MaIN.Services/Services/LLMService/GroqCloudService.cs +++ b/src/MaIN.Services/Services/LLMService/GroqCloudService.cs @@ -19,7 +19,6 @@ public sealed class GroqCloudService( : OpenAiCompatibleService(notificationService, httpClientFactory, memoryFactory, memoryService, logger) { private readonly MaINSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); protected override string HttpClientName => ServiceConstants.HttpClients.GroqCloudClient; protected override string ChatCompletionsUrl => ServiceConstants.ApiUrls.GroqCloudOpenAiChatCompletions; diff --git a/src/MaIN.Services/Services/LLMService/XaiService.cs b/src/MaIN.Services/Services/LLMService/XaiService.cs index bd70fc4e..0b1cd5b1 100644 --- a/src/MaIN.Services/Services/LLMService/XaiService.cs +++ b/src/MaIN.Services/Services/LLMService/XaiService.cs @@ -5,6 +5,7 @@ using MaIN.Services.Services.Abstract; using MaIN.Services.Services.LLMService.Memory; using Microsoft.Extensions.Logging; +using System.Text; namespace MaIN.Services.Services.LLMService; @@ -18,7 +19,6 @@ public sealed class XaiService( : OpenAiCompatibleService(notificationService, httpClientFactory, memoryFactory, memoryService, logger) { private readonly MaINSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); protected override string HttpClientName => ServiceConstants.HttpClients.XaiClient; protected override string ChatCompletionsUrl => ServiceConstants.ApiUrls.XaiOpenAiChatCompletions; @@ -38,11 +38,40 @@ protected override void ValidateApiKey() } } - public override Task AskMemory( + public override async Task AskMemory( Chat chat, ChatMemoryOptions memoryOptions, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); // todo + var lastMsg = chat.Messages.Last(); + var filePaths = await DocumentProcessor.ConvertToFilesContent(memoryOptions); + var message = new Message() + { + Role = ServiceConstants.Roles.User, + Content = ComposeMessage(lastMsg, filePaths), + Type = MessageType.CloudLLM + }; + + chat.Messages.Last().Content = message.Content; + chat.Messages.Last().Files = []; + var result = await Send(chat, new ChatRequestOptions(), cancellationToken); + chat.Messages.Last().Content = lastMsg.Content; + return result; + } + + private string ComposeMessage(Message lastMsg, string[] filePaths) + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine($"== FILES IN MEMORY"); + foreach (var path in filePaths) + { + var doc = DocumentProcessor.ProcessDocument(path); + stringBuilder.Append(doc); + stringBuilder.AppendLine(); + } + stringBuilder.AppendLine($"== END OF FILES"); + stringBuilder.AppendLine(); + stringBuilder.Append(lastMsg.Content); + return stringBuilder.ToString(); } } \ No newline at end of file From 9d52e657bf45e64e093c273ed2f28981d435d093 Mon Sep 17 00:00:00 2001 From: Magdalena Majta Date: Wed, 24 Sep 2025 14:21:58 +0200 Subject: [PATCH 3/5] Add XaiImageGenService --- .../ImageGenServices/XaiImageGenService.cs | 112 ++++++++++++++++++ .../Factory/ImageGenServiceFactory.cs | 2 + 2 files changed, 114 insertions(+) create mode 100644 src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs diff --git a/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs b/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs new file mode 100644 index 00000000..e6be9f6c --- /dev/null +++ b/src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs @@ -0,0 +1,112 @@ +using MaIN.Domain.Configuration; +using MaIN.Domain.Entities; +using MaIN.Services.Constants; +using MaIN.Services.Services.Abstract; +using MaIN.Services.Services.Models; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace MaIN.Services.Services.ImageGenServices; + +public class XaiImageGenService( + IHttpClientFactory httpClientFactory, + MaINSettings settings) + : IImageGenService +{ + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + private readonly MaINSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + + public async Task Send(Chat chat) + { + var client = _httpClientFactory.CreateClient(ServiceConstants.HttpClients.XaiClient); + string apiKey = _settings.XaiKey ?? Environment.GetEnvironmentVariable("XAI_API_KEY") ?? + throw new InvalidOperationException("xAI Key not configured"); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + var requestBody = new + { + model = string.IsNullOrWhiteSpace(chat.Model) ? Models.GROK_IMAGE : chat.Model, + prompt = BuildPromptFromChat(chat), + n = 1, + response_format = "b64_json" //or "url" + }; + + using var response = await client.PostAsJsonAsync(ServiceConstants.ApiUrls.XaiImageGenerations, requestBody); + var imageBytes = await ProcessXaiResponse(response); + return CreateChatResult(imageBytes); + } + + private static string BuildPromptFromChat(Chat chat) + { + return chat.Messages + .Select((msg, index) => index == 0 ? msg.Content : $"&& {msg.Content}") + .Aggregate((current, next) => $"{current} {next}"); + } + + private async Task ProcessXaiResponse(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + var responseData = await response.Content.ReadFromJsonAsync(new JsonSerializerOptions{ PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }); + + var first = responseData?.Data.FirstOrDefault() + ?? throw new InvalidOperationException("No image data returned from xAI"); + + if (!string.IsNullOrEmpty(first.B64Json)) + { + return Convert.FromBase64String(first.B64Json); + } + + if (!string.IsNullOrEmpty(first.Url)) + { + return await DownloadImageAsync(first.Url); + } + + throw new InvalidOperationException("No image content returned from xAI"); + } + + private async Task DownloadImageAsync(string imageUrl) + { + var imageClient = _httpClientFactory.CreateClient(ServiceConstants.HttpClients.ImageDownloadClient); + + using var imageResponse = await imageClient.GetAsync(imageUrl); + imageResponse.EnsureSuccessStatusCode(); + + return await imageResponse.Content.ReadAsByteArrayAsync(); + } + + private static ChatResult CreateChatResult(byte[] imageBytes) + { + return new ChatResult + { + Done = true, + Message = new Message + { + Content = ServiceConstants.Messages.GeneratedImageContent, + Role = ServiceConstants.Roles.Assistant, + Image = imageBytes, + Type = MessageType.Image + }, + Model = Models.GROK_IMAGE, + CreatedAt = DateTime.UtcNow + }; + } + + private struct Models + { + public const string GROK_IMAGE = "grok-2-image"; + } +} + + +file class XaiImageResponse +{ + public XaiImageData[] Data { get; set; } = []; +} + +file class XaiImageData +{ + public string? Url { get; set; } + public string? B64Json { get; set; } + public string? RevisedPrompt { get; set; } +} \ No newline at end of file diff --git a/src/MaIN.Services/Services/LLMService/Factory/ImageGenServiceFactory.cs b/src/MaIN.Services/Services/LLMService/Factory/ImageGenServiceFactory.cs index 2d44c5c6..1bedeebc 100644 --- a/src/MaIN.Services/Services/LLMService/Factory/ImageGenServiceFactory.cs +++ b/src/MaIN.Services/Services/LLMService/Factory/ImageGenServiceFactory.cs @@ -18,6 +18,8 @@ public class ImageGenServiceFactory(IServiceProvider serviceProvider) : IImageGe BackendType.DeepSeek => null, BackendType.GroqCloud => null, BackendType.Anthropic => null, + BackendType.Xai => new XaiImageGenService(serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService()), BackendType.Self => new ImageGenService(serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService()), From 65ef696666dca258969c7f8c3922ed673fd79390 Mon Sep 17 00:00:00 2001 From: Magdalena Majta Date: Wed, 19 Nov 2025 16:07:15 +0100 Subject: [PATCH 4/5] Add Xai backend support to McpService --- src/MaIN.Services/Services/McpService.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/MaIN.Services/Services/McpService.cs b/src/MaIN.Services/Services/McpService.cs index 4ec74f01..06de96ba 100644 --- a/src/MaIN.Services/Services/McpService.cs +++ b/src/MaIN.Services/Services/McpService.cs @@ -107,6 +107,16 @@ private PromptExecutionSettings InitializeChatCompletions(IKernelBuilder kernelB ExtensionData = new Dictionary{ ["max_tokens"] = 4096 } }; + case BackendType.Xai: + kernelBuilder.Services.AddOpenAIChatCompletion( + modelId: model, + apiKey: GetXaiKey() ?? throw new ArgumentNullException(nameof(GetXaiKey)), + endpoint: new Uri("https://api.x.ai/v1")); + return new OpenAIPromptExecutionSettings() + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true }) + }; + case BackendType.Self: throw new NotSupportedException("Self backend (local models) does not support MCP integration."); @@ -123,4 +133,6 @@ private PromptExecutionSettings InitializeChatCompletions(IKernelBuilder kernelB => settings.GroqCloudKey ?? Environment.GetEnvironmentVariable("GROQ_API_KEY"); string? GetAnthropicKey() => settings.AnthropicKey ?? Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"); + string? GetXaiKey() + => settings.XaiKey ?? Environment.GetEnvironmentVariable("XAI_API_KEY"); } \ No newline at end of file From 31e6a018e580a52213ada70fc414cf58f1f3cb8e Mon Sep 17 00:00:00 2001 From: Magdalena Majta Date: Wed, 19 Nov 2025 16:24:07 +0100 Subject: [PATCH 5/5] versioning --- Releases/0.7.9.md | 3 +++ src/MaIN.Core/.nuspec | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 Releases/0.7.9.md diff --git a/Releases/0.7.9.md b/Releases/0.7.9.md new file mode 100644 index 00000000..1d04d603 --- /dev/null +++ b/Releases/0.7.9.md @@ -0,0 +1,3 @@ +# 0.7.9 release + +xAi integration has been added. \ No newline at end of file diff --git a/src/MaIN.Core/.nuspec b/src/MaIN.Core/.nuspec index da1f63af..78f647b4 100644 --- a/src/MaIN.Core/.nuspec +++ b/src/MaIN.Core/.nuspec @@ -2,7 +2,7 @@ MaIN.NET - 0.7.8 + 0.7.9 Wisedev Wisedev favicon.png