Let the assistant own the URL of the web content reader

This commit is contained in:
Thorsten Sommer 2026-09-13 15:40:14 +02:00
parent a65c1162a5
commit 2b73286f1b
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
2 changed files with 39 additions and 23 deletions

View File

@ -5,8 +5,8 @@
{
<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")" />
<MudStack Row="@true" AlignItems="@AlignItems.Baseline" Class="mb-3">
<MudTextField T="string" Label="@T("URL from which to load the content")" @bind-Value="@this.providedURL" 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()">
<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">
@T("Fetch")
</MudButton>
</MudStack>

View File

@ -35,7 +35,20 @@ public partial class ReadWebContent : MSGComponentBase
[Parameter]
public EventCallback<string> ContentChanged { get; set; }
/// <summary>
/// The URL the content is loaded from.
/// </summary>
/// <remarks>
/// The URL belongs to the parent, so that it is cleared when the parent resets its form and
/// is kept when the parent stores its state.
/// </remarks>
[Parameter]
public string URL { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> URLChanged { get; set; }
[Parameter]
public AIStudio.Settings.Provider ProviderSettings { get; set; } = AIStudio.Settings.Provider.NONE;
@ -60,8 +73,6 @@ public partial class ReadWebContent : MSGComponentBase
private readonly Process<ReadWebContentSteps> process = Process<ReadWebContentSteps>.INSTANCE;
private ProcessStepValue processStep;
private string providedURL = string.Empty;
private bool urlIsValid;
private bool isProviderValid;
private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
@ -105,7 +116,7 @@ public partial class ReadWebContent : MSGComponentBase
// the URL, so their own network is not off limits.
//
var retrievedPage = await this.WebPageRetrievalService.RetrieveAsync(
new Uri(this.providedURL),
new Uri(this.URL),
new WebPageRetrievalOptions
{
TimeoutSeconds = TIMEOUT_SECONDS,
@ -115,14 +126,14 @@ public partial class ReadWebContent : MSGComponentBase
this.processStep = this.process[ReadWebContentSteps.PARSING];
this.StateHasChanged();
markdown = retrievedPage.ExtractedPage.Markdown;
markdown = await this.PromptInjectionGuardService.SanitizeAsync(markdown, PromptInjectionSource.WebContent(this.providedURL));
markdown = await this.PromptInjectionGuardService.SanitizeAsync(markdown, PromptInjectionSource.WebContent(this.URL));
if (this.PreselectContentCleanerAgent && this.providerSettings != AIStudio.Settings.Provider.NONE)
{
this.AgentTextContentCleaner.ProviderSettings = this.providerSettings;
var additionalData = new Dictionary<string, string>
{
{ "sourceURL", this.providedURL },
{ "sourceURL", this.URL },
};
this.processStep = this.process[ReadWebContentSteps.CLEANING];
@ -164,8 +175,8 @@ public partial class ReadWebContent : MSGComponentBase
// and the reasons a page cannot be read are things the user can act on: a link to a
// PDF rather than a page, a host that does not answer, a server refusing the request.
//
this.Logger.LogWarning(exception, "Could not load the web content from '{ProvidedUrl}'.", this.providedURL);
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, string.Format(this.T("The content of '{0}' could not be loaded: {1}"), this.providedURL, exception.Message)));
this.Logger.LogWarning(exception, "Could not load the web content from '{ProvidedUrl}'.", this.URL);
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, string.Format(this.T("The content of '{0}' could not be loaded: {1}"), this.URL, exception.Message)));
}
this.Content = markdown;
@ -176,16 +187,31 @@ public partial class ReadWebContent : MSGComponentBase
{
get
{
if(!this.urlIsValid)
if(!this.UrlIsValid)
return false;
if(this.PreselectContentCleanerAgent && !this.isProviderValid)
return false;
return true;
}
}
/// <summary>
/// Whether the current URL can be loaded.
/// </summary>
/// <remarks>
/// Asked of the current value instead of remembered from the last validation run: the parent
/// clears the URL when it resets its form, and the form validation does not run again at that
/// point. The fetch button would otherwise stay enabled with an empty field.
/// </remarks>
private bool UrlIsValid => this.ValidateURL(this.URL) is null;
private async Task URLValueChanged(string url)
{
await this.URLChanged.InvokeAsync(url);
}
private async Task ShowWebContentReaderChanged(bool state)
{
await this.PreselectChanged.InvokeAsync(state);
@ -211,25 +237,15 @@ public partial class ReadWebContent : MSGComponentBase
private string? ValidateURL(string url)
{
if(string.IsNullOrWhiteSpace(url))
{
this.urlIsValid = false;
return T("Please provide a URL to load the content from.");
}
var urlParsingResult = Uri.TryCreate(url, UriKind.Absolute, out var uriResult);
if(!urlParsingResult)
{
this.urlIsValid = false;
return T("Please provide a valid URL.");
}
if(uriResult is not { Scheme: "http" or "https" })
{
this.urlIsValid = false;
return T("Please provide a valid HTTP or HTTPS URL.");
}
this.urlIsValid = true;
return null;
}
}