Fixed agents being left without a model when none was set aside for them (#985)
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-09-19 21:39:16 +02:00
1 parent 1379ff6aab
commit 459165f1be
13 files changed
+540 -91

No files matched your search

@@ -3,7 +3,7 @@
@{
var availableProviderItems = this.GetAvailableProviderSelectionItems().ToList();
}
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Margin="Margin.Dense" Label="@(this.Label ?? T("Provider"))" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
@foreach (var providerItem in availableProviderItems)
{
<MudSelectItem Value="@providerItem.Provider">
@@ -19,6 +19,17 @@ public partial class ProviderSelection : MSGComponentBase
[Parameter]
public Func<AIStudio.Settings.Provider, string?> ValidateProvider { get; set; } = _ => null;
/// <summary>
/// What this place calls the thing being picked, when "Provider" is not the word it uses.
/// </summary>
/// <remarks>
/// Some places have the user pick a provider in order to set it up, and there the word is right.
/// Others have them pick one to get a job done, and speak of the model throughout. A field
/// labelled "Provider" in the middle of such a text reads like a second, different choice.
/// </remarks>
[Parameter]
public string? Label { get; set; }
/// <summary>
/// Gets or sets whether provider selection is disabled.
/// </summary>
@@ -3,7 +3,13 @@
<MudTextSwitch Label="@T("Read content from web?")" Disabled="@this.AgentIsRunning" Value="@this.Preselect" ValueChanged="@this.ShowWebContentReaderChanged" LabelOn="@T("Show web content options")" LabelOff="@T("Hide web content options")" />
@if (this.Preselect)
{
<MudTextSwitch Label="@T("Cleanup content by using an LLM agent?")" Value="@this.PreselectContentCleanerAgent" ValueChanged="@this.UseContentCleanerAgentChanged" Validation="@this.ValidateProvider" Disabled="@this.AgentIsRunning" LabelOn="@T("The content is cleaned using an LLM agent: the main content is extracted, advertisements and other irrelevant things are attempted to be removed; relative links are attempted to be converted into absolute links so that they can be used.")" LabelOff="@T("No content cleaning")" />
<MudTextSwitch Label="@T("Cleanup content by using an LLM agent?")" Value="@this.PreselectContentCleanerAgent" ValueChanged="@this.UseContentCleanerAgentChanged" Disabled="@this.AgentIsRunning" LabelOn="@T("The content is cleaned using an LLM agent: the main content is extracted, advertisements and other irrelevant things are attempted to be removed; relative links are attempted to be converted into absolute links so that they can be used.")" LabelOff="@T("No content cleaning")" />
@if (this.ContentCleanerHint is { } contentCleanerHint)
{
<MudText Typo="Typo.body2" Color="Color.Error" Class="mb-3">
@contentCleanerHint
</MudText>
}
<MudStack Row="@true" AlignItems="@AlignItems.Baseline" Class="mb-3">
<MudTextField T="string" Label="@T("URL from which to load the content")" Value="@this.URL" ValueChanged="@this.URLValueChanged" Validation="@this.ValidateURL" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Link" Placeholder="https://..." HelperText="@T("Loads the content from your URL. Does not work when the content is hidden behind a paywall.")" Variant="Variant.Outlined" Immediate="@true" Disabled="@this.AgentIsRunning"/>
<MudButton Disabled="@(!this.IsReady || this.AgentIsRunning)" Variant="Variant.Filled" Size="Size.Large" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Download" OnClick="@this.LoadFromWeb">
@@ -72,33 +72,66 @@ public partial class ReadWebContent : MSGComponentBase
private readonly Process<ReadWebContentSteps> process = Process<ReadWebContentSteps>.INSTANCE;
private ProcessStepValue processStep;
private bool isProviderValid;
/// <summary>
/// The model the content cleaner runs with.
/// </summary>
/// <remarks>
/// This is a resolved value, not a chosen one: the reader has no model selection of its own,
/// it takes what the assistant around it uses, unless a dedicated one for the cleaner or an
/// app-wide default takes precedence. Because the assistant's model can change at any moment,
/// this is resolved again on every render instead of being remembered from the first one.
/// </remarks>
private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_TEXT_CONTENT_CLEANER, this.ProviderSettings.Id, true);
this.providerSettings = this.ProviderSettings;
this.ValidateProvider(this.PreselectContentCleanerAgent);
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
this.ResolveProvider();
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
if (!this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions)
this.providerSettings = this.ProviderSettings;
this.ValidateProvider(this.PreselectContentCleanerAgent);
this.ResolveProvider();
await base.OnParametersSetAsync();
}
#endregion
#region Overrides of MSGComponentBase
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED)
{
//
// A dedicated model for the cleaner, or the app-wide default, may be set while this
// assistant is open. Nothing about that reaches us as a parameter, so without this the
// user would have to leave the assistant and come back for it to take effect.
//
this.ResolveProvider();
this.StateHasChanged();
}
return Task.CompletedTask;
}
#endregion
/// <summary>
/// Determines the model the content cleaner runs with.
/// </summary>
/// <remarks>
/// Called from both lifecycle methods, and with the same arguments: the assistant's model is a
/// parameter, and a parameter arrives whenever the parent renders. Resolving only once would
/// leave the cleaner with whatever was set the first time this component was built.
/// </remarks>
private void ResolveProvider() => this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_TEXT_CONTENT_CLEANER, this.ProviderSettings.Id, true);
private async Task LoadFromWeb()
{
if(!this.IsReady)
@@ -128,6 +161,16 @@ public partial class ReadWebContent : MSGComponentBase
markdown = retrievedPage.ExtractedPage.Markdown;
markdown = await this.PromptInjectionGuardService.SanitizeAsync(markdown, PromptInjectionSource.WebContent(this.URL));
if (this.PreselectContentCleanerAgent && this.providerSettings == AIStudio.Settings.Provider.NONE)
{
//
// Say that the cleaning did not happen. The user asked for it, the page arrives,
// and without a word they would take the raw markdown -- navigation, cookie banner
// and advertising included -- for the cleaned result.
//
await this.MessageBus.SendError(new(Icons.Material.Filled.SettingsSuggest, T("The content was loaded, but not cleaned: no model is available for the content cleaner.")));
}
if (this.PreselectContentCleanerAgent && this.providerSettings != AIStudio.Settings.Provider.NONE)
{
this.AgentTextContentCleaner.ProviderSettings = this.providerSettings;
@@ -183,19 +226,16 @@ public partial class ReadWebContent : MSGComponentBase
await this.ContentChanged.InvokeAsync(this.Content);
}
private bool IsReady
{
get
{
if(!this.UrlIsValid)
return false;
if(this.PreselectContentCleanerAgent && !this.isProviderValid)
return false;
return true;
}
}
/// <summary>
/// Whether the content can be fetched.
/// </summary>
/// <remarks>
/// A missing model for the content cleaner is deliberately not part of this. Cleaning is an
/// option of the fetch, not a condition for it: making it one would leave the user with a
/// switch they turned on, no way to get their page, and a dead button to explain it. The page
/// is fetched, and LoadFromWeb says that it arrived uncleaned.
/// </remarks>
private bool IsReady => this.UrlIsValid;
/// <summary>
/// Whether the current URL can be loaded.
@@ -222,18 +262,30 @@ public partial class ReadWebContent : MSGComponentBase
await this.PreselectContentCleanerAgentChanged.InvokeAsync(state);
}
private string? ValidateProvider(bool shouldUseAgent)
/// <summary>
/// Says why the content cleaner has no model, or nothing when it has one.
/// </summary>
/// <remarks>
/// This is a hint, not a validation: the cleaner is an option of the reader, and an option
/// nobody can use yet must not keep the assistant around it from running. It is also stated
/// rather than remembered, so that choosing a model below makes it disappear at once.
/// The two causes lead to different places, which is why they are told apart: either no model
/// was chosen at all, or the chosen one is not trusted enough for this agent.
/// </remarks>
private string? ContentCleanerHint
{
if(shouldUseAgent && this.providerSettings == AIStudio.Settings.Provider.NONE)
get
{
this.isProviderValid = false;
return T("Please select a provider to use the cleanup agent.");
}
if(!this.PreselectContentCleanerAgent || this.providerSettings != AIStudio.Settings.Provider.NONE)
return null;
this.isProviderValid = true;
return null;
if(this.ProviderSettings == AIStudio.Settings.Provider.NONE)
return T("The content cleaner uses the model of this assistant. Please select one below.");
return T("The selected model does not meet the confidence requirements of the content cleaner. Please select another model, or configure an eligible one in the app settings.");
}
}
private string? ValidateURL(string url)
{
if(string.IsNullOrWhiteSpace(url))