mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-21 19:38:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a66d88c989 | ||
|
|
2e84f10a97 | ||
|
|
dbc2b4f6bc | ||
|
|
9d2b63bbaa | ||
|
|
277dc73468 | ||
|
|
ccac2b04ac | ||
|
|
0c4b94c527 | ||
|
|
37a9046be4 | ||
|
|
b984b4432e | ||
|
|
5e3b632eee | ||
|
|
99dac520cb | ||
|
|
e21e30ce8c |
No files matched your search
+5
-5
@@ -2,13 +2,13 @@
|
||||
* @MindWorkAI/maintainer
|
||||
|
||||
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
|
||||
/.github/ @MindWorkAI/release
|
||||
|
||||
# The release team is responsible for the update directory:
|
||||
/.updates/ @MindWorkAI/release
|
||||
/.github/ @MindWorkAI/release @SommerEngineering
|
||||
|
||||
# Our Rust experts are responsible for the Rust codebase:
|
||||
/runtime/ @MindWorkAI/rust-experts
|
||||
|
||||
# Our .NET experts are responsible for the .NET codebase:
|
||||
/app/ @MindWorkAI/net-experts
|
||||
/app/ @MindWorkAI/net-experts
|
||||
|
||||
# The source code rules must be reviewed by the release team:
|
||||
/app/SourceCodeRules/ @MindWorkAI/release @SommerEngineering
|
||||
@@ -56,7 +56,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
|
||||
|
||||
**Key advantages:**
|
||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/).
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/). For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
|
||||
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
|
||||
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
||||
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||
@@ -9,6 +10,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -2,15 +2,12 @@ namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public static class AllowedLLMProvidersExtensions
|
||||
{
|
||||
public static string Description(this AllowedLLMProviders provider)
|
||||
public static string Description(this AllowedLLMProviders provider) => provider switch
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
|
||||
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
|
||||
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
|
||||
|
||||
_ => "Unknown option was selected"
|
||||
};
|
||||
}
|
||||
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
|
||||
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
|
||||
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
|
||||
|
||||
_ => "Unknown option was selected"
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,8 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (206, "v0.9.31, build 206 (2025-03-03 15:33 UTC)", "v0.9.31.md"),
|
||||
new (205, "v0.9.30, build 205 (2025-02-24 19:55 UTC)", "v0.9.30.md"),
|
||||
new (204, "v0.9.29, build 204 (2025-02-24 13:48 UTC)", "v0.9.29.md"),
|
||||
new (203, "v0.9.28, build 203 (2025-02-09 16:33 UTC)", "v0.9.28.md"),
|
||||
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
|
||||
|
||||
@@ -14,7 +14,7 @@ public partial class MudTextList : ComponentBase
|
||||
public string Icon { get; set; } = Icons.Material.Filled.CheckCircle;
|
||||
|
||||
[Parameter]
|
||||
public string Class { get; set; } = "";
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
private string Classes => $"mud-text-list {this.Class}";
|
||||
}
|
||||
|
||||
@@ -26,6 +26,38 @@
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="Changelog">
|
||||
<Changelog/>
|
||||
</ExpansionPanel>
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Book" HeaderText="Logbook">
|
||||
<MudText Typo="Typo.h4">
|
||||
Explanation
|
||||
</MudText>
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
AI Studio creates a log file at startup, in which events during startup are recorded. After startup,
|
||||
another log file is created that records all events that occur during the use of the app. This
|
||||
includes any errors that may occur. Depending on when an error occurs (at startup or during use),
|
||||
the contents of these log files can be helpful for troubleshooting. Sensitive information such as
|
||||
passwords is not included in the log files.
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
By clicking on the respective path, the path is copied to the clipboard. You might open these files
|
||||
with a text editor to view their contents.
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudText Typo="Typo.h4">
|
||||
Startup log file
|
||||
</MudText>
|
||||
<MudList T="string" Class="mb-3">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogStartupPath" OnClick="() => this.CopyStartupLogPath()"/>
|
||||
</MudList>
|
||||
|
||||
<MudText Typo="Typo.h4">
|
||||
Usage log file
|
||||
</MudText>
|
||||
<MudList T="string" Class="mb-3">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Folder" Text="@this.logPaths.LogAppPath" OnClick="() => this.CopyAppLogPath()"/>
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Engineering" HeaderText="Motivation">
|
||||
<Motivation/>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
@@ -9,9 +12,15 @@ public partial class About : ComponentBase
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
|
||||
|
||||
private static string VersionDotnetRuntime => $"Used .NET runtime: v{META_DATA.DotnetVersion}";
|
||||
|
||||
private static string VersionDotnetSdk => $"Used .NET SDK: v{META_DATA.DotnetSdkVersion}";
|
||||
@@ -26,6 +35,28 @@ public partial class About : ComponentBase
|
||||
|
||||
private static string TauriVersion => $"Tauri: v{META_DATA.TauriVersion}";
|
||||
|
||||
private GetLogPathsResponse logPaths;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
private async Task CopyStartupLogPath()
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||
}
|
||||
|
||||
private async Task CopyAppLogPath()
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogAppPath);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.logPaths = await this.RustService.GetLogPaths();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private const string LICENSE = """
|
||||
# Functional Source License, Version 1.1, MIT Future License
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public partial class Home : ComponentBase
|
||||
private static readonly TextItem[] ITEMS_ADVANTAGES =
|
||||
[
|
||||
new("Free of charge", "The app is free to use, both for personal and commercial purposes."),
|
||||
new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks."),
|
||||
new("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using llama.cpp, ollama, LM Studio, Groq, or Fireworks. For scientists and employees of research institutions, we also support Helmholtz and GWDG AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities."),
|
||||
new("Assistants", "You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants."),
|
||||
new("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
|
||||
new("Cost-effective", "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -87,21 +88,15 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(new[]
|
||||
var additionalModels = new[]
|
||||
{
|
||||
new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (latest)"),
|
||||
new Model("claude-3-5-sonnet-20240620", "Claude 3.5 Sonnet (20. June 2024)"),
|
||||
new Model("claude-3-5-sonnet-20241022", "Claude 3.5 Sonnet (22. October 2024)"),
|
||||
|
||||
new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (latest)"),
|
||||
new Model("claude-3-5-heiku-20241022", "Claude 3.5 Haiku (22. October 2024)"),
|
||||
|
||||
new Model("claude-3-opus-20240229", "Claude 3.0 Opus (29. February 2024)"),
|
||||
new Model("claude-3-opus-latest", "Claude 3.0 Opus (latest)"),
|
||||
|
||||
new Model("claude-3-sonnet-20240229", "Claude 3.0 Sonnet (29. February 2024)"),
|
||||
new Model("claude-3-haiku-20240307", "Claude 3.0 Haiku (7. March 2024)"),
|
||||
}.AsEnumerable());
|
||||
new Model("claude-3-7-sonnet-latest", "Claude 3.7 Sonnet (Latest)"),
|
||||
new Model("claude-3-5-sonnet-latest", "Claude 3.5 Sonnet (Latest)"),
|
||||
new Model("claude-3-5-haiku-latest", "Claude 3.5 Haiku (Latest)"),
|
||||
new Model("claude-3-opus-latest", "Claude 3 Opus (Latest)"),
|
||||
};
|
||||
|
||||
return this.LoadModels(token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -117,4 +112,37 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> 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?limit=100");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Add("x-api-key", secretKey);
|
||||
|
||||
// Set the Anthropic version:
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
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<ModelsResponse>(JSON_SERIALIZER_OPTIONS, token);
|
||||
return modelResponse.Data;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,12 @@ public sealed record Confidence
|
||||
Description = "The provider operates its service from the USA and is subject to **US jurisdiction**. In case of suspicion, authorities in the USA can access your data. However, **your data is not used for training** purposes.",
|
||||
};
|
||||
|
||||
public static readonly Confidence CHINA_NO_TRAINING = new()
|
||||
{
|
||||
Level = ConfidenceLevel.MODERATE,
|
||||
Description = "The provider operates its service from China. In case of suspicion, authorities in the respective countries of operation may access your data. However, **your data is not used for training** purposes.",
|
||||
};
|
||||
|
||||
public static readonly Confidence GDPR_NO_TRAINING = new()
|
||||
{
|
||||
Level = ConfidenceLevel.MEDIUM,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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.DeepSeek;
|
||||
|
||||
public sealed class ProviderDeepSeek(ILogger logger) : BaseProvider("https://api.deepseek.com/", logger)
|
||||
{
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.DEEP_SEEK.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "DeepSeek";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> 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 DeepSeek HTTP chat request:
|
||||
var deepSeekChatRequest = 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<HttpRequestMessage> 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(deepSeekChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("Helmholtz", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> 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
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return this.LoadModels(token, apiKeyProvisional);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> 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<ModelsResponse>(token);
|
||||
return modelResponse.Data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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.GWDG;
|
||||
|
||||
public sealed class ProviderGWDG(ILogger logger) : BaseProvider("https://chat-ai.academiccloud.de/v1/", logger)
|
||||
{
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.GWDG.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "GWDG SAIA";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> 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 GWDG HTTP chat request:
|
||||
var gwdgChatRequest = 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<HttpRequestMessage> 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(gwdgChatRequest, Encoding.UTF8, "application/json");
|
||||
return request;
|
||||
}
|
||||
|
||||
await foreach (var content in this.StreamChatCompletionInternal<ResponseStreamLine>("GWDG", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> 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
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(token, apiKeyProvisional);
|
||||
return models.Where(model => !model.Id.StartsWith("e5-mistral-7b-instruct", StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
var models = await this.LoadModels(token, apiKeyProvisional);
|
||||
return models.Where(model => model.Id.StartsWith("e5-", StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task<IEnumerable<Model>> 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<ModelsResponse>(token);
|
||||
return modelResponse.Data;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Id => LLMProviders.HELMHOLTZ.ToName();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string InstanceName { get; set; } = "Helmholtz Blablador";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<string> 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<HttpRequestMessage> 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<ResponseStreamLine>("Helmholtz", RequestBuilder, token))
|
||||
yield return content;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ImageURL> 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
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<IEnumerable<Model>> 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<IEnumerable<Model>> 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<ModelsResponse>(token);
|
||||
return modelResponse.Data;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,13 @@ public enum LLMProviders
|
||||
MISTRAL = 3,
|
||||
GOOGLE = 7,
|
||||
X = 8,
|
||||
DEEP_SEEK = 11,
|
||||
|
||||
FIREWORKS = 5,
|
||||
GROQ = 6,
|
||||
|
||||
SELF_HOSTED = 4,
|
||||
|
||||
HELMHOLTZ = 9,
|
||||
GWDG = 10,
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using AIStudio.Provider.Anthropic;
|
||||
using AIStudio.Provider.DeepSeek;
|
||||
using AIStudio.Provider.Fireworks;
|
||||
using AIStudio.Provider.Google;
|
||||
using AIStudio.Provider.Groq;
|
||||
using AIStudio.Provider.GWDG;
|
||||
using AIStudio.Provider.Helmholtz;
|
||||
using AIStudio.Provider.Mistral;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
@@ -28,12 +31,16 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.MISTRAL => "Mistral",
|
||||
LLMProviders.GOOGLE => "Google",
|
||||
LLMProviders.X => "xAI",
|
||||
LLMProviders.DEEP_SEEK => "DeepSeek",
|
||||
|
||||
LLMProviders.GROQ => "Groq",
|
||||
LLMProviders.FIREWORKS => "Fireworks.ai",
|
||||
|
||||
LLMProviders.SELF_HOSTED => "Self-hosted",
|
||||
|
||||
LLMProviders.HELMHOLTZ => "Helmholtz Blablador",
|
||||
LLMProviders.GWDG => "GWDG SAIA",
|
||||
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
@@ -66,8 +73,13 @@ public static class LLMProvidersExtensions
|
||||
|
||||
LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
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)),
|
||||
LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
|
||||
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)),
|
||||
};
|
||||
|
||||
@@ -84,6 +96,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.OPEN_AI => true,
|
||||
LLMProviders.MISTRAL => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.HELMHOLTZ => true,
|
||||
|
||||
//
|
||||
// Providers that do not support embeddings:
|
||||
@@ -92,6 +105,8 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => false,
|
||||
LLMProviders.FIREWORKS => false,
|
||||
LLMProviders.X => false,
|
||||
LLMProviders.GWDG => false,
|
||||
LLMProviders.DEEP_SEEK => false,
|
||||
|
||||
//
|
||||
// Self-hosted providers are treated as a special case anyway.
|
||||
@@ -134,12 +149,16 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.MISTRAL => new ProviderMistral(logger) { InstanceName = instanceName },
|
||||
LLMProviders.GOOGLE => new ProviderGoogle(logger) { InstanceName = instanceName },
|
||||
LLMProviders.X => new ProviderX(logger) { InstanceName = instanceName },
|
||||
LLMProviders.DEEP_SEEK => new ProviderDeepSeek(logger) { InstanceName = instanceName },
|
||||
|
||||
LLMProviders.GROQ => new ProviderGroq(logger) { InstanceName = instanceName },
|
||||
LLMProviders.FIREWORKS => new ProviderFireworks(logger) { InstanceName = instanceName },
|
||||
|
||||
LLMProviders.SELF_HOSTED => new ProviderSelfHosted(logger, host, hostname) { InstanceName = instanceName },
|
||||
|
||||
LLMProviders.HELMHOLTZ => new ProviderHelmholtz(logger) { InstanceName = instanceName },
|
||||
LLMProviders.GWDG => new ProviderGWDG(logger) { InstanceName = instanceName },
|
||||
|
||||
_ => new NoProvider(),
|
||||
};
|
||||
}
|
||||
@@ -157,10 +176,14 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => "https://console.anthropic.com/dashboard",
|
||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/",
|
||||
LLMProviders.X => "https://accounts.x.ai/sign-up",
|
||||
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/sign_up",
|
||||
|
||||
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",
|
||||
LLMProviders.GWDG => "https://docs.hpc.gwdg.de/services/saia/index.html#api-request",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
@@ -173,6 +196,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GROQ => "https://console.groq.com/settings/usage",
|
||||
LLMProviders.GOOGLE => "https://console.cloud.google.com/billing",
|
||||
LLMProviders.FIREWORKS => "https://fireworks.ai/account/billing",
|
||||
LLMProviders.DEEP_SEEK => "https://platform.deepseek.com/usage",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
@@ -186,6 +210,7 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.DEEP_SEEK => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
@@ -227,9 +252,12 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.X => true,
|
||||
LLMProviders.DEEP_SEEK => true,
|
||||
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.HELMHOLTZ => true,
|
||||
LLMProviders.GWDG => true,
|
||||
|
||||
LLMProviders.SELF_HOSTED => host is Host.OLLAMA,
|
||||
|
||||
@@ -243,9 +271,12 @@ public static class LLMProvidersExtensions
|
||||
LLMProviders.ANTHROPIC => true,
|
||||
LLMProviders.GOOGLE => true,
|
||||
LLMProviders.X => true,
|
||||
LLMProviders.DEEP_SEEK => true,
|
||||
|
||||
LLMProviders.GROQ => true,
|
||||
LLMProviders.FIREWORKS => true,
|
||||
LLMProviders.HELMHOLTZ => true,
|
||||
LLMProviders.GWDG => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -15,19 +15,11 @@ public static class HostExtensions
|
||||
|
||||
public static string BaseURL(this Host host) => host switch
|
||||
{
|
||||
Host.LM_STUDIO => "/v1/",
|
||||
Host.LLAMACPP => "/v1/",
|
||||
Host.OLLAMA => "/v1/",
|
||||
|
||||
_ => "/v1/",
|
||||
};
|
||||
|
||||
public static string ChatURL(this Host host) => host switch
|
||||
{
|
||||
Host.LM_STUDIO => "chat/completions",
|
||||
Host.LLAMACPP => "chat/completions",
|
||||
Host.OLLAMA => "chat/completions",
|
||||
|
||||
_ => "chat/completions",
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ internal static class Redirect
|
||||
await nextHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class DataLLMProviders
|
||||
/// <summary>
|
||||
/// Which confidence scheme to use.
|
||||
/// </summary>
|
||||
public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_USA_EUROPE;
|
||||
public ConfidenceSchemes ConfidenceScheme { get; set; } = ConfidenceSchemes.TRUST_ALL;
|
||||
|
||||
/// <summary>
|
||||
/// Provide custom confidence levels for each LLM provider.
|
||||
|
||||
@@ -10,15 +10,12 @@ public static class DataSourceTypeExtension
|
||||
/// </summary>
|
||||
/// <param name="type">The data source type.</param>
|
||||
/// <returns>The display name of the data source type.</returns>
|
||||
public static string GetDisplayName(this DataSourceType type)
|
||||
public static string GetDisplayName(this DataSourceType type) => type switch
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
DataSourceType.LOCAL_FILE => "Local File",
|
||||
DataSourceType.LOCAL_DIRECTORY => "Local Directory",
|
||||
DataSourceType.ERI_V1 => "External ERI Server (v1)",
|
||||
|
||||
_ => "None",
|
||||
};
|
||||
}
|
||||
DataSourceType.LOCAL_FILE => "Local File",
|
||||
DataSourceType.LOCAL_DIRECTORY => "Local Directory",
|
||||
DataSourceType.ERI_V1 => "External ERI Server (v1)",
|
||||
|
||||
_ => "None",
|
||||
};
|
||||
}
|
||||
@@ -2,15 +2,12 @@ namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public static class ThemesExtensions
|
||||
{
|
||||
public static string GetName(this Themes theme)
|
||||
public static string GetName(this Themes theme) => theme switch
|
||||
{
|
||||
return theme switch
|
||||
{
|
||||
Themes.SYSTEM => "Synchronized with the operating system settings",
|
||||
Themes.LIGHT => "Always use light theme",
|
||||
Themes.DARK => "Always use dark theme",
|
||||
|
||||
_ => "Unknown setting",
|
||||
};
|
||||
}
|
||||
Themes.SYSTEM => "Synchronized with the operating system settings",
|
||||
Themes.LIGHT => "Always use light theme",
|
||||
Themes.DARK => "Always use dark theme",
|
||||
|
||||
_ => "Unknown setting",
|
||||
};
|
||||
}
|
||||
@@ -211,11 +211,19 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
|
||||
switch (this.ConfigurationData.LLMProviders.ConfidenceScheme)
|
||||
{
|
||||
case ConfidenceSchemes.TRUST_ALL:
|
||||
return llmProvider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
|
||||
|
||||
_ => ConfidenceLevel.MEDIUM,
|
||||
};
|
||||
|
||||
case ConfidenceSchemes.TRUST_USA_EUROPE:
|
||||
return llmProvider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
|
||||
LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
|
||||
LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
|
||||
|
||||
_ => ConfidenceLevel.MEDIUM,
|
||||
};
|
||||
@@ -224,8 +232,10 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
return llmProvider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
|
||||
LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
|
||||
LLMProviders.MISTRAL => ConfidenceLevel.LOW,
|
||||
LLMProviders.HELMHOLTZ => ConfidenceLevel.LOW,
|
||||
LLMProviders.GWDG => ConfidenceLevel.LOW,
|
||||
LLMProviders.DEEP_SEEK => ConfidenceLevel.LOW,
|
||||
|
||||
_ => ConfidenceLevel.MEDIUM,
|
||||
};
|
||||
@@ -234,8 +244,18 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
return llmProvider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
|
||||
LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
|
||||
LLMProviders.MISTRAL => ConfidenceLevel.MEDIUM,
|
||||
LLMProviders.HELMHOLTZ => ConfidenceLevel.MEDIUM,
|
||||
LLMProviders.GWDG => ConfidenceLevel.MEDIUM,
|
||||
|
||||
_ => ConfidenceLevel.LOW,
|
||||
};
|
||||
|
||||
case ConfidenceSchemes.TRUST_ASIA:
|
||||
return llmProvider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
|
||||
LLMProviders.DEEP_SEEK => ConfidenceLevel.MEDIUM,
|
||||
|
||||
_ => ConfidenceLevel.LOW,
|
||||
};
|
||||
@@ -244,7 +264,6 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
return llmProvider switch
|
||||
{
|
||||
LLMProviders.SELF_HOSTED => ConfidenceLevel.HIGH,
|
||||
LLMProviders.FIREWORKS => ConfidenceLevel.UNTRUSTED,
|
||||
|
||||
_ => ConfidenceLevel.VERY_LOW,
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ public static class SettingsMigrations
|
||||
{
|
||||
Version = Version.V2,
|
||||
|
||||
Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = "" }).ToList(),
|
||||
Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = string.Empty }).ToList(),
|
||||
|
||||
EnableSpellchecking = previousData.EnableSpellchecking,
|
||||
IsSavingEnergy = previousData.IsSavingEnergy,
|
||||
|
||||
@@ -2,11 +2,13 @@ namespace AIStudio.Tools;
|
||||
|
||||
public enum ConfidenceSchemes
|
||||
{
|
||||
TRUST_USA_EUROPE = 0,
|
||||
TRUST_USA = 1,
|
||||
TRUST_EUROPE = 2,
|
||||
TRUST_ALL,
|
||||
TRUST_USA_EUROPE,
|
||||
TRUST_USA,
|
||||
TRUST_EUROPE,
|
||||
TRUST_ASIA,
|
||||
|
||||
LOCAL_TRUST_ONLY = 3,
|
||||
LOCAL_TRUST_ONLY,
|
||||
|
||||
CUSTOM = 10_000,
|
||||
}
|
||||
@@ -4,9 +4,11 @@ public static class ConfidenceSchemesExtensions
|
||||
{
|
||||
public static string GetListDescription(this ConfidenceSchemes scheme) => scheme switch
|
||||
{
|
||||
ConfidenceSchemes.TRUST_ALL => "Trust all LLM providers",
|
||||
ConfidenceSchemes.TRUST_USA_EUROPE => "Trust LLM providers from the USA and Europe",
|
||||
ConfidenceSchemes.TRUST_USA => "Trust LLM providers from the USA",
|
||||
ConfidenceSchemes.TRUST_EUROPE => "Trust LLM providers from Europe",
|
||||
ConfidenceSchemes.TRUST_ASIA => "Trust LLM providers from Asia",
|
||||
ConfidenceSchemes.LOCAL_TRUST_ONLY => "Trust only local LLM providers",
|
||||
|
||||
ConfidenceSchemes.CUSTOM => "Configure your own confidence scheme",
|
||||
|
||||
@@ -134,7 +134,7 @@ public static class RetrievalContentTypeExtensions
|
||||
ContentType.VIDEO => RetrievalContentType.NOT_SPECIFIED,
|
||||
|
||||
ContentType.NONE => RetrievalContentType.NOT_SPECIFIED,
|
||||
ContentType.UNKNOWN => RetrievalContentType.UNKNOWN,
|
||||
|
||||
_ => RetrievalContentType.UNKNOWN,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
public readonly record struct GetLogPathsResponse(string LogStartupPath, string LogAppPath);
|
||||
@@ -0,0 +1,15 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the paths of the log files.
|
||||
/// </summary>
|
||||
/// <returns>The paths of the log files.</returns>
|
||||
public async Task<GetLogPathsResponse> GetLogPaths()
|
||||
{
|
||||
return await this.http.GetFromJsonAsync<GetLogPathsResponse>("/log/paths", this.jsonRustSerializerOptions);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,6 @@ public class TemporaryChatService(ILogger<TemporaryChatService> logger, Settings
|
||||
WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_180_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(180),
|
||||
WorkspaceStorageTemporaryMaintenancePolicy.DELETE_OLDER_THAN_365_DAYS => DateTime.UtcNow - lastWriteTime > TimeSpan.FromDays(365),
|
||||
|
||||
WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE => false,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
||||
@@ -207,6 +207,6 @@
|
||||
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
|
||||
}
|
||||
},
|
||||
"net8.0/osx-arm64": {}
|
||||
"net8.0/osx-x64": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# v0.9.30, build 205 (2025-02-24 19:55 UTC)
|
||||
- Implemented the new models API for the Anthropic provider. This allows you to use the latest models, like Claude 3.7 Sonnet, which were released in the last hours.
|
||||
@@ -0,0 +1,5 @@
|
||||
# v0.9.31, build 206 (2025-03-03 15:33 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.
|
||||
- Added GWDG SAIA as provider. This provider is available to all researchers and employees of the GWDG, the Max Planck Society, the 18 Helmholtz Centers, and most German universities.
|
||||
- Added DeepSeek as provider.
|
||||
- Added paths to log files in the "About" dialog for easier troubleshooting and support.
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
-----------|----------|----------|------------------------
|
||||
MWAIS0001 | Usage | Error | ProviderAccessAnalyzer
|
||||
Rule ID | Category | Severity | Notes
|
||||
-----------|----------|----------|--------------------------------
|
||||
MWAIS0001 | Usage | Error | ProviderAccessAnalyzer
|
||||
MWAIS0002 | Naming | Error | ConstStaticAnalyzer
|
||||
MWAIS0003 | Naming | Error | UnderscorePrefixAnalyzer
|
||||
MWAIS0004 | Usage | Error | RandomInstantiationAnalyzer
|
||||
MWAIS0005 | Usage | Error | ThisUsageAnalyzer
|
||||
MWAIS0006 | Style | Error | SwitchExpressionMethodAnalyzer
|
||||
MWAIS0007 | Usage | Error | EmptyStringAnalyzer
|
||||
MWAIS0008 | Naming | Error | LocalConstantsAnalyzer
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace SourceCodeRules;
|
||||
|
||||
public static class Identifier
|
||||
{
|
||||
public const string PROVIDER_ACCESS_ANALYZER = $"{Tools.ID_PREFIX}0001";
|
||||
public const string CONST_STATIC_ANALYZER = $"{Tools.ID_PREFIX}0002";
|
||||
public const string UNDERSCORE_PREFIX_ANALYZER = $"{Tools.ID_PREFIX}0003";
|
||||
public const string RANDOM_INSTANTIATION_ANALYZER = $"{Tools.ID_PREFIX}0004";
|
||||
public const string THIS_USAGE_ANALYZER = $"{Tools.ID_PREFIX}0005";
|
||||
public const string SWITCH_EXPRESSION_METHOD_ANALYZER = $"{Tools.ID_PREFIX}0006";
|
||||
public const string EMPTY_STRING_ANALYZER = $"{Tools.ID_PREFIX}0007";
|
||||
public const string LOCAL_CONSTANTS_ANALYZER = $"{Tools.ID_PREFIX}0008";
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.NamingAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class ConstStaticAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.CONST_STATIC_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Constant and static fields must be in UPPER_CASE";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "Field '{0}' must be in UPPER_CASE";
|
||||
|
||||
private static readonly string DESCRIPTION = "All constant and static fields should be named using UPPER_CASE.";
|
||||
|
||||
private const string CATEGORY = "Naming";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeField, SyntaxKind.FieldDeclaration);
|
||||
}
|
||||
|
||||
private void AnalyzeField(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var fieldDeclaration = (FieldDeclarationSyntax)context.Node;
|
||||
|
||||
// Prüfen ob das Feld static oder const ist
|
||||
if (!fieldDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword) || m.IsKind(SyntaxKind.ConstKeyword)))
|
||||
return;
|
||||
|
||||
foreach (var variable in fieldDeclaration.Declaration.Variables)
|
||||
{
|
||||
var fieldName = variable.Identifier.Text;
|
||||
|
||||
// Prüfen ob der Name bereits in UPPER_CASE ist
|
||||
if (!IsUpperCase(fieldName))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(
|
||||
RULE,
|
||||
variable.Identifier.GetLocation(),
|
||||
fieldName);
|
||||
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUpperCase(string name)
|
||||
{
|
||||
// Erlaubt: Nur Großbuchstaben, Zahlen und Unterstriche
|
||||
return name.All(c => char.IsUpper(c) || char.IsDigit(c) || c == '_');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.NamingAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class LocalConstantsAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.LOCAL_CONSTANTS_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Local constant variables must be in UPPER_CASE";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "Local constant variable '{0}' must be in UPPER_CASE";
|
||||
|
||||
private static readonly string DESCRIPTION = "All local constant variables should be named using UPPER_CASE with words separated by underscores.";
|
||||
|
||||
private const string CATEGORY = "Naming";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(
|
||||
DIAGNOSTIC_ID,
|
||||
TITLE,
|
||||
MESSAGE_FORMAT,
|
||||
CATEGORY,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeLocalDeclaration, SyntaxKind.LocalDeclarationStatement);
|
||||
}
|
||||
|
||||
private static void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;
|
||||
if (!localDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.ConstKeyword)))
|
||||
return;
|
||||
|
||||
foreach (var variable in localDeclaration.Declaration.Variables)
|
||||
{
|
||||
var variableName = variable.Identifier.Text;
|
||||
if (!IsUpperCase(variableName))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, variable.Identifier.GetLocation(), variableName);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUpperCase(string name) => name.All(c => char.IsUpper(c) || char.IsDigit(c) || c == '_') &&
|
||||
!string.IsNullOrEmpty(name) && name.Any(char.IsLetter);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.NamingAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class UnderscorePrefixAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.UNDERSCORE_PREFIX_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Variable names cannot start with underscore";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "The variable name '{0}' starts with an underscore which is not allowed";
|
||||
|
||||
private static readonly string DESCRIPTION = "Variable names cannot start with an underscore prefix.";
|
||||
|
||||
private const string CATEGORY = "Naming";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeVariableDeclaration, SyntaxKind.VariableDeclarator);
|
||||
}
|
||||
|
||||
private static void AnalyzeVariableDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var variableDeclarator = (VariableDeclaratorSyntax)context.Node;
|
||||
var variableName = variableDeclarator.Identifier.Text;
|
||||
if (variableName.StartsWith("_"))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, variableDeclarator.Identifier.GetLocation(), variableName);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Rename;
|
||||
|
||||
namespace SourceCodeRules.NamingCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ConvertToUpperCodeFixProvider)), Shared]
|
||||
public sealed class ConvertToUpperCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => [Identifier.CONST_STATIC_ANALYZER, Identifier.LOCAL_CONSTANTS_ANALYZER];
|
||||
|
||||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var declaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<VariableDeclaratorSyntax>().First();
|
||||
if (declaration is null)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(CodeAction.Create(title: "Convert to UPPER_CASE", createChangedDocument: c => this.ConvertToUpperCaseAsync(context.Document, declaration, c), equivalenceKey: nameof(ConvertToUpperCodeFixProvider)), diagnostic);
|
||||
}
|
||||
|
||||
private async Task<Document> ConvertToUpperCaseAsync(Document document, VariableDeclaratorSyntax declarator, CancellationToken cancellationToken)
|
||||
{
|
||||
var oldName = declarator.Identifier.Text;
|
||||
var newName = ConvertToUpperCase(oldName);
|
||||
|
||||
var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
|
||||
var symbol = semanticModel?.GetDeclaredSymbol(declarator, cancellationToken);
|
||||
if (symbol is null)
|
||||
return document;
|
||||
|
||||
var solution = document.Project.Solution;
|
||||
var newSolution = await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), newName, cancellationToken);
|
||||
|
||||
return newSolution.GetDocument(document.Id) ?? document;
|
||||
}
|
||||
|
||||
private static string ConvertToUpperCase(string name)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
var current = name[i];
|
||||
|
||||
// Insert an underscore before each uppercase letter, except the first one:
|
||||
if (i > 0 && char.IsUpper(current) && !char.IsUpper(name[i - 1]))
|
||||
result.Append('_');
|
||||
|
||||
result.Append(char.ToUpper(current));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Rename;
|
||||
|
||||
namespace SourceCodeRules.NamingCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UnderscorePrefixCodeFixProvider)), Shared]
|
||||
public sealed class UnderscorePrefixCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
public override ImmutableArray<string> FixableDiagnosticIds => [Identifier.UNDERSCORE_PREFIX_ANALYZER];
|
||||
|
||||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var declaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<VariableDeclaratorSyntax>().First();
|
||||
if (declaration is null)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: "Remove underscore prefix",
|
||||
createChangedDocument: c => this.RemoveUnderscorePrefixAsync(context.Document, declaration, c),
|
||||
equivalenceKey: nameof(UnderscorePrefixCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
|
||||
private async Task<Document> RemoveUnderscorePrefixAsync(Document document, VariableDeclaratorSyntax declarator, CancellationToken cancellationToken)
|
||||
{
|
||||
var oldName = declarator.Identifier.Text;
|
||||
var newName = oldName.TrimStart('_');
|
||||
|
||||
var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
|
||||
var symbol = semanticModel?.GetDeclaredSymbol(declarator, cancellationToken);
|
||||
if (symbol is null)
|
||||
return document;
|
||||
|
||||
var solution = document.Project.Solution;
|
||||
var newSolution = await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), newName, cancellationToken);
|
||||
|
||||
return newSolution.GetDocument(document.Id) ?? document;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.StyleAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public class SwitchExpressionMethodAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.SWITCH_EXPRESSION_METHOD_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Method with switch expression should use inline expression body";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "Method with a switch expression should use inline expression body syntax with the switch keyword on the same line";
|
||||
|
||||
private static readonly string DESCRIPTION = "Methods that only return a switch expression should use the expression body syntax (=>) with the switch keyword on the same line for better readability.";
|
||||
|
||||
private const string CATEGORY = "Style";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(
|
||||
DIAGNOSTIC_ID,
|
||||
TITLE,
|
||||
MESSAGE_FORMAT,
|
||||
CATEGORY,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
|
||||
}
|
||||
|
||||
private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// Fall 1: Methode hat Block-Body mit einem Return-Statement, das eine Switch-Expression ist
|
||||
if (methodDeclaration is { Body: not null, ExpressionBody: null })
|
||||
{
|
||||
var statements = methodDeclaration.Body.Statements;
|
||||
if (statements.Count == 1 && statements[0] is ReturnStatementSyntax { Expression: SwitchExpressionSyntax })
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, methodDeclaration.Identifier.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall 2: Methode hat Expression-Body, aber die Switch-Expression beginnt auf einer neuen Zeile
|
||||
var expressionBody = methodDeclaration.ExpressionBody;
|
||||
if (expressionBody?.Expression is SwitchExpressionSyntax switchExpr)
|
||||
{
|
||||
var arrowToken = expressionBody.ArrowToken;
|
||||
var switchToken = switchExpr.SwitchKeyword;
|
||||
bool hasNewLineBetweenArrowAndSwitch = false;
|
||||
|
||||
foreach (var trivia in arrowToken.TrailingTrivia)
|
||||
{
|
||||
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
|
||||
{
|
||||
hasNewLineBetweenArrowAndSwitch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe Leading Trivia des Switch-Keywords, falls notwendig
|
||||
if (!hasNewLineBetweenArrowAndSwitch)
|
||||
{
|
||||
foreach (var trivia in switchToken.LeadingTrivia)
|
||||
{
|
||||
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
|
||||
{
|
||||
hasNewLineBetweenArrowAndSwitch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewLineBetweenArrowAndSwitch)
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, methodDeclaration.Identifier.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace SourceCodeRules.StyleCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SwitchExpressionMethodCodeFixProvider)), Shared]
|
||||
public class SwitchExpressionMethodCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => [Identifier.SWITCH_EXPRESSION_METHOD_ANALYZER];
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
if (root == null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var methodDeclaration = root.FindToken(diagnosticSpan.Start)
|
||||
.Parent?.AncestorsAndSelf()
|
||||
.OfType<MethodDeclarationSyntax>()
|
||||
.First();
|
||||
|
||||
if(methodDeclaration == null)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: "Use inline expression body for switch expression",
|
||||
createChangedDocument: c => UseInlineExpressionBodyAsync(context.Document, methodDeclaration, c),
|
||||
equivalenceKey: nameof(SwitchExpressionMethodCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
|
||||
private static async Task<Document> UseInlineExpressionBodyAsync(Document document, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken)
|
||||
{
|
||||
var sourceText = await document.GetTextAsync(cancellationToken);
|
||||
var parameterText = methodDecl.ParameterList.ToString();
|
||||
var methodStartLine = sourceText.Lines.GetLineFromPosition(methodDecl.SpanStart);
|
||||
|
||||
SwitchExpressionSyntax? switchExpr = null;
|
||||
ExpressionSyntax? governingExpression = null;
|
||||
var switchBodyText = string.Empty;
|
||||
|
||||
if (methodDecl.Body != null)
|
||||
{
|
||||
// Case: Block-Body with a Return-Statement that contains a Switch-Expression
|
||||
var returnStmt = (ReturnStatementSyntax)methodDecl.Body.Statements[0];
|
||||
if (returnStmt.Expression is not SwitchExpressionSyntax matchingSwitchExpr)
|
||||
return document;
|
||||
|
||||
switchExpr = matchingSwitchExpr;
|
||||
governingExpression = switchExpr.GoverningExpression;
|
||||
|
||||
// Extract the switch body text:
|
||||
var switchStart = switchExpr.SwitchKeyword.SpanStart;
|
||||
var switchEnd = switchExpr.CloseBraceToken.Span.End;
|
||||
switchBodyText = sourceText.ToString(TextSpan.FromBounds(switchStart, switchEnd));
|
||||
}
|
||||
else if (methodDecl.ExpressionBody != null)
|
||||
{
|
||||
// Case 2: Expression-Body with a poorly formatted Switch-Expression
|
||||
switchExpr = (SwitchExpressionSyntax)methodDecl.ExpressionBody.Expression;
|
||||
governingExpression = switchExpr.GoverningExpression;
|
||||
|
||||
// Extract the switch body text:
|
||||
var switchStart = switchExpr.SwitchKeyword.SpanStart;
|
||||
var switchEnd = switchExpr.CloseBraceToken.Span.End;
|
||||
switchBodyText = sourceText.ToString(TextSpan.FromBounds(switchStart, switchEnd));
|
||||
}
|
||||
|
||||
if (switchExpr is null || governingExpression is null)
|
||||
return document;
|
||||
|
||||
// Extract the governing expression and the switch body:
|
||||
var govExprText = sourceText.ToString(governingExpression.Span);
|
||||
|
||||
// Create the new method with inline expression body and correct formatting:
|
||||
var returnTypeText = methodDecl.ReturnType.ToString();
|
||||
var modifiersText = string.Join(" ", methodDecl.Modifiers);
|
||||
var methodNameText = methodDecl.Identifier.Text;
|
||||
|
||||
// Determine the indentation of the method:
|
||||
var methodIndentation = "";
|
||||
for (var i = methodStartLine.Start; i < methodDecl.SpanStart; i++)
|
||||
{
|
||||
if (char.IsWhiteSpace(sourceText[i]))
|
||||
methodIndentation += sourceText[i];
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Erstelle die neue Methode mit Expression-Body und korrekter Formatierung
|
||||
var newMethodText = new StringBuilder();
|
||||
newMethodText.Append($"{modifiersText} {returnTypeText} {methodNameText}{parameterText} => {govExprText} switch");
|
||||
|
||||
// Formatiere die geschweiften Klammern und den Switch-Body
|
||||
var switchBody = switchBodyText.Substring("switch".Length).Trim();
|
||||
|
||||
// Bestimme die Einrückung für die Switch-Cases (4 Spaces oder 1 Tab mehr als die Methode)
|
||||
var caseIndentation = methodIndentation + " "; // 4 Spaces Einrückung
|
||||
|
||||
// Verarbeite die Klammern und formatiere den Body
|
||||
var formattedSwitchBody = FormatSwitchBody(switchBody, methodIndentation, caseIndentation);
|
||||
newMethodText.Append(formattedSwitchBody);
|
||||
|
||||
// Ersetze die alte Methoden-Deklaration mit dem neuen Text
|
||||
var newText = sourceText.Replace(methodDecl.Span, newMethodText.ToString());
|
||||
return document.WithText(newText);
|
||||
}
|
||||
|
||||
private static string FormatSwitchBody(string switchBody, string methodIndentation, string caseIndentation)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
|
||||
// Remove braces from the switch body:
|
||||
var bodyWithoutBraces = switchBody.Trim();
|
||||
if (bodyWithoutBraces.StartsWith("{"))
|
||||
bodyWithoutBraces = bodyWithoutBraces.Substring(1);
|
||||
if (bodyWithoutBraces.EndsWith("}"))
|
||||
bodyWithoutBraces = bodyWithoutBraces.Substring(0, bodyWithoutBraces.Length - 1);
|
||||
|
||||
bodyWithoutBraces = bodyWithoutBraces.Trim();
|
||||
|
||||
// Add braces with correct indentation:
|
||||
result.AppendLine();
|
||||
result.Append($"{methodIndentation}{{");
|
||||
|
||||
// Process each line of the switch body:
|
||||
var lines = bodyWithoutBraces.Split(["\r\n", "\n"], System.StringSplitOptions.None);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
result.AppendLine();
|
||||
|
||||
var trimmedLine = line.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmedLine))
|
||||
continue;
|
||||
|
||||
// Add correct indentation for each case:
|
||||
result.Append(caseIndentation);
|
||||
result.Append(trimmedLine);
|
||||
}
|
||||
|
||||
// Add the closing brace with correct indentation:
|
||||
result.AppendLine();
|
||||
result.Append($"{methodIndentation}}};");
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class EmptyStringAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.EMPTY_STRING_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = """
|
||||
Use string.Empty instead of ""
|
||||
""";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = """
|
||||
Use string.Empty instead of ""
|
||||
""";
|
||||
|
||||
private static readonly string DESCRIPTION = """Empty string literals ("") should be replaced with string.Empty for better code consistency and readability except in const contexts.""";
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeEmptyStringLiteral, SyntaxKind.StringLiteralExpression);
|
||||
}
|
||||
|
||||
private static void AnalyzeEmptyStringLiteral(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var stringLiteral = (LiteralExpressionSyntax)context.Node;
|
||||
if (stringLiteral.Token.ValueText != string.Empty)
|
||||
return;
|
||||
|
||||
if (IsInConstContext(stringLiteral))
|
||||
return;
|
||||
|
||||
var diagnostic = Diagnostic.Create(RULE, stringLiteral.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsInConstContext(LiteralExpressionSyntax stringLiteral)
|
||||
{
|
||||
var variableDeclarator = stringLiteral.FirstAncestorOrSelf<VariableDeclaratorSyntax>();
|
||||
if (variableDeclarator is null)
|
||||
return false;
|
||||
|
||||
var declaration = variableDeclarator.Parent?.Parent;
|
||||
return declaration switch
|
||||
{
|
||||
FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword),
|
||||
LocalDeclarationStatementSyntax localDeclaration => localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword),
|
||||
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -6,14 +6,14 @@ using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules;
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
public sealed class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = $"{Tools.ID_PREFIX}0001";
|
||||
private const string DIAGNOSTIC_ID = Identifier.PROVIDER_ACCESS_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Direct access to `Providers` is not allowed";
|
||||
|
||||
@@ -25,7 +25,7 @@ public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(RULE);
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
@@ -37,15 +37,15 @@ public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
|
||||
|
||||
// Prüfen, ob wir eine Kette von Zugriffen haben, die auf "Providers" endet
|
||||
|
||||
// Check if the member access is not on the `Providers` property:
|
||||
if (memberAccess.Name.Identifier.Text != "Providers")
|
||||
return;
|
||||
|
||||
// Den kompletten Zugriffspfad aufbauen
|
||||
|
||||
// Get the full path of the member access:
|
||||
var fullPath = this.GetFullMemberAccessPath(memberAccess);
|
||||
|
||||
// Prüfen, ob der Pfad unserem verbotenen Muster entspricht
|
||||
// Check for the forbidden pattern:
|
||||
if (fullPath.EndsWith("ConfigurationData.Providers"))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, memberAccess.GetLocation());
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public class RandomInstantiationAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.RANDOM_INSTANTIATION_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "Direct instantiation of Random is not allowed";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "Do not use 'new Random()'. Instead, inject and use the ThreadSafeRandom service from the DI container.";
|
||||
|
||||
private static readonly string DESCRIPTION = "Using 'new Random()' can lead to issues in multi-threaded scenarios. Use the ThreadSafeRandom service instead.";
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(
|
||||
DIAGNOSTIC_ID,
|
||||
TITLE,
|
||||
MESSAGE_FORMAT,
|
||||
CATEGORY,
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression);
|
||||
}
|
||||
|
||||
private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
|
||||
if (context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol is not ITypeSymbol typeSymbol)
|
||||
return;
|
||||
|
||||
if (typeSymbol.ToString() == "System.Random" || typeSymbol is { Name: "Random", ContainingNamespace.Name: "System" })
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, objectCreation.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class ThisUsageAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.THIS_USAGE_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "`this.` must be used";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "`this.` must be used to access variables, methods, and properties";
|
||||
|
||||
private static readonly string DESCRIPTION = MESSAGE_FORMAT;
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeIdentifier, SyntaxKind.IdentifierName);
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeGenericName, SyntaxKind.GenericName);
|
||||
}
|
||||
|
||||
private void AnalyzeGenericName(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var genericNameSyntax = (GenericNameSyntax)context.Node;
|
||||
|
||||
// Skip if already part of a 'this' expression
|
||||
if (IsAccessedThroughThis(genericNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsWithinInitializer(genericNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsPartOfMemberAccess(genericNameSyntax))
|
||||
return;
|
||||
|
||||
// Get symbol info for the generic name
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(genericNameSyntax);
|
||||
var symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
return;
|
||||
|
||||
// Skip static methods
|
||||
if (symbol.IsStatic)
|
||||
return;
|
||||
|
||||
// Skip local functions
|
||||
if (symbol is IMethodSymbol methodSymbol && IsLocalFunction(methodSymbol))
|
||||
return;
|
||||
|
||||
// Get the containing type of the current context
|
||||
var containingSymbol = context.ContainingSymbol;
|
||||
var currentType = containingSymbol?.ContainingType;
|
||||
|
||||
// If we're in a static context, allow accessing members without this
|
||||
if (IsInStaticContext(containingSymbol))
|
||||
return;
|
||||
|
||||
if (symbol is IMethodSymbol)
|
||||
{
|
||||
var containingType = symbol.ContainingType;
|
||||
|
||||
// If the symbol is a member of the current type or a base type, then require this
|
||||
if (currentType != null && (SymbolEqualityComparer.Default.Equals(containingType, currentType) ||
|
||||
IsBaseTypeOf(containingType, currentType)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, genericNameSyntax.Identifier.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var identifierNameSyntax = (IdentifierNameSyntax)context.Node;
|
||||
|
||||
// Skip if this identifier is part of a generic name - we'll handle that separately
|
||||
if (identifierNameSyntax.Parent is GenericNameSyntax)
|
||||
return;
|
||||
|
||||
// Skip if already part of a 'this' expression
|
||||
if (IsAccessedThroughThis(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsWithinInitializer(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsPartOfMemberAccess(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Also skip if it's part of static import statements
|
||||
if (IsPartOfUsingStaticDirective(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Skip if it's part of a namespace or type name
|
||||
if (IsPartOfNamespaceOrTypeName(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Get symbol info
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(identifierNameSyntax);
|
||||
var symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
return;
|
||||
|
||||
// Skip local variables, parameters, and range variables
|
||||
if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter or SymbolKind.RangeVariable or SymbolKind.TypeParameter)
|
||||
return;
|
||||
|
||||
// Skip types and namespaces
|
||||
if (symbol.Kind is SymbolKind.NamedType or SymbolKind.Namespace)
|
||||
return;
|
||||
|
||||
// Explicitly check if this is a local function
|
||||
if (symbol is IMethodSymbol methodSymbol && IsLocalFunction(methodSymbol))
|
||||
return;
|
||||
|
||||
// Get the containing type of the current context
|
||||
var containingSymbol = context.ContainingSymbol;
|
||||
var currentType = containingSymbol?.ContainingType;
|
||||
|
||||
// If we're in a static context, allow accessing members without this
|
||||
if (IsInStaticContext(containingSymbol))
|
||||
return;
|
||||
|
||||
// Now check if the symbol is an instance member of the current class
|
||||
if (symbol is IFieldSymbol or IPropertySymbol or IMethodSymbol or IEventSymbol)
|
||||
{
|
||||
// Skip static members
|
||||
if (symbol.IsStatic)
|
||||
return;
|
||||
|
||||
// Skip constants
|
||||
if (symbol is IFieldSymbol { IsConst: true })
|
||||
return;
|
||||
|
||||
var containingType = symbol.ContainingType;
|
||||
|
||||
// If the symbol is a member of the current type or a base type, then require this
|
||||
if (currentType != null && (SymbolEqualityComparer.Default.Equals(containingType, currentType) ||
|
||||
IsBaseTypeOf(containingType, currentType)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, identifierNameSyntax.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLocalFunction(IMethodSymbol methodSymbol) => methodSymbol.MethodKind is MethodKind.LocalFunction;
|
||||
|
||||
private static bool IsBaseTypeOf(INamedTypeSymbol baseType, INamedTypeSymbol derivedType)
|
||||
{
|
||||
var currentType = derivedType.BaseType;
|
||||
while (currentType != null)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(currentType, baseType))
|
||||
return true;
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsInStaticContext(ISymbol? containingSymbol) => containingSymbol?.IsStatic is true;
|
||||
|
||||
private static bool IsAccessedThroughThis(SyntaxNode node)
|
||||
{
|
||||
if (node.Parent is MemberAccessExpressionSyntax memberAccess)
|
||||
if (memberAccess.Expression is ThisExpressionSyntax && memberAccess.Name == node)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWithinInitializer(SyntaxNode node)
|
||||
{
|
||||
for (var current = node.Parent; current != null; current = current.Parent)
|
||||
if (current is InitializerExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfMemberAccess(SyntaxNode node)
|
||||
{
|
||||
// Check if the node is part of a member access expression where the expression is not 'this':
|
||||
if (node.Parent is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
// If the member access expression is 'this', it's allowed:
|
||||
if (memberAccess.Expression is ThisExpressionSyntax)
|
||||
return false;
|
||||
|
||||
// If the member access expression is something else (e.g., instance.Member), skip:
|
||||
if (memberAccess.Name == node)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check for conditional access expressions (e.g., instance?.Member):
|
||||
if (node.Parent is ConditionalAccessExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfUsingStaticDirective(SyntaxNode node)
|
||||
{
|
||||
for (var current = node.Parent; current != null; current = current.Parent)
|
||||
if (current is UsingDirectiveSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfNamespaceOrTypeName(SyntaxNode node)
|
||||
{
|
||||
// Check if a node is part of a namespace, class, or type declaration:
|
||||
if (node.Parent is NameSyntax && node.Parent is not MemberAccessExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
|
||||
namespace SourceCodeRules.UsageCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EmptyStringCodeFixProvider)), Shared]
|
||||
public class EmptyStringCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private const string TITLE = """Replace "" with string.Empty""";
|
||||
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => [Identifier.EMPTY_STRING_ANALYZER];
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
if(root is null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
|
||||
if (root.FindToken(diagnosticSpan.Start).Parent is not LiteralExpressionSyntax emptyStringLiteral)
|
||||
return;
|
||||
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => ReplaceWithStringEmpty(context.Document, emptyStringLiteral, c),
|
||||
equivalenceKey: TITLE),
|
||||
diagnostic);
|
||||
}
|
||||
private static async Task<Document> ReplaceWithStringEmpty(Document document, LiteralExpressionSyntax emptyStringLiteral, CancellationToken cancellationToken)
|
||||
{
|
||||
var stringEmptyExpression = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("string"), SyntaxFactory.IdentifierName("Empty")).WithAdditionalAnnotations(Formatter.Annotation);
|
||||
var root = await document.GetSyntaxRootAsync(cancellationToken);
|
||||
if (root is null)
|
||||
return document;
|
||||
|
||||
var newRoot = root.ReplaceNode(emptyStringLiteral, stringEmptyExpression);
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace SourceCodeRules.UsageCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ThisUsageCodeFixProvider)), Shared]
|
||||
public class ThisUsageCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private const string TITLE = "Add 'this.' prefix";
|
||||
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => [Identifier.THIS_USAGE_ANALYZER];
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
if (root == null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var node = root.FindNode(diagnosticSpan);
|
||||
|
||||
if (node is IdentifierNameSyntax identifierNode)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => AddThisPrefixAsync(context.Document, identifierNode, c),
|
||||
equivalenceKey: nameof(ThisUsageCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
else if (node is GenericNameSyntax genericNameNode)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => AddThisPrefixAsync(context.Document, genericNameNode, c),
|
||||
equivalenceKey: nameof(ThisUsageCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Document> AddThisPrefixAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
|
||||
{
|
||||
var root = await document.GetSyntaxRootAsync(cancellationToken);
|
||||
if (root == null)
|
||||
return document;
|
||||
|
||||
var thisExpression = SyntaxFactory.ThisExpression();
|
||||
var leadingTrivia = node.GetLeadingTrivia();
|
||||
var memberAccessExpression = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
thisExpression.WithLeadingTrivia(leadingTrivia),
|
||||
((SimpleNameSyntax)node).WithLeadingTrivia(SyntaxTriviaList.Empty))
|
||||
.WithTrailingTrivia(node.GetTrailingTrivia());
|
||||
|
||||
var newRoot = root.ReplaceNode(node, memberAccessExpression);
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
0.9.29
|
||||
2025-02-24 13:48:37 UTC
|
||||
204
|
||||
0.9.31
|
||||
2025-03-03 15:33:03 UTC
|
||||
206
|
||||
8.0.113 (commit 8f216348dc)
|
||||
8.0.13 (commit eba546b0f0)
|
||||
1.85.0 (commit 4d91de4e4)
|
||||
7.16.0
|
||||
1.8.1
|
||||
03431b85b48, release
|
||||
2e84f10a977, release
|
||||
Generated
+1
-1
@@ -2347,7 +2347,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.29"
|
||||
version = "0.9.31"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.29"
|
||||
version = "0.9.31"
|
||||
edition = "2021"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
|
||||
@@ -30,14 +30,14 @@ pub fn start_tauri() {
|
||||
*MAIN_WINDOW.lock().unwrap() = Some(window);
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Setup is running.");
|
||||
let logger_path = app.path_resolver().app_local_data_dir().unwrap();
|
||||
let logger_path = logger_path.join("data");
|
||||
let data_path = app.path_resolver().app_local_data_dir().unwrap();
|
||||
let data_path = data_path.join("data");
|
||||
|
||||
DATA_DIRECTORY.set(logger_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not abe to set the data directory.")).unwrap();
|
||||
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not abe to set the data directory.")).unwrap();
|
||||
CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {logger_path:?}");
|
||||
switch_to_file_logging(logger_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
|
||||
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
|
||||
switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
+88
-12
@@ -1,16 +1,25 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::env::{current_dir, temp_dir};
|
||||
use std::error::Error;
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{absolute, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger, LoggerHandle};
|
||||
use flexi_logger::writers::FileLogWriter;
|
||||
use log::kv;
|
||||
use log::kv::{Key, Value, VisitSource};
|
||||
use rocket::get;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::Serialize;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::environment::is_dev;
|
||||
|
||||
static LOGGER: OnceLock<RuntimeLoggerHandle> = OnceLock::new();
|
||||
|
||||
static LOG_STARTUP_PATH: OnceLock<String> = OnceLock::new();
|
||||
|
||||
static LOG_APP_PATH: OnceLock<String> = OnceLock::new();
|
||||
|
||||
/// Initialize the logging system.
|
||||
pub fn init_logging() {
|
||||
|
||||
@@ -42,11 +51,17 @@ pub fn init_logging() {
|
||||
false => "AI Studio Events",
|
||||
};
|
||||
|
||||
let log_path = FileSpec::default()
|
||||
.directory(get_startup_log_path())
|
||||
.basename(log_basename)
|
||||
.suppress_timestamp()
|
||||
.suffix("log");
|
||||
|
||||
// Store the startup log path:
|
||||
let _ = LOG_STARTUP_PATH.set(convert_log_path_to_string(&log_path));
|
||||
|
||||
let runtime_logger = Logger::try_with_str(log_config).expect("Cannot create logging")
|
||||
.log_to_file(FileSpec::default()
|
||||
.basename(log_basename)
|
||||
.suppress_timestamp()
|
||||
.suffix("log"))
|
||||
.log_to_file(log_path)
|
||||
.duplicate_to_stdout(Duplicate::All)
|
||||
.use_utc()
|
||||
.format_for_files(file_logger_format)
|
||||
@@ -61,14 +76,60 @@ pub fn init_logging() {
|
||||
LOGGER.set(runtime_logger).expect("Cannot set LOGGER");
|
||||
}
|
||||
|
||||
/// Switch the logging system to a file-based output.
|
||||
fn convert_log_path_to_string(log_path: &FileSpec) -> String {
|
||||
let log_path = log_path.as_pathbuf(None);
|
||||
|
||||
// Case: The path is already absolute:
|
||||
if log_path.is_absolute() {
|
||||
return log_path.to_str().unwrap().to_string();
|
||||
}
|
||||
|
||||
// Case: The path is relative. Let's try to convert it to an absolute path:
|
||||
match log_path.canonicalize() {
|
||||
// Case: The path exists:
|
||||
Ok(log_path) => log_path.to_str().unwrap().to_string(),
|
||||
|
||||
// Case: The path does not exist. Let's try to build the
|
||||
// absolute path without touching the file system:
|
||||
Err(_) => match absolute(log_path.clone()) {
|
||||
|
||||
// Case: We could build the absolute path:
|
||||
Ok(log_path) => log_path.to_str().unwrap().to_string(),
|
||||
|
||||
// Case: We could not reconstruct the path using the working directory.
|
||||
Err(_) => log_path.to_str().unwrap().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Rust plans to remove the deprecation flag for std::env::home_dir() in Rust 1.86.0.
|
||||
#[allow(deprecated)]
|
||||
fn get_startup_log_path() -> String {
|
||||
match std::env::home_dir() {
|
||||
// Case: We could determine the home directory:
|
||||
Some(home_dir) => home_dir.to_str().unwrap().to_string(),
|
||||
|
||||
// Case: We could not determine the home directory. Let's try to use the working directory:
|
||||
None => match current_dir() {
|
||||
|
||||
// Case: We could determine the working directory:
|
||||
Ok(working_directory) => working_directory.to_str().unwrap().to_string(),
|
||||
|
||||
// Case: We could not determine the working directory. Let's use the temporary directory:
|
||||
Err(_) => temp_dir().to_str().unwrap().to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch the logging system to a file-based output inside the given directory.
|
||||
pub fn switch_to_file_logging(logger_path: PathBuf) -> Result<(), Box<dyn Error>>{
|
||||
LOGGER.get().expect("No LOGGER was set").handle.reset_flw(&FileLogWriter::builder(
|
||||
FileSpec::default()
|
||||
.directory(logger_path)
|
||||
.basename("events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log")))?;
|
||||
let log_path = FileSpec::default()
|
||||
.directory(logger_path)
|
||||
.basename("events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log");
|
||||
let _ = LOG_APP_PATH.set(convert_log_path_to_string(&log_path));
|
||||
LOGGER.get().expect("No LOGGER was set").handle.reset_flw(&FileLogWriter::builder(log_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -160,4 +221,19 @@ fn file_logger_format(
|
||||
|
||||
// Write the log message:
|
||||
write!(w, "{}", &record.args())
|
||||
}
|
||||
|
||||
#[get("/log/paths")]
|
||||
pub async fn get_log_paths(_token: APIToken) -> Json<LogPathsResponse> {
|
||||
Json(LogPathsResponse {
|
||||
log_startup_path: LOG_STARTUP_PATH.get().expect("No startup log path was set").clone(),
|
||||
log_app_path: LOG_APP_PATH.get().expect("No app log path was set").clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The response the get log paths request.
|
||||
#[derive(Serialize)]
|
||||
pub struct LogPathsResponse {
|
||||
log_startup_path: String,
|
||||
log_app_path: String,
|
||||
}
|
||||
+35
-17
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashSet;
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::config::Shutdown;
|
||||
@@ -25,22 +24,9 @@ pub static API_SERVER_PORT: Lazy<u16> = Lazy::new(|| {
|
||||
pub fn start_runtime_api() {
|
||||
let api_port = *API_SERVER_PORT;
|
||||
info!("Try to start the API server on 'http://localhost:{api_port}'...");
|
||||
|
||||
// The shutdown configuration for the runtime API server:
|
||||
let mut shutdown = Shutdown {
|
||||
// We do not want to use the Ctrl+C signal to stop the server:
|
||||
ctrlc: false,
|
||||
|
||||
// Everything else is set to default for now:
|
||||
..Shutdown::default()
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// We do not want to use the termination signal to stop the server.
|
||||
// This option, however, is only available on Unix systems:
|
||||
shutdown.signals = HashSet::new();
|
||||
}
|
||||
|
||||
// Get the shutdown configuration:
|
||||
let shutdown = create_shutdown();
|
||||
|
||||
// Configure the runtime API server:
|
||||
let figment = Figment::from(rocket::Config::release_default())
|
||||
@@ -91,8 +77,40 @@ pub fn start_runtime_api() {
|
||||
crate::secret::delete_secret,
|
||||
crate::environment::get_data_directory,
|
||||
crate::environment::get_config_directory,
|
||||
crate::log::get_log_paths,
|
||||
])
|
||||
.ignite().await.unwrap()
|
||||
.launch().await.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn create_shutdown() -> Shutdown {
|
||||
//
|
||||
// Create a shutdown configuration, depending on the operating system:
|
||||
//
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::collections::HashSet;
|
||||
let mut shutdown = Shutdown {
|
||||
// We do not want to use the Ctrl+C signal to stop the server:
|
||||
ctrlc: false,
|
||||
|
||||
// Everything else is set to default for now:
|
||||
..Shutdown::default()
|
||||
};
|
||||
|
||||
shutdown.signals = HashSet::new();
|
||||
shutdown
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Shutdown {
|
||||
// We do not want to use the Ctrl+C signal to stop the server:
|
||||
ctrlc: false,
|
||||
|
||||
// Everything else is set to default for now:
|
||||
..Shutdown::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "MindWork AI Studio",
|
||||
"version": "0.9.29"
|
||||
"version": "0.9.31"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
Reference in new issue
Block a user