diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs new file mode 100644 index 0000000..005475e --- /dev/null +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -0,0 +1,143 @@ +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +using AIStudio.Chat; +using AIStudio.Provider.OpenAI; +using AIStudio.Settings; + +namespace AIStudio.Provider.Helmholtz; + +public sealed class ProviderHelmholtz(ILogger logger) : BaseProvider("https://api.helmholtz-blablador.fz-juelich.de/v1/", logger) +{ + #region Implementation of IProvider + + /// + public override string Id => LLMProviders.HELMHOLTZ.ToName(); + + /// + public override string InstanceName { get; set; } = "Helmholtz Blablador"; + + /// + public override async IAsyncEnumerable StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default) + { + // Get the API key: + var requestedSecret = await RUST_SERVICE.GetAPIKey(this); + if(!requestedSecret.Success) + yield break; + + // Prepare the system prompt: + var systemPrompt = new Message + { + Role = "system", + Content = chatThread.PrepareSystemPrompt(settingsManager, chatThread, this.logger), + }; + + // Prepare the Helmholtz HTTP chat request: + var helmholtzChatRequest = JsonSerializer.Serialize(new ChatRequest + { + Model = chatModel.Id, + + // Build the messages: + // - First of all the system prompt + // - Then none-empty user and AI messages + Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message + { + Role = n.Role switch + { + ChatRole.USER => "user", + ChatRole.AI => "assistant", + ChatRole.AGENT => "assistant", + ChatRole.RAG => "assistant", + ChatRole.SYSTEM => "system", + + _ => "user", + }, + + Content = n.Content switch + { + ContentText text => text.Text, + _ => string.Empty, + } + }).ToList()], + Stream = true, + }, JSON_SERIALIZER_OPTIONS); + + async Task RequestBuilder() + { + // Build the HTTP post request: + var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions"); + + // Set the authorization header: + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(helmholtzChatRequest, Encoding.UTF8, "application/json"); + return request; + } + + await foreach (var content in this.StreamChatCompletionInternal("Helmholtz", RequestBuilder, token)) + yield return content; + } + + #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// + public override async IAsyncEnumerable StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) + { + yield break; + } + #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + /// + public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + var models = await this.LoadModels(token, apiKeyProvisional); + return models.Where(model => !model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) && + !model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase)); + } + + /// + public override Task> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + return Task.FromResult(Enumerable.Empty()); + } + + /// + public override async Task> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) + { + var models = await this.LoadModels(token, apiKeyProvisional); + return models.Where(model => + model.Id.StartsWith("alias-embedding", StringComparison.InvariantCultureIgnoreCase) || + model.Id.StartsWith("text-", StringComparison.InvariantCultureIgnoreCase) || + model.Id.Contains("gritlm", StringComparison.InvariantCultureIgnoreCase)); + } + + #endregion + + private async Task> LoadModels(CancellationToken token, string? apiKeyProvisional = null) + { + var secretKey = apiKeyProvisional switch + { + not null => apiKeyProvisional, + _ => await RUST_SERVICE.GetAPIKey(this) switch + { + { Success: true } result => await result.Secret.Decrypt(ENCRYPTION), + _ => null, + } + }; + + if (secretKey is null) + return []; + + using var request = new HttpRequestMessage(HttpMethod.Get, "models"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey); + + using var response = await this.httpClient.SendAsync(request, token); + if(!response.IsSuccessStatusCode) + return []; + + var modelResponse = await response.Content.ReadFromJsonAsync(token); + return modelResponse.Data; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/LLMProviders.cs b/app/MindWork AI Studio/Provider/LLMProviders.cs index 2078e5d..d35fb7b 100644 --- a/app/MindWork AI Studio/Provider/LLMProviders.cs +++ b/app/MindWork AI Studio/Provider/LLMProviders.cs @@ -17,4 +17,6 @@ public enum LLMProviders GROQ = 6, SELF_HOSTED = 4, + + HELMHOLTZ = 9, } \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index f9addf8..554fc50 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -2,6 +2,7 @@ using AIStudio.Provider.Anthropic; using AIStudio.Provider.Fireworks; using AIStudio.Provider.Google; using AIStudio.Provider.Groq; +using AIStudio.Provider.Helmholtz; using AIStudio.Provider.Mistral; using AIStudio.Provider.OpenAI; using AIStudio.Provider.SelfHosted; @@ -34,6 +35,8 @@ public static class LLMProvidersExtensions LLMProviders.SELF_HOSTED => "Self-hosted", + LLMProviders.HELMHOLTZ => "Helmholtz Blablador", + _ => "Unknown", }; @@ -68,6 +71,8 @@ public static class LLMProvidersExtensions LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), + _ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), }; @@ -84,6 +89,7 @@ public static class LLMProvidersExtensions LLMProviders.OPEN_AI => true, LLMProviders.MISTRAL => true, LLMProviders.GOOGLE => true, + LLMProviders.HELMHOLTZ => true, // // Providers that do not support embeddings: @@ -140,6 +146,8 @@ public static class LLMProvidersExtensions LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, host, hostname) { InstanceName = instanceName }, + LLMProviders.HELMHOLTZ => new ProviderHelmholtz(logger) { InstanceName = instanceName }, + _ => new NoProvider(), }; } @@ -161,6 +169,8 @@ public static class LLMProvidersExtensions LLMProviders.GROQ => "https://console.groq.com/", LLMProviders.FIREWORKS => "https://fireworks.ai/login", + LLMProviders.HELMHOLTZ => "https://sdlaml.pages.jsc.fz-juelich.de/ai/guides/blablador_api_access/#step-1-register-on-gitlab", + _ => string.Empty, }; @@ -230,6 +240,7 @@ public static class LLMProvidersExtensions LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, + LLMProviders.HELMHOLTZ => true, LLMProviders.SELF_HOSTED => host is Host.OLLAMA, @@ -246,6 +257,7 @@ public static class LLMProvidersExtensions LLMProviders.GROQ => true, LLMProviders.FIREWORKS => true, + LLMProviders.HELMHOLTZ => true, _ => false, }; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md new file mode 100644 index 0000000..bac28d4 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.31.md @@ -0,0 +1,2 @@ +# v0.9.31, build 206 (2025-02-xx xx:xx UTC) +- Added Helmholtz (aka "Blablador") as provider. This provider is available to all researchers and employees of the 18 Helmholtz Centers as well as all eduGAIN organizations worldwide. \ No newline at end of file