Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Examples/Examples/Chat/ChatExampleXai.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions Examples/Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ static void RegisterExamples(IServiceCollection services)
services.AddTransient<ChatWithTextToSpeechExample>();
services.AddTransient<ChatExampleGroqCloud>();
services.AddTransient<ChatExampleAnthropic>();
services.AddTransient<ChatExampleXai>();
}

async Task RunSelectedExample(IServiceProvider serviceProvider)
Expand Down Expand Up @@ -172,6 +173,7 @@ public class ExampleRegistry(IServiceProvider serviceProvider)
("\u25a0 DeepSeek Chat with reasoning", serviceProvider.GetRequiredService<ChatWithReasoningDeepSeekExample>()),
("\u25a0 GroqCloud Chat", serviceProvider.GetRequiredService<ChatExampleGroqCloud>()),
("\u25a0 Anthropic Chat", serviceProvider.GetRequiredService<ChatExampleAnthropic>()),
("\u25a0 xAI Chat", serviceProvider.GetRequiredService<ChatExampleXai>()),
("\u25a0 McpClient example", serviceProvider.GetRequiredService<McpExample>()),
("\u25a0 McpAgent example", serviceProvider.GetRequiredService<McpAgentsExample>()),
("\u25a0 Chat with TTS example", serviceProvider.GetRequiredService<ChatWithTextToSpeechExample>()),
Expand Down
16 changes: 16 additions & 0 deletions Examples/Examples/Utils/XaiExample.cs
Original file line number Diff line number Diff line change
@@ -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 = "<YOUR_XAI_KEY>";
});
}
}
3 changes: 3 additions & 0 deletions Releases/0.7.9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 0.7.9 release

xAi integration has been added.
2 changes: 1 addition & 1 deletion src/MaIN.Core/.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package>
<metadata>
<id>MaIN.NET</id>
<version>0.7.8</version>
<version>0.7.9</version>
<authors>Wisedev</authors>
<owners>Wisedev</owners>
<icon>favicon.png</icon>
Expand Down
2 changes: 2 additions & 0 deletions src/MaIN.Domain/Configuration/MaINSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -28,4 +29,5 @@ public enum BackendType
DeepSeek = 3,
GroqCloud = 4,
Anthropic = 5,
Xai = 6,
}
5 changes: 5 additions & 0 deletions src/MaIN.Services/Constants/ServiceConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand All @@ -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
Expand Down
112 changes: 112 additions & 0 deletions src/MaIN.Services/Services/ImageGenServices/XaiImageGenService.cs
Original file line number Diff line number Diff line change
@@ -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<ChatResult?> 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<byte[]> ProcessXaiResponse(HttpResponseMessage response)
{
response.EnsureSuccessStatusCode();
var responseData = await response.Content.ReadFromJsonAsync<XaiImageResponse>(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<byte[]> 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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHttpClientFactory>(),
serviceProvider.GetRequiredService<MaINSettings>()),
BackendType.Self => new ImageGenService(serviceProvider.GetRequiredService<IHttpClientFactory>(),
serviceProvider.GetRequiredService<MaINSettings>()),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public ILLMService CreateService(BackendType backendType)
serviceProvider.GetRequiredService<IMemoryFactory>(),
serviceProvider.GetRequiredService<IMemoryService>()),

BackendType.Xai => new XaiService(
serviceProvider.GetRequiredService<MaINSettings>(),
serviceProvider.GetRequiredService<INotificationService>(),
serviceProvider.GetRequiredService<IHttpClientFactory>(),
serviceProvider.GetRequiredService<IMemoryFactory>(),
serviceProvider.GetRequiredService<IMemoryService>()),

BackendType.Anthropic => new AnthropicService(
serviceProvider.GetRequiredService<MaINSettings>(),
serviceProvider.GetRequiredService<INotificationService>(),
Expand Down
1 change: 0 additions & 1 deletion src/MaIN.Services/Services/LLMService/GroqCloudService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 0 additions & 2 deletions src/MaIN.Services/Services/LLMService/OpenAiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
77 changes: 77 additions & 0 deletions src/MaIN.Services/Services/LLMService/XaiService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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;
using System.Text;

namespace MaIN.Services.Services.LLMService;

public sealed class XaiService(
MaINSettings settings,
INotificationService notificationService,
IHttpClientFactory httpClientFactory,
IMemoryFactory memoryFactory,
IMemoryService memoryService,
ILogger<OpenAiService>? logger = null)
: OpenAiCompatibleService(notificationService, httpClientFactory, memoryFactory, memoryService, logger)
{
private readonly MaINSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings));

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 async Task<ChatResult?> AskMemory(
Chat chat,
ChatMemoryOptions memoryOptions,
CancellationToken cancellationToken = default)
{
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();
}
}
12 changes: 12 additions & 0 deletions src/MaIN.Services/Services/McpService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ private PromptExecutionSettings InitializeChatCompletions(IKernelBuilder kernelB
ExtensionData = new Dictionary<string, object>{ ["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.");

Expand All @@ -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");
}
Loading