mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-20 20:38:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eccf65f0a6 | ||
|
|
2429788ccc | ||
|
|
0125f48f61 | ||
|
|
94028b2c06 | ||
|
|
c5acce7278 | ||
|
|
4eb51f947a | ||
|
|
e11211d215 | ||
|
|
032c7ee56e | ||
|
|
030990ee90 | ||
|
|
371731ac13 | ||
|
|
99622cec48 | ||
|
|
6a4a7dc0d6 | ||
|
|
df01ce188e | ||
|
|
3bfca45e11 | ||
|
|
ccaf2e894a | ||
|
|
1d22232005 | ||
|
|
a66d88c989 | ||
|
|
2e84f10a97 | ||
|
|
dbc2b4f6bc | ||
|
|
9d2b63bbaa | ||
|
|
277dc73468 | ||
|
|
ccac2b04ac | ||
|
|
0c4b94c527 | ||
|
|
37a9046be4 | ||
|
|
b984b4432e | ||
|
|
5e3b632eee | ||
|
|
99dac520cb | ||
|
|
e21e30ce8c |
No files matched your search
+4
-4
@@ -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
|
||||
|
||||
# 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>
|
||||
@@ -199,7 +199,7 @@ public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection>
|
||||
// Call the ERI server to get the server description:
|
||||
//
|
||||
using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!;
|
||||
var authResponse = await eriClient.AuthenticateAsync(eriDataSource, rustService, token);
|
||||
var authResponse = await eriClient.AuthenticateAsync(rustService, cancellationToken: token);
|
||||
if (authResponse.Successful)
|
||||
{
|
||||
var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token);
|
||||
|
||||
@@ -293,7 +293,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
|
||||
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
@@ -2,9 +2,7 @@ namespace AIStudio.Assistants.ERI;
|
||||
|
||||
public static class AllowedLLMProvidersExtensions
|
||||
{
|
||||
public static string Description(this AllowedLLMProviders provider)
|
||||
{
|
||||
return provider switch
|
||||
public static string Description(this AllowedLLMProviders provider) => 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",
|
||||
@@ -13,4 +11,3 @@ public static class AllowedLLMProvidersExtensions
|
||||
_ => "Unknown option was selected"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -290,6 +290,7 @@ public partial class AssistantERI : AssistantBaseCore
|
||||
- You consider the security of the implementation by applying the Security by Design principle.
|
||||
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
|
||||
create a separate code block with its file path and name as chapter title.
|
||||
- Important: The JSON objects of the API messages use camel case for the data field names.
|
||||
""");
|
||||
|
||||
return sb.ToString();
|
||||
|
||||
@@ -12,7 +12,6 @@ public enum ChatRole
|
||||
USER,
|
||||
AI,
|
||||
AGENT,
|
||||
RAG,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -40,6 +40,16 @@ public sealed record ChatThread
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The augmented data for this chat thread. Will be inserted into the system prompt.
|
||||
/// </summary>
|
||||
public string AugmentedData { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The data security to use, derived from the data sources used so far.
|
||||
/// </summary>
|
||||
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
@@ -74,31 +84,48 @@ public sealed record ChatThread
|
||||
/// <returns>The prepared system prompt.</returns>
|
||||
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
|
||||
{
|
||||
var isAugmentedDataAvailable = !string.IsNullOrWhiteSpace(chatThread.AugmentedData);
|
||||
var systemPromptWithAugmentedData = isAugmentedDataAvailable switch
|
||||
{
|
||||
true => $"""
|
||||
{chatThread.SystemPrompt}
|
||||
|
||||
{chatThread.AugmentedData}
|
||||
""",
|
||||
|
||||
false => chatThread.SystemPrompt,
|
||||
};
|
||||
|
||||
if(isAugmentedDataAvailable)
|
||||
logger.LogInformation("Augmented data is available for the chat thread.");
|
||||
else
|
||||
logger.LogInformation("No augmented data is available for the chat thread.");
|
||||
|
||||
//
|
||||
// Prepare the system prompt:
|
||||
//
|
||||
string systemPromptText;
|
||||
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
|
||||
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
systemPromptText = systemPromptWithAugmentedData;
|
||||
else
|
||||
{
|
||||
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
systemPromptText = systemPromptWithAugmentedData;
|
||||
else
|
||||
{
|
||||
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
systemPromptText = systemPromptWithAugmentedData;
|
||||
else
|
||||
{
|
||||
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
|
||||
if(profile == default)
|
||||
systemPromptText = chatThread.SystemPrompt;
|
||||
systemPromptText = systemPromptWithAugmentedData;
|
||||
else
|
||||
{
|
||||
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
|
||||
systemPromptText = $"""
|
||||
{chatThread.SystemPrompt}
|
||||
{systemPromptWithAugmentedData}
|
||||
|
||||
{profile.ToSystemPrompt()}
|
||||
""";
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
public static class ChatThreadExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the specified provider is allowed for the chat thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We don't check if the provider is allowed to use the data sources of the chat thread.
|
||||
/// That kind of check is done in the RAG process itself.<br/><br/>
|
||||
///
|
||||
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
|
||||
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
|
||||
/// from the data sources is stored in the chat thread. This means we must check if the
|
||||
/// selected provider is allowed to use this thread's data.
|
||||
/// </remarks>
|
||||
/// <param name="chatThread">The chat thread to check.</param>
|
||||
/// <param name="provider">The provider to check.</param>
|
||||
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
|
||||
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider)
|
||||
{
|
||||
// No chat thread available means we have a new chat. That's fine:
|
||||
if (chatThread is null)
|
||||
return true;
|
||||
|
||||
// The chat thread is available, but the data security is not specified.
|
||||
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
|
||||
// That's fine as well:
|
||||
if (chatThread.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
|
||||
return true;
|
||||
|
||||
//
|
||||
// Is the provider self-hosted?
|
||||
//
|
||||
var isSelfHostedProvider = provider switch
|
||||
{
|
||||
ProviderSelfHosted => true,
|
||||
AIStudio.Settings.Provider p => p.IsSelfHosted,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
//
|
||||
// Check the chat data security against the selected provider:
|
||||
//
|
||||
return isSelfHostedProvider switch
|
||||
{
|
||||
// The provider is self-hosted -- we can use any data source:
|
||||
true => true,
|
||||
|
||||
// The provider is not self-hosted -- it depends on the data security of the chat thread:
|
||||
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="Regenerate" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" OnClick="@this.RegenerateBlock"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.RemoveBlockFunc is not null)
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ContentImage : IContent, IImageSource
|
||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
|
||||
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -36,17 +36,32 @@ public sealed class ContentText : IContent
|
||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||
{
|
||||
if(chatThread is null)
|
||||
return;
|
||||
return new();
|
||||
|
||||
if(!chatThread.IsLLMProviderAllowed(provider))
|
||||
{
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||
logger.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
// Call the RAG process. Right now, we only have one RAG process:
|
||||
if (lastPrompt is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rag = new AISrcSelWithRetCtxVal();
|
||||
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
|
||||
logger.LogError(e, "Skipping the RAG process due to an error.");
|
||||
}
|
||||
}
|
||||
|
||||
// Store the last time we got a response. We use this later
|
||||
// to determine whether we should notify the UI about the
|
||||
@@ -107,6 +122,7 @@ public sealed class ContentText : IContent
|
||||
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -41,7 +41,7 @@ public interface IContent
|
||||
/// <summary>
|
||||
/// Uses the provider to create the content.
|
||||
/// </summary>
|
||||
public Task CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default);
|
||||
public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the corresponding ERI content type.
|
||||
|
||||
@@ -13,6 +13,11 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (209, "v0.9.34, build 209 (2025-03-11 13:02 UTC)", "v0.9.34.md"),
|
||||
new (208, "v0.9.33, build 208 (2025-03-11 08:14 UTC)", "v0.9.33.md"),
|
||||
new (207, "v0.9.32, build 207 (2025-03-08 20:15 UTC)", "v0.9.32.md"),
|
||||
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"),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
IsLastContentBlock="@isLastBlock"
|
||||
IsSecondToLastBlock="@isSecondLastBlock"
|
||||
RegenerateFunc="@this.RegenerateBlock"
|
||||
RegenerateEnabled="@(() => this.IsProviderSelected)"
|
||||
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
|
||||
EditLastBlockFunc="@this.EditLastBlock"
|
||||
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Send"
|
||||
OnAdornmentClick="() => this.SendMessage()"
|
||||
ReadOnly="!this.IsProviderSelected || this.isStreaming"
|
||||
Disabled="@this.IsInputForbidden()"
|
||||
Immediate="@true"
|
||||
OnKeyUp="this.InputKeyEvent"
|
||||
UserAttributes="@USER_INPUT_ATTRIBUTES"
|
||||
@@ -113,6 +113,14 @@
|
||||
{
|
||||
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
|
||||
}
|
||||
|
||||
@if (!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
{
|
||||
<MudTooltip Text="The selected provider is not allowed in this chat due to data security reasons." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudIconButton />
|
||||
</MudToolBar>
|
||||
</FooterContent>
|
||||
</InnerScrolling>
|
||||
@@ -275,6 +275,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
|
||||
this.earlyDataSourceOptions = chatDefaultOptions;
|
||||
if(this.ChatThread is not null)
|
||||
this.ChatThread.DataSourceOptions = chatDefaultOptions;
|
||||
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions);
|
||||
}
|
||||
|
||||
@@ -337,6 +340,20 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.earlyDataSourceOptions = updatedOptions;
|
||||
}
|
||||
|
||||
private bool IsInputForbidden()
|
||||
{
|
||||
if (!this.IsProviderSelected)
|
||||
return true;
|
||||
|
||||
if(this.isStreaming)
|
||||
return true;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
|
||||
@@ -371,6 +388,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
return;
|
||||
|
||||
// We need to blur the focus away from the input field
|
||||
// to be able to clear the field:
|
||||
await this.inputField.BlurAsync();
|
||||
@@ -475,7 +495,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
this.cancellationTokenSource = null;
|
||||
@@ -773,6 +793,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
return;
|
||||
|
||||
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
@@ -42,16 +42,13 @@
|
||||
else
|
||||
{
|
||||
<MudExpansionPanels Class="mb-3">
|
||||
@for (var index = 0; index < this.retrievalInfoformation.Count; index++)
|
||||
{
|
||||
var info = this.retrievalInfoformation[index];
|
||||
<ExpansionPanel HeaderText="@this.RetrievalName(info)" HeaderIcon="@Icons.Material.Filled.Info" IsExpanded="index == 0">
|
||||
<TextInfoLines Label="Description" MaxLines="14" Value="@info.Description" ClipboardTooltipSubject="the retrieval description"/>
|
||||
<TextInfoLines Label="Parameters" MaxLines="14" Value="@this.RetrievalParameters(info)" ClipboardTooltipSubject="the retrieval parameters"/>
|
||||
<ExpansionPanel HeaderText="@this.RetrievalName(this.selectedRetrievalInfo)" HeaderIcon="@Icons.Material.Filled.Info">
|
||||
<TextInfoLines Label="Description" MaxLines="14" Value="@this.selectedRetrievalInfo.Description" ClipboardTooltipSubject="the retrieval description"/>
|
||||
<TextInfoLines Label="Parameters" MaxLines="14" Value="@this.RetrievalParameters(this.selectedRetrievalInfo)" ClipboardTooltipSubject="the retrieval parameters"/>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(info.Link))
|
||||
@if (!string.IsNullOrWhiteSpace(this.selectedRetrievalInfo.Link))
|
||||
{
|
||||
<MudButton Href="@info.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
|
||||
<MudButton Href="@this.selectedRetrievalInfo.Link" Target="_blank" Class="mt-3" Color="Color.Primary" StartIcon="@Icons.Material.Filled.OpenInNew">
|
||||
Open web link, show more information
|
||||
</MudButton>
|
||||
}
|
||||
@@ -59,7 +56,8 @@
|
||||
<MudText Typo="Typo.h6" Class="mt-3">
|
||||
Embeddings
|
||||
</MudText>
|
||||
@if (!info.Embeddings.Any())
|
||||
@* ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract *@
|
||||
@if (this.selectedRetrievalInfo.Embeddings is null || !this.selectedRetrievalInfo.Embeddings.Any())
|
||||
{
|
||||
<MudJustifiedText Typo="Typo.body1" Color="Color.Info" Class="mb-3">
|
||||
The data source does not provide any embedding information.
|
||||
@@ -68,9 +66,9 @@
|
||||
else
|
||||
{
|
||||
<MudExpansionPanels>
|
||||
@for (var embeddingIndex = 0; embeddingIndex < info.Embeddings.Count; embeddingIndex++)
|
||||
@for (var embeddingIndex = 0; embeddingIndex < this.selectedRetrievalInfo.Embeddings.Count; embeddingIndex++)
|
||||
{
|
||||
var embedding = info.Embeddings[embeddingIndex];
|
||||
var embedding = this.selectedRetrievalInfo.Embeddings[embeddingIndex];
|
||||
<ExpansionPanel HeaderText="@embedding.EmbeddingName" HeaderIcon="@Icons.Material.Filled.Info" IsExpanded="embeddingIndex == 0">
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.FormatShapes" Label="Type" Value="@embedding.EmbeddingType" ClipboardTooltipSubject="the embedding type"/>
|
||||
<TextInfoLines Label="Description" MaxLines="14" Value="@embedding.Description" ClipboardTooltipSubject="the embedding description"/>
|
||||
@@ -87,7 +85,6 @@
|
||||
</MudExpansionPanels>
|
||||
}
|
||||
</ExpansionPanel>
|
||||
}
|
||||
</MudExpansionPanels>
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposabl
|
||||
private string serverDescription = string.Empty;
|
||||
private ProviderType securityRequirements = ProviderType.NONE;
|
||||
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
|
||||
private RetrievalInfo selectedRetrievalInfo;
|
||||
|
||||
private bool IsOperationInProgress { get; set; } = true;
|
||||
|
||||
@@ -97,7 +98,7 @@ public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposabl
|
||||
return;
|
||||
}
|
||||
|
||||
var loginResult = await client.AuthenticateAsync(this.DataSource, this.RustService);
|
||||
var loginResult = await client.AuthenticateAsync(this.RustService);
|
||||
if (!loginResult.Successful)
|
||||
{
|
||||
this.dataIssues.Add(loginResult.Message);
|
||||
@@ -130,7 +131,7 @@ public partial class DataSourceERI_V1InfoDialog : ComponentBase, IAsyncDisposabl
|
||||
}
|
||||
|
||||
this.retrievalInfoformation = retrievalInfoResult.Data ?? [];
|
||||
|
||||
this.selectedRetrievalInfo = this.retrievalInfoformation.FirstOrDefault(x => x.Id == this.DataSource.SelectedRetrievalId);
|
||||
this.StateHasChanged();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -110,10 +110,24 @@
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
}
|
||||
|
||||
@if (this.availableRetrievalProcesses.Count > 0)
|
||||
{
|
||||
<MudSelect @bind-Value="@this.dataSelectedRetrievalProcess" Text="@this.dataSelectedRetrievalProcess.Name" Label="Select one retrieval process" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateRetrievalProcess">
|
||||
@foreach (var retrievalProcess in this.availableRetrievalProcesses)
|
||||
{
|
||||
<MudSelectItem Value="@retrievalProcess">
|
||||
@retrievalProcess.Name
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudSelect @bind-Value="@this.dataSecurityPolicy" Text="@this.dataSecurityPolicy.ToSelectionText()" Label="Your security policy" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateSecurityPolicy">
|
||||
@foreach (var policy in Enum.GetValues<DataSourceSecurity>())
|
||||
{
|
||||
<MudSelectItem Value="@policy">@policy.ToSelectionText()</MudSelectItem>
|
||||
<MudSelectItem Value="@policy">
|
||||
@policy.ToSelectionText()
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ using AIStudio.Tools.Validation;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
@@ -59,6 +61,8 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
private int dataPort;
|
||||
private AuthMethod dataAuthMethod;
|
||||
private string dataUsername = string.Empty;
|
||||
private List<RetrievalInfo> availableRetrievalProcesses = [];
|
||||
private RetrievalInfo dataSelectedRetrievalProcess;
|
||||
|
||||
// We get the form reference from Blazor code to validate it manually:
|
||||
private MudForm form = null!;
|
||||
@@ -91,6 +95,9 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
// When editing, we need to load the data:
|
||||
if(this.IsEditing)
|
||||
{
|
||||
//
|
||||
// Assign the data to the form fields:
|
||||
//
|
||||
this.dataEditingPreviousInstanceName = this.DataSource.Name.ToLowerInvariant();
|
||||
this.dataNum = this.DataSource.Num;
|
||||
this.dataId = this.DataSource.Id;
|
||||
@@ -114,6 +121,12 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
await this.form.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
// Load the data:
|
||||
await this.TestConnection();
|
||||
|
||||
// Select the retrieval process:
|
||||
this.dataSelectedRetrievalProcess = this.availableRetrievalProcesses.FirstOrDefault(n => n.Id == this.DataSource.SelectedRetrievalId);
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
@@ -153,6 +166,7 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
Username = this.dataUsername,
|
||||
Type = DataSourceType.ERI_V1,
|
||||
SecurityPolicy = this.dataSecurityPolicy,
|
||||
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,13 +188,8 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(14));
|
||||
var dataSource = new DataSourceERI_V1
|
||||
{
|
||||
Hostname = this.dataHostname,
|
||||
Port = this.dataPort
|
||||
};
|
||||
|
||||
using var client = ERIClientFactory.Get(ERIVersion.V1, dataSource);
|
||||
this.DataSource = this.CreateDataSource();
|
||||
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
|
||||
if(client is null)
|
||||
{
|
||||
await this.form.Validate();
|
||||
@@ -202,7 +211,7 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
|
||||
this.availableAuthMethods = authSchemes.Data!.Select(n => n.AuthMethod).ToList();
|
||||
|
||||
var loginResult = await client.AuthenticateAsync(this.DataSource, this.RustService, cts.Token);
|
||||
var loginResult = await client.AuthenticateAsync(this.RustService, this.dataSecret, cts.Token);
|
||||
if (!loginResult.Successful)
|
||||
{
|
||||
await this.form.Validate();
|
||||
@@ -224,6 +233,18 @@ public partial class DataSourceERI_V1Dialog : ComponentBase, ISecretId
|
||||
|
||||
this.dataSourceSecurityRequirements = securityRequirementsRequest.Data;
|
||||
|
||||
var retrievalInfoRequest = await client.GetRetrievalInfoAsync(cts.Token);
|
||||
if (!retrievalInfoRequest.Successful)
|
||||
{
|
||||
await this.form.Validate();
|
||||
|
||||
Array.Resize(ref this.dataIssues, this.dataIssues.Length + 1);
|
||||
this.dataIssues[^1] = retrievalInfoRequest.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
this.availableRetrievalProcesses = retrievalInfoRequest.Data ?? [];
|
||||
|
||||
this.connectionTested = true;
|
||||
this.connectionSuccessfulTested = true;
|
||||
this.Logger.LogInformation("Connection to the ERI v1 server was successful tested.");
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="7.1.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
|
||||
<PackageReference Include="MudBlazor" Version="7.16.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="7.14.0" />
|
||||
|
||||
@@ -23,10 +23,65 @@
|
||||
</MudButton>
|
||||
</ExpansionPanel>
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Custom.Brands.GitHub" HeaderText="Community & Code">
|
||||
<MudList T="string" Class="mb-1">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Home" Target="_blank" Href="http://mindworkai.org/">
|
||||
Discover MindWork AI's mission and vision on our official homepage.
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Custom.Brands.GitHub" Target="_blank" Href="https://github.com/MindWorkAI/AI-Studio">
|
||||
Browse AI Studio's source code on GitHub — we welcome your contributions.
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.PrivateConnectivity" Target="_blank" Href="https://github.com/MindWorkAI/ERI">
|
||||
Connect AI Studio to your organization's data with our External Retrieval Interface (ERI).
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Timeline" Target="_blank" Href="https://github.com/orgs/MindWorkAI/projects/2/views/3">
|
||||
View our project roadmap and help shape AI Studio's future development.
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.BugReport" Target="_blank" Href="https://github.com/MindWorkAI/AI-Studio/issues">
|
||||
Did you find a bug or are you experiencing issues? Report your concern here.
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Lightbulb" Target="_blank" Href="https://github.com/MindWorkAI/Planning/issues">
|
||||
Have feature ideas? Submit suggestions for future AI Studio enhancements.
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
</ExpansionPanel>
|
||||
|
||||
<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/>
|
||||
</ExpansionPanel>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
@@ -9,6 +12,12 @@ 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>()!;
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
<MudList T="string">
|
||||
<Supporter Name="DevNullx64" Type="SupporterType.INDIVIDUAL" URL="https://github.com/DevNullx64" Acknowledgment="Thanks Luc for your build script contribution."/>
|
||||
<Supporter Name="SolsticeSpectrum" Type="SupporterType.INDIVIDUAL" URL="https://github.com/SolsticeSpectrum" Acknowledgment="Thanks for your build script contribution."/>
|
||||
<Supporter Name="peerschuett" Type="SupporterType.INDIVIDUAL" URL="https://github.com/peerschuett" Acknowledgment="Thanks for your contributions to the project."/>
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
@@ -139,7 +139,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
||||
this.isStreaming = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||
this.suggestion = aiText.Text;
|
||||
|
||||
this.isStreaming = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@@ -37,7 +38,6 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
@@ -87,21 +87,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-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)"),
|
||||
};
|
||||
|
||||
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());
|
||||
return this.LoadModels(token, apiKeyProvisional).ContinueWith(t => t.Result.Concat(additionalModels).OrderBy(x => x.Id).AsEnumerable(), token);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -117,4 +111,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,136 @@
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,6 @@ public class ProviderGoogle(ILogger logger) : BaseProvider("https://generativela
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
@@ -50,7 +50,6 @@ public class ProviderGroq(ILogger logger) : BaseProvider("https://api.groq.com/o
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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.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,
|
||||
};
|
||||
|
||||
@@ -48,7 +48,6 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
@@ -76,7 +76,6 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.RAG => "assistant",
|
||||
ChatRole.SYSTEM => systemPromptRole,
|
||||
|
||||
_ => "user",
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostnam
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
@@ -50,7 +50,6 @@ public sealed class ProviderX(ILogger logger) : BaseProvider("https://api.x.ai/v
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
ChatRole.SYSTEM => "system",
|
||||
ChatRole.RAG => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ internal static class Redirect
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
if (path.StartsWith(SYSTEM, StringComparison.InvariantCulture))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -51,6 +51,9 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
/// <inheritdoc />
|
||||
public ERIVersion Version { get; init; } = ERIVersion.V1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SelectedRetrievalId { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<IRetrievalContext>> RetrieveDataAsync(IContent lastPrompt, ChatThread thread, CancellationToken token = default)
|
||||
{
|
||||
@@ -59,7 +62,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
var logger = Program.SERVICE_PROVIDER.GetRequiredService<ILogger<DataSourceERI_V1>>();
|
||||
|
||||
using var eriClient = ERIClientFactory.Get(this.Version, this)!;
|
||||
var authResponse = await eriClient.AuthenticateAsync(this, rustService, token);
|
||||
var authResponse = await eriClient.AuthenticateAsync(rustService, cancellationToken: token);
|
||||
if (authResponse.Successful)
|
||||
{
|
||||
var retrievalRequest = new RetrievalRequest
|
||||
@@ -74,7 +77,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
|
||||
Thread = await thread.ToERIChatThread(token),
|
||||
MaxMatches = 10,
|
||||
RetrievalProcessId = null, // The ERI server selects the retrieval process when multiple processes are available
|
||||
RetrievalProcessId = string.IsNullOrWhiteSpace(this.SelectedRetrievalId) ? null : this.SelectedRetrievalId,
|
||||
Parameters = null, // The ERI server selects useful default parameters
|
||||
};
|
||||
|
||||
@@ -97,7 +100,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
Links = eriContext.Links,
|
||||
Category = eriContext.Type.ToRetrievalContentCategory(),
|
||||
MatchedText = eriContext.MatchedContent,
|
||||
DataSourceName = eriContext.Name,
|
||||
DataSourceName = this.Name,
|
||||
SurroundingContent = eriContext.SurroundingContent,
|
||||
});
|
||||
break;
|
||||
@@ -111,7 +114,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
Source = eriContext.MatchedContent,
|
||||
Category = eriContext.Type.ToRetrievalContentCategory(),
|
||||
SourceType = ContentImageSource.BASE64,
|
||||
DataSourceName = eriContext.Name,
|
||||
DataSourceName = this.Name,
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ 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)
|
||||
{
|
||||
return type switch
|
||||
public static string GetDisplayName(this DataSourceType type) => type switch
|
||||
{
|
||||
DataSourceType.LOCAL_FILE => "Local File",
|
||||
DataSourceType.LOCAL_DIRECTORY => "Local Directory",
|
||||
@@ -21,4 +19,3 @@ public static class DataSourceTypeExtension
|
||||
_ => "None",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@ namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public static class ThemesExtensions
|
||||
{
|
||||
public static string GetName(this Themes theme)
|
||||
{
|
||||
return theme switch
|
||||
public static string GetName(this Themes theme) => theme switch
|
||||
{
|
||||
Themes.SYSTEM => "Synchronized with the operating system settings",
|
||||
Themes.LIGHT => "Always use light theme",
|
||||
@@ -13,4 +11,3 @@ public static class ThemesExtensions
|
||||
_ => "Unknown setting",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -29,4 +29,9 @@ public interface IERIDataSource : IExternalDataSource
|
||||
/// The ERI specification to use.
|
||||
/// </summary>
|
||||
public ERIVersion Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the selected retrieval process.
|
||||
/// </summary>
|
||||
public string SelectedRetrievalId { get; init; }
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Tools.ERIClient;
|
||||
|
||||
public abstract class ERIClientBase(string baseAddress) : IDisposable
|
||||
public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
|
||||
{
|
||||
protected readonly IERIDataSource dataSource = dataSource;
|
||||
|
||||
protected static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNamingPolicy = null,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
|
||||
@@ -20,7 +25,7 @@ public abstract class ERIClientBase(string baseAddress) : IDisposable
|
||||
|
||||
protected readonly HttpClient httpClient = new()
|
||||
{
|
||||
BaseAddress = new Uri(baseAddress),
|
||||
BaseAddress = new Uri($"{dataSource.Hostname}:{dataSource.Port}"),
|
||||
};
|
||||
|
||||
protected string securityToken = string.Empty;
|
||||
|
||||
@@ -7,7 +7,7 @@ public static class ERIClientFactory
|
||||
{
|
||||
public static IERIClient? Get(ERIVersion version, IERIDataSource dataSource) => version switch
|
||||
{
|
||||
ERIVersion.V1 => new ERIClientV1($"{dataSource.Hostname}:{dataSource.Port}"),
|
||||
ERIVersion.V1 => new ERIClientV1(dataSource),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools.ERIClient;
|
||||
|
||||
public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIClient
|
||||
public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource), IERIClient
|
||||
{
|
||||
#region Implementation of IERIClient
|
||||
|
||||
@@ -59,13 +59,13 @@ public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIC
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<APIResponse<AuthResponse>> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default)
|
||||
public async Task<APIResponse<AuthResponse>> AuthenticateAsync(RustService rustService, string? temporarySecret = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var authMethod = dataSource.AuthMethod;
|
||||
var username = dataSource.Username;
|
||||
switch (dataSource.AuthMethod)
|
||||
var authMethod = this.dataSource.AuthMethod;
|
||||
var username = this.dataSource.Username;
|
||||
switch (this.dataSource.AuthMethod)
|
||||
{
|
||||
case AuthMethod.NONE:
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
|
||||
@@ -99,7 +99,10 @@ public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIC
|
||||
}
|
||||
|
||||
case AuthMethod.USERNAME_PASSWORD:
|
||||
var passwordResponse = await rustService.GetSecret(dataSource);
|
||||
string password;
|
||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||
{
|
||||
var passwordResponse = await rustService.GetSecret(this.dataSource);
|
||||
if (!passwordResponse.Success)
|
||||
{
|
||||
return new()
|
||||
@@ -109,7 +112,11 @@ public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIC
|
||||
};
|
||||
}
|
||||
|
||||
var password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||
password = await passwordResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||
}
|
||||
else
|
||||
password = temporarySecret;
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
|
||||
{
|
||||
// We must send both values inside the header. The username field is named 'user'.
|
||||
@@ -146,7 +153,10 @@ public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIC
|
||||
}
|
||||
|
||||
case AuthMethod.TOKEN:
|
||||
var tokenResponse = await rustService.GetSecret(dataSource);
|
||||
string token;
|
||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||
{
|
||||
var tokenResponse = await rustService.GetSecret(this.dataSource);
|
||||
if (!tokenResponse.Success)
|
||||
{
|
||||
return new()
|
||||
@@ -156,7 +166,11 @@ public class ERIClientV1(string baseAddress) : ERIClientBase(baseAddress), IERIC
|
||||
};
|
||||
}
|
||||
|
||||
var token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||
token = await tokenResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||
}
|
||||
else
|
||||
token = temporarySecret;
|
||||
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Post, $"auth?authMethod={authMethod}"))
|
||||
{
|
||||
request.Headers.Add("Authorization", $"Bearer {token}");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
@@ -19,11 +18,11 @@ public interface IERIClient : IDisposable
|
||||
/// <summary>
|
||||
/// Authenticate the user to the ERI server.
|
||||
/// </summary>
|
||||
/// <param name="dataSource">The data source to use.</param>
|
||||
/// <param name="rustService">The Rust service.</param>
|
||||
/// <param name="temporarySecret">The temporary secret when adding a new data source, and the secret is not yet stored in the OS.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The authentication response.</returns>
|
||||
public Task<APIResponse<AuthResponse>> AuthenticateAsync(IERIDataSource dataSource, RustService rustService, CancellationToken cancellationToken = default);
|
||||
public Task<APIResponse<AuthResponse>> AuthenticateAsync(RustService rustService, string? temporarySecret = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the data source information from the ERI server.
|
||||
|
||||
@@ -66,22 +66,8 @@ public sealed class AugmentationOne : IAugmentationProcess
|
||||
// Let's convert all retrieval contexts to Markdown:
|
||||
await retrievalContexts.AsMarkdown(sb, token);
|
||||
|
||||
//
|
||||
// Append the entire augmentation to the chat thread,
|
||||
// just before the user prompt:
|
||||
//
|
||||
chatThread.Blocks.Insert(chatThread.Blocks.Count - 1, new()
|
||||
{
|
||||
Role = ChatRole.RAG,
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
ContentType = ContentType.TEXT,
|
||||
HideFromUser = true,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = sb.ToString(),
|
||||
}
|
||||
});
|
||||
|
||||
// Add the augmented data to the chat thread:
|
||||
chatThread.AugmentedData = sb.ToString();
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.RAG.AugmentationProcesses;
|
||||
using AIStudio.Tools.RAG.DataSourceSelectionProcesses;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -38,6 +39,30 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
// makes sense to proceed with the RAG process:
|
||||
var proceedWithRAG = true;
|
||||
|
||||
//
|
||||
// We read the last block in the chat thread. We need to re-arrange
|
||||
// the order of blocks later, after the augmentation process takes
|
||||
// place:
|
||||
//
|
||||
if(chatThread.Blocks.Count == 0)
|
||||
{
|
||||
logger.LogError("The chat thread is empty. Skipping the RAG process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
if (chatThread.Blocks.Last().Role != ChatRole.AI)
|
||||
{
|
||||
logger.LogError("The last block in the chat thread is not the AI block. There is something wrong with the chat thread. Skipping the RAG process.");
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
//
|
||||
// At this point in time, the chat thread contains already the
|
||||
// last block, which is the waiting AI block. We need to remove
|
||||
// this block before we call some parts of the RAG process:
|
||||
//
|
||||
var chatThreadWithoutWaitingAIBlock = chatThread with { Blocks = chatThread.Blocks[..^1] };
|
||||
|
||||
//
|
||||
// When the user wants to bind data sources to the chat, we
|
||||
// have to check if the data sources are available for the
|
||||
@@ -72,6 +97,56 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
logger.LogWarning("No data sources are selected. The RAG process is skipped.");
|
||||
proceedWithRAG = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
var previousDataSecurity = chatThread.DataSecurity;
|
||||
|
||||
//
|
||||
// Update the data security of the chat thread. We consider the current data security
|
||||
// of the chat thread and the data security of the selected data sources:
|
||||
//
|
||||
var dataSecurityRestrictedToSelfHosted = selectedDataSources.Any(x => x.SecurityPolicy is DataSourceSecurity.SELF_HOSTED);
|
||||
chatThread.DataSecurity = dataSecurityRestrictedToSelfHosted switch
|
||||
{
|
||||
//
|
||||
//
|
||||
// Case: the data sources which are selected have a security policy
|
||||
// of SELF_HOSTED (at least one data source).
|
||||
//
|
||||
// When the policy was already set to ALLOW_ANY, we restrict it
|
||||
// to SELF_HOSTED.
|
||||
//
|
||||
true => DataSourceSecurity.SELF_HOSTED,
|
||||
|
||||
//
|
||||
// Case: the data sources which are selected have a security policy
|
||||
// of ALLOW_ANY (none of the data sources has a SELF_HOSTED policy).
|
||||
//
|
||||
// When the policy was already set to SELF_HOSTED, we must keep that.
|
||||
//
|
||||
false => chatThread.DataSecurity switch
|
||||
{
|
||||
//
|
||||
// When the policy was not specified yet, we set it to ALLOW_ANY.
|
||||
//
|
||||
DataSourceSecurity.NOT_SPECIFIED => DataSourceSecurity.ALLOW_ANY,
|
||||
DataSourceSecurity.ALLOW_ANY => DataSourceSecurity.ALLOW_ANY,
|
||||
|
||||
//
|
||||
// When the policy was already set to SELF_HOSTED, we must keep that.
|
||||
// This is important since the thread might already contain data
|
||||
// from a data source with a SELF_HOSTED policy.
|
||||
//
|
||||
DataSourceSecurity.SELF_HOSTED => DataSourceSecurity.SELF_HOSTED,
|
||||
|
||||
// Default case: we use the current data security of the chat thread.
|
||||
_ => chatThread.DataSecurity,
|
||||
}
|
||||
};
|
||||
|
||||
if (previousDataSecurity != chatThread.DataSecurity)
|
||||
logger.LogInformation($"The data security of the chat thread was updated from '{previousDataSecurity}' to '{chatThread.DataSecurity}'.");
|
||||
}
|
||||
|
||||
//
|
||||
// Trigger the retrieval part of the (R)AG process:
|
||||
@@ -84,7 +159,7 @@ public sealed class AISrcSelWithRetCtxVal : IRagProcess
|
||||
//
|
||||
var retrievalTasks = new List<Task<IReadOnlyList<IRetrievalContext>>>(selectedDataSources.Count);
|
||||
foreach (var dataSource in selectedDataSources)
|
||||
retrievalTasks.Add(dataSource.RetrieveDataAsync(lastPrompt, chatThread, token));
|
||||
retrievalTasks.Add(dataSource.RetrieveDataAsync(lastPrompt, chatThreadWithoutWaitingAIBlock, token));
|
||||
|
||||
//
|
||||
// Wait for all retrieval tasks to finish:
|
||||
|
||||
@@ -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);
|
||||
@@ -113,7 +113,7 @@ public sealed class DataSourceService
|
||||
}
|
||||
|
||||
this.logger.LogInformation($"Authenticating with ERI source '{source.Name}' (id={source.Id})...");
|
||||
var loginResult = await client.AuthenticateAsync(eriSource, this.rustService, cancellationTokenSource.Token);
|
||||
var loginResult = await client.AuthenticateAsync(this.rustService, cancellationToken: cancellationTokenSource.Token);
|
||||
if (!loginResult.Successful)
|
||||
{
|
||||
this.logger.LogWarning($"Authentication with ERI source '{source.Name}' (id={source.Id}) failed. We skip this source. Reason: {loginResult.Message}");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -93,6 +93,14 @@ public sealed class DataSourceValidation
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidateRetrievalProcess(RetrievalInfo retrievalInfo)
|
||||
{
|
||||
if(retrievalInfo == default)
|
||||
return "Please select one retrieval process.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? ValidatingName(string dataSourceName)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(dataSourceName))
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
},
|
||||
"HtmlAgilityPack": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.11.72, )",
|
||||
"resolved": "1.11.72",
|
||||
"contentHash": "RNLgXxTFdIGFI+o5l8c2aJ2L5StIRn9Uv8HKR76p7QP4ZUL26wzpWUCWh08xWUdkL2kocl+Xhv6VUu0rA1npVg=="
|
||||
"requested": "[1.11.74, )",
|
||||
"resolved": "1.11.74",
|
||||
"contentHash": "q0wRGbegtr4sZXjCNoV3OeRLTOcTNJQKiO9etNVSKPoTo33unmSK8Ahg36C4jIg/Hd3aw8YnTQjtKpBy+wlOpg=="
|
||||
},
|
||||
"Microsoft.Extensions.FileProviders.Embedded": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,16 @@
|
||||
# v0.9.32, build 207 (2025-03-08 20:15 UTC)
|
||||
- Added the "Community & Code" section to the about page. It includes links to the GitHub repositories and the project website.
|
||||
- Added the retrieval process section for ERI data sources in the configuration dialog. Like all RAG functions, this is hidden behind the RAG feature flag in the app settings due to its preview status.
|
||||
- Improved data security by preventing the use of cloud LLMs after confidential data has been retrieved previously.
|
||||
- Improved the ERI client to expect JSON responses and send JSON requests using camel case.
|
||||
- Improved the ERI client to raise an error when the server responds with additional JSON data that is not expected.
|
||||
- Improved the error handling in the ERI data source info dialog in cases where servers respond with an invalid message.
|
||||
- Improved the error handling for the entire RAG process.
|
||||
- Improved chat thread persistence after modifications through the RAG process.
|
||||
- Improved the augmentation and generation part of RAG by passing the augmented data into the system prompt.
|
||||
- Fixed the chat thread we use for the data retrieval by removing the last block, which is meant to be for the final AI answer.
|
||||
- Fixed the data source name for ERI data sources when performing data retrieval.
|
||||
- Fixed the default data source selection when replacing the current chat with a new one.
|
||||
- Fixed the state of the re-generate button in the chat thread, when no provider is selected or the data security is preventing the use of cloud LLMs.
|
||||
- Updated the code contributions section in the supporter page. Thanks `peerschuett` for being our next contributor.
|
||||
- Upgraded code dependencies.
|
||||
@@ -0,0 +1,2 @@
|
||||
# v0.9.33, build 208 (2025-03-11 08:14 UTC)
|
||||
- Fixed a bug where the authentication methods for added ERI servers were not applied correctly. Thanks Kerstin `KeSah001` for reporting this issue.
|
||||
@@ -0,0 +1,2 @@
|
||||
# v0.9.34, build 209 (2025-03-11 13:02 UTC)
|
||||
- Fixed another bug related to ERI server authentication when adding new servers.
|
||||
@@ -3,5 +3,12 @@
|
||||
### New Rules
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -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)
|
||||
{
|
||||
@@ -38,14 +38,14 @@ public class ProviderAccessAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
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.34
|
||||
2025-03-11 13:02:01 UTC
|
||||
209
|
||||
8.0.113 (commit 8f216348dc)
|
||||
8.0.13 (commit eba546b0f0)
|
||||
1.85.0 (commit 4d91de4e4)
|
||||
7.16.0
|
||||
1.8.1
|
||||
03431b85b48, release
|
||||
2429788ccc0, release
|
||||
Generated
+31
-22
@@ -377,9 +377,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.6"
|
||||
version = "1.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
|
||||
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
@@ -2181,9 +2184,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "3.6.1"
|
||||
version = "3.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833"
|
||||
checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dbus-secret-service",
|
||||
@@ -2214,9 +2217,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
version = "0.2.170"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -2261,9 +2264,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
version = "0.4.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -2347,7 +2350,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.29"
|
||||
version = "0.9.34"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arboard",
|
||||
@@ -2365,6 +2368,7 @@ dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rcgen",
|
||||
"reqwest 0.12.12",
|
||||
"ring",
|
||||
"rocket",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2758,9 +2762,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
version = "1.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
@@ -3557,15 +3561,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
version = "0.17.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -3873,18 +3876,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4028,6 +4031,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
@@ -4719,9 +4728,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.42.0"
|
||||
version = "1.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
||||
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -4736,9 +4745,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mindwork-ai-studio"
|
||||
version = "0.9.29"
|
||||
version = "0.9.34"
|
||||
edition = "2021"
|
||||
description = "MindWork AI Studio"
|
||||
authors = ["Thorsten Sommer"]
|
||||
@@ -11,14 +11,14 @@ tauri-build = { version = "1.5", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog"] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
serde_json = "1.0.134"
|
||||
keyring = { version = "3.6.1", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||
keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] }
|
||||
arboard = "3.4.1"
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.44", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
flexi_logger = "0.29.8"
|
||||
log = { version = "0.4", features = ["kv"] }
|
||||
once_cell = "1.20"
|
||||
log = { version = "0.4.26", features = ["kv"] }
|
||||
once_cell = "1.20.3"
|
||||
rocket = { version = "0.5.1", features = ["json", "tls"] }
|
||||
rand = "0.9"
|
||||
rand_chacha = "0.9"
|
||||
@@ -33,6 +33,7 @@ rcgen = { version = "0.13.2", features = ["pem"] }
|
||||
|
||||
# Fixes security vulnerability downstream, where the upstream is not fixed yet:
|
||||
url = "2.5"
|
||||
ring = "0.17.13"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# See issue https://github.com/tauri-apps/tauri/issues/4470
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
|
||||
+84
-8
@@ -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 runtime_logger = Logger::try_with_str(log_config).expect("Cannot create logging")
|
||||
.log_to_file(FileSpec::default()
|
||||
let log_path = FileSpec::default()
|
||||
.directory(get_startup_log_path())
|
||||
.basename(log_basename)
|
||||
.suppress_timestamp()
|
||||
.suffix("log"))
|
||||
.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(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()
|
||||
let log_path = FileSpec::default()
|
||||
.directory(logger_path)
|
||||
.basename("events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log")))?;
|
||||
.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(())
|
||||
}
|
||||
@@ -161,3 +222,18 @@ 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,
|
||||
}
|
||||
+34
-16
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashSet;
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::config::Shutdown;
|
||||
@@ -26,21 +25,8 @@ 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.34"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
||||
Reference in new issue
Block a user