mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 04:38:00 +00:00
Added mailboxes as local data sources (#1022)
Build and Release / Determine run mode (push) Waiting to run
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
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
This commit is contained in:
1 parent
93be539e50
commit
c4400c0ff6
343 files changed
+26051
-3122
No files matched your search
@@ -18,6 +18,9 @@ public partial class AssistantPluginSecurityCard : MSGComponentBase
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ToolRegistry ToolRegistry { get; init; } = null!;
|
||||
|
||||
private PluginAssistantSecurityState SecurityState => this.Plugin is null
|
||||
? new PluginAssistantSecurityState()
|
||||
: PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.Plugin);
|
||||
@@ -29,9 +32,10 @@ public partial class AssistantPluginSecurityCard : MSGComponentBase
|
||||
/// Tools are a capability, not a detail: an assistant allowed to search the web or read a page
|
||||
/// can carry what a user typed out of the app. Whoever decides whether to enable this plugin
|
||||
/// should see that beforehand, which is why the count sits in the header next to the audit
|
||||
/// level and the tools themselves are named in the details.
|
||||
/// level and the tools themselves are named in the details. A collection is named by its
|
||||
/// tools, since each of them is a capability of its own, see ToolRegistry.ExpandSelection.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<string> PluginToolIds => ToolSelectionRules.NormalizeSelection(this.Plugin?.AssistantToolIds ?? this.Plugin?.ChatLaunchConfiguration?.ToolIds ?? [])
|
||||
private IReadOnlyList<string> PluginToolIds => this.ToolRegistry.ExpandSelection(this.Plugin?.AssistantToolIds ?? this.Plugin?.ChatLaunchConfiguration?.ToolIds ?? [])
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
@if (this.SettingsManager.AreToolsEnabled())
|
||||
{
|
||||
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
|
||||
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" RequiredOutboundDataRestriction="@(this.ChatThread?.RequiredOutboundDataRestriction ?? OutboundDataRequirement.NONE)" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.IsCurrentChatStreaming" />
|
||||
}
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
|
||||
@@ -729,7 +729,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
if (this.currentChatTemplate.ToolIds is not { } templateToolIds)
|
||||
{
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
this.selectedToolIds = this.ToolRegistry.GetDefaultToolIds(Tools.Components.CHAT);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1149,7 +1149,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.StateHasChanged();
|
||||
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
|
||||
this.ChatThread.SelectedToolIds = [..this.selectedToolIds];
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds);
|
||||
this.ChatThread.RuntimeSelectedToolIds = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, this.ChatThread.RequiredOutboundDataRestriction.Restriction);
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
ChatThread = this.ChatThread,
|
||||
@@ -1178,11 +1178,13 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// the footer would keep showing the tools of the chat before it.
|
||||
/// </remarks>
|
||||
private void ApplyToolSelectionOfLoadedChat() =>
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(this.ChatThread?.SelectedToolIds ?? this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT));
|
||||
this.selectedToolIds = this.ChatThread?.SelectedToolIds is { } storedToolIds
|
||||
? this.ToolRegistry.NormalizeSelection(storedToolIds)
|
||||
: this.ToolRegistry.GetDefaultToolIds(Tools.Components.CHAT);
|
||||
|
||||
private void SelectedToolIdsChanged(HashSet<string> updatedToolIds)
|
||||
{
|
||||
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
|
||||
this.selectedToolIds = this.ToolRegistry.NormalizeSelection(updatedToolIds);
|
||||
|
||||
//
|
||||
// The thread keeps the selection so that reopening the chat tomorrow brings the same tools
|
||||
@@ -1667,8 +1669,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// The tools the next request would offer the model.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Filtered for the provider the same way they are before sending, so that a tool the provider
|
||||
/// is not trusted enough to receive does not count either.
|
||||
/// Filtered for the provider and the chat the same way they are before sending, so that a tool
|
||||
/// the provider is not trusted enough to receive does not count, and neither does one which a
|
||||
/// mailbox the chat read from keeps back. A new chat read no mailbox, so nothing is kept back.
|
||||
///
|
||||
/// Asked for once and used twice: their policy goes into the system prompt, and their schemas
|
||||
/// travel next to it in the request body. Both cost tokens, and both change the moment somebody
|
||||
@@ -1679,14 +1682,18 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// the request asks for.
|
||||
///
|
||||
/// Read Web Page likewise counts with its registered instructions, those of its default free
|
||||
/// address choice. With the choice switched on, a request carries a shorter instruction, so the
|
||||
/// count comes out a few tokens high.
|
||||
/// address choice in a chat which read no mailbox. With the choice switched on, a request
|
||||
/// carries a shorter instruction, and in a chat which read a mailbox a longer one, so the count
|
||||
/// comes out a few tokens off. One case it gets wrong as a whole: a mailbox which allows the
|
||||
/// configured services only, with no wiki configured, leaves Read Web Page nothing to offer, and
|
||||
/// only the request finds that out, see ReadWebPageTool.ResolveFunctionAsync.
|
||||
/// </remarks>
|
||||
/// <param name="offersSemanticSearch">Whether the next request offers Semantic Search, see OffersSemanticSearchAsync.</param>
|
||||
/// <returns>The definitions of the selected tools, and of Semantic Search when it is offered.</returns>
|
||||
private IReadOnlyList<ToolDefinition> GetRunnableToolDefinitions(bool offersSemanticSearch)
|
||||
{
|
||||
var definitions = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds)
|
||||
var outboundDataRestriction = (this.ChatThread?.RequiredOutboundDataRestriction ?? OutboundDataRequirement.NONE).Restriction;
|
||||
var definitions = this.ToolRegistry.FilterToolIdsForProvider(this.Provider, this.selectedToolIds, outboundDataRestriction)
|
||||
.Select(this.ToolRegistry.GetDefinition)
|
||||
.Where(definition => definition is not null)
|
||||
.Select(definition => definition!)
|
||||
|
||||
@@ -39,6 +39,7 @@ public partial class DataSourceCloudEmbeddingWarning : MSGComponentBase
|
||||
{
|
||||
DataSourceType.LOCAL_DIRECTORY => T("All files in this folder and its subfolders"),
|
||||
DataSourceType.LOCAL_FILE => T("The selected file"),
|
||||
DataSourceType.MAILBOX => T("Every mail of this mailbox in the selected folder and period, together with the text of its attachments,"),
|
||||
_ => T("The selected data")
|
||||
};
|
||||
|
||||
@@ -46,6 +47,7 @@ public partial class DataSourceCloudEmbeddingWarning : MSGComponentBase
|
||||
{
|
||||
DataSourceType.LOCAL_DIRECTORY => string.Format(T("All files in the folder '{0}' and its subfolders"), this.SourcePath),
|
||||
DataSourceType.LOCAL_FILE => string.Format(T("The file '{0}'"), this.SourcePath),
|
||||
DataSourceType.MAILBOX => string.Format(T("Every mail of the mailbox '{0}' in the selected folder and period, together with the text of its attachments,"), this.SourcePath),
|
||||
_ => string.Format(T("The data source '{0}'"), this.SourcePath)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,18 +3,25 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("You might configure different data sources. A data source can include one file, all files in a directory, or data from your company. Later, you can incorporate these data sources as needed when the AI requires this data to complete a certain task.")
|
||||
@if (this.AreMailboxesEnabled)
|
||||
{
|
||||
@T("You might configure different data sources. A data source can include one file, all files in a directory, a mailbox, or data from your company. Later, you can incorporate these data sources as needed when the AI requires this data to complete a certain task.")
|
||||
}
|
||||
else
|
||||
{
|
||||
@T("You might configure different data sources. A data source can include one file, all files in a directory, or data from your company. Later, you can incorporate these data sources as needed when the AI requires this data to complete a certain task.")
|
||||
}
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3" Wrap="Wrap.Wrap">
|
||||
<MudTextSwitch Label="@T("Automatic local data source refresh")" Value="@this.SettingsManager.ConfigurationData.App.DataSourceIndexing.AutomaticRefresh" LabelOn="@T("Local data sources refresh when files change.")" LabelOff="@T("Local data sources refresh only when triggered manually.")" ValueChanged="@this.AutomaticRefreshChanged"/>
|
||||
<MudTextSwitch Label="@T("Automatic local data source refresh")" Value="@this.SettingsManager.ConfigurationData.App.DataSourceIndexing.AutomaticRefresh" LabelOn="@(this.AreMailboxesEnabled ? T("Local data sources refresh when files change, mailboxes every 16 minutes.") : T("Local data sources refresh when files change."))" LabelOff="@T("Local data sources refresh only when triggered manually.")" ValueChanged="@this.AutomaticRefreshChanged"/>
|
||||
<MudTooltip Text="@T("Refresh all")">
|
||||
<MudIconButton Color="Color.Primary" Icon="@Icons.Material.Filled.Sync" Disabled="@(!this.HasRefreshableDataSources())" OnClick="@this.RefreshAllDataSources"/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
|
||||
@{ var embeddingStatuses = this.DataSourceEmbeddingService.GetStatuses().ToDictionary(status => status.DataSourceId, StringComparer.OrdinalIgnoreCase); }
|
||||
<MudTable Items="@this.SettingsManager.ConfigurationData.DataSources" Hover="@true" Class="border-dashed border rounded-lg">
|
||||
<MudTable T="IDataSourceBase" Items="@this.ConfiguredDataSources" Hover="@true" Class="border-dashed border rounded-lg">
|
||||
<ColGroup>
|
||||
<col style="width: 3em;"/>
|
||||
<col/>
|
||||
@@ -28,7 +35,7 @@
|
||||
<MudTh>@T("Name")</MudTh>
|
||||
<MudTh>@T("Type")</MudTh>
|
||||
<MudTh>@T("Embedding")</MudTh>
|
||||
<MudTh>@T("Indexed files")</MudTh>
|
||||
<MudTh>@T("Indexed")</MudTh>
|
||||
<MudTh>@T("Actions")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
@@ -38,12 +45,12 @@
|
||||
<MudTd Style="white-space: nowrap;">@context.Type.GetDisplayName()</MudTd>
|
||||
<MudTd Style="white-space: nowrap;">@this.GetEmbeddingName(context)</MudTd>
|
||||
<MudTd>
|
||||
@if (context is IInternalDataSource)
|
||||
@if (context is IIndexedDataSource)
|
||||
{
|
||||
<MudTooltip Text="@this.GetIndexingStatusTooltip(embeddingStatus)">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Circle" Color="@GetIndexingStatusColor(embeddingStatus)" Size="Size.Small" Style="width: 0.75rem; height: 0.75rem; flex-shrink: 0;"/>
|
||||
<MudText Typo="Typo.body2">@(embeddingStatus is null ? T("Not available") : string.Format(T("{0} of {1}"), embeddingStatus.IndexedFiles, embeddingStatus.TotalFiles))</MudText>
|
||||
<MudText Typo="Typo.body2">@(embeddingStatus is null ? T("Not available") : string.Format(T("{0} of {1}"), embeddingStatus.IndexedDocuments.CompactCount(), embeddingStatus.TotalDocuments.CompactCount()))</MudText>
|
||||
</MudStack>
|
||||
</MudTooltip>
|
||||
}
|
||||
@@ -56,9 +63,12 @@
|
||||
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.NoWrap">
|
||||
<MudTooltip Text="@T("Information")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Outlined.Info" OnClick="@(() => this.ShowInformation(context))"/>
|
||||
</MudTooltip>
|
||||
@if (context is IDataSource dataSource)
|
||||
{
|
||||
<MudTooltip Text="@T("Information")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Outlined.Info" OnClick="@(() => this.ShowInformation(dataSource))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@*
|
||||
Outside the two branches below on purpose: an index which cannot be read is a
|
||||
matter of this machine, not of the configuration. Hiding the repair for a data
|
||||
@@ -80,7 +90,7 @@
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditDataSource(context))"/>
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditEntry(context))"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Refresh")">
|
||||
<MudIconButton Color="Color.Primary" Icon="@Icons.Material.Filled.Sync" Disabled="@(!this.CanRefreshDataSource(context))" OnClick="@(() => this.RefreshDataSource(context))"/>
|
||||
@@ -90,7 +100,7 @@
|
||||
<AdminExportButton OnClick="@(() => this.ExportDataSource(context))" />
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteDataSource(context))"/>
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteEntry(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
@@ -98,7 +108,7 @@
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
|
||||
@if (!this.ConfiguredDataSources.Any())
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mt-3">
|
||||
@T("No data sources configured yet.")
|
||||
@@ -115,4 +125,11 @@
|
||||
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_FILE))">
|
||||
@T("Local File")
|
||||
</MudMenuItem>
|
||||
@if (PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
@* Shown with a lock rather than left out, like the locked buttons for adding providers: *@
|
||||
<MudMenuItem OnClick="@this.AddMailbox" Disabled="@(!this.MayAddMailbox)" Icon="@(this.MayAddMailbox ? null : Icons.Material.Filled.Lock)">
|
||||
@(this.MayAddMailbox ? T("Mailbox (IMAP)") : T("Mailbox (IMAP), not allowed by your organization"))
|
||||
</MudMenuItem>
|
||||
}
|
||||
</MudMenu>
|
||||
@@ -1,6 +1,7 @@
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -26,8 +27,21 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
|
||||
|
||||
private readonly List<ConfigurationSelectData<string>> availableEmbeddingProviders = new();
|
||||
|
||||
/// <summary>
|
||||
/// The data sources and then the mailboxes, which the table shows together.
|
||||
/// </summary>
|
||||
private IEnumerable<IDataSourceBase> ConfiguredDataSources => this.SettingsManager.ConfigurationData.DataSources
|
||||
.Concat(this.SettingsManager.ConfigurationData.Mailboxes.Cast<IDataSourceBase>());
|
||||
|
||||
private bool AreMailboxesEnabled => PreviewFeatures.PRE_MAILBOXES_2026.IsEnabled(this.SettingsManager);
|
||||
|
||||
private bool MayAddMailbox => this.SettingsManager.ConfigurationData.App.AllowUserToAddMailbox;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -77,7 +91,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
if (status is null || status.State is DataSourceEmbeddingState.IDLE or DataSourceEmbeddingState.QUEUED or DataSourceEmbeddingState.RUNNING)
|
||||
return Color.Warning;
|
||||
|
||||
return status.State is DataSourceEmbeddingState.FAILED || status.FailedFiles > 0
|
||||
return status.State is DataSourceEmbeddingState.FAILED || status.FailedDocuments > 0
|
||||
? Color.Error
|
||||
: Color.Success;
|
||||
}
|
||||
@@ -95,17 +109,18 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
if (status is null)
|
||||
return T("Waiting for indexing status");
|
||||
|
||||
if (status.PermanentlySkippedFiles == 0)
|
||||
// Only files are skipped for want of readable text. A mail is read from the server, never from a file:
|
||||
if (status.PermanentlySkippedDocuments == 0 || status.DataSourceType is DataSourceType.MAILBOX)
|
||||
return status.StateLabel;
|
||||
|
||||
return $"{status.StateLabel} — {string.Format(T("{0} files were skipped because they contain no readable text. AI Studio reads them again once they change."), status.PermanentlySkippedFiles)}";
|
||||
return $"{status.StateLabel} — {string.Format(T("{0} files were skipped because they contain no readable text. AI Studio reads them again once they change."), status.PermanentlySkippedDocuments)}";
|
||||
}
|
||||
|
||||
private string GetEmbeddingName(IDataSource dataSource)
|
||||
private string GetEmbeddingName(IDataSourceBase dataSource)
|
||||
{
|
||||
if(dataSource is IInternalDataSource internalDataSource)
|
||||
if(dataSource is IIndexedDataSource indexedDataSource)
|
||||
{
|
||||
var matchedEmbedding = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == internalDataSource.EmbeddingId);
|
||||
var matchedEmbedding = this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == indexedDataSource.EmbeddingId);
|
||||
if(matchedEmbedding == default)
|
||||
return T("No valid embedding");
|
||||
|
||||
@@ -118,14 +133,14 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
return T("Unknown");
|
||||
}
|
||||
|
||||
private bool CanRefreshDataSource(IDataSource dataSource)
|
||||
private bool CanRefreshDataSource(IDataSourceBase dataSource)
|
||||
{
|
||||
return this.DataSourceEmbeddingService.CanRefreshDataSource(dataSource);
|
||||
}
|
||||
|
||||
private bool HasRefreshableDataSources()
|
||||
{
|
||||
return this.SettingsManager.ConfigurationData.DataSources.Any(this.CanRefreshDataSource);
|
||||
return this.ConfiguredDataSources.Any(this.CanRefreshDataSource);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
@@ -134,12 +149,12 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
/// every internal data source regardless of state, and singling this one out would say more
|
||||
/// about the state than that button ever has.
|
||||
/// </remarks>
|
||||
private bool CanRepairDataSource(IDataSource dataSource)
|
||||
private bool CanRepairDataSource(IDataSourceBase dataSource)
|
||||
{
|
||||
return this.DataSourceEmbeddingService.NeedsIndexRepair(dataSource);
|
||||
}
|
||||
|
||||
private async Task RepairDataSource(IDataSource dataSource)
|
||||
private async Task RepairDataSource(IDataSourceBase dataSource)
|
||||
{
|
||||
if (!this.CanRepairDataSource(dataSource))
|
||||
return;
|
||||
@@ -161,7 +176,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task RefreshDataSource(IDataSource dataSource)
|
||||
private async Task RefreshDataSource(IDataSourceBase dataSource)
|
||||
{
|
||||
if (!this.CanRefreshDataSource(dataSource))
|
||||
return;
|
||||
@@ -235,7 +250,7 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportDataSource(IDataSource dataSource)
|
||||
private async Task ExportDataSource(IDataSourceBase dataSource)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
@@ -442,6 +457,112 @@ public partial class DataSourceManagement : MSGComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edits a row of the table, which is either a data source or a mailbox.
|
||||
/// </summary>
|
||||
private Task EditEntry(IDataSourceBase entry) => entry switch
|
||||
{
|
||||
DataSourceMailbox mailbox => this.EditMailbox(mailbox),
|
||||
IDataSource dataSource => this.EditDataSource(dataSource),
|
||||
|
||||
_ => Task.CompletedTask,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a row of the table, which is either a data source or a mailbox.
|
||||
/// </summary>
|
||||
private Task DeleteEntry(IDataSourceBase entry) => entry switch
|
||||
{
|
||||
DataSourceMailbox mailbox => this.DeleteMailbox(mailbox),
|
||||
IDataSource dataSource => this.DeleteDataSource(dataSource),
|
||||
|
||||
_ => Task.CompletedTask,
|
||||
};
|
||||
|
||||
private async Task AddMailbox()
|
||||
{
|
||||
if (!this.MayAddMailbox)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<DataSourceMailboxDialog>
|
||||
{
|
||||
{ x => x.IsEditing, false },
|
||||
{ x => x.AvailableEmbeddings, this.availableEmbeddingProviders }
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<DataSourceMailboxDialog>(T("Add Mailbox"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var mailbox = (DataSourceMailbox)dialogResult.Data!;
|
||||
|
||||
//
|
||||
// The dialog refuses to add a mailbox once the organization rules them out, but the rule
|
||||
// may change while it stores the password. Then the password goes again, as far as possible:
|
||||
//
|
||||
if (!this.MayAddMailbox)
|
||||
{
|
||||
await this.RustService.DeleteSecret(mailbox, SecretStoreType.DATA_SOURCE);
|
||||
return;
|
||||
}
|
||||
|
||||
mailbox = mailbox with { Num = this.SettingsManager.ConfigurationData.NextDataSourceNum++ };
|
||||
|
||||
this.SettingsManager.ConfigurationData.Mailboxes.Add(mailbox);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.DataSourceEmbeddingService.QueueDataSourceAsync(mailbox);
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task EditMailbox(DataSourceMailbox mailbox)
|
||||
{
|
||||
if (await MailboxEditing.EditAsync(this.DialogService, this.SettingsManager, this.DataSourceEmbeddingService, mailbox.Id))
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a mailbox from AI Studio: its settings, its password, its index, and its recorded sign-in failure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sign-in failure is deleted on its own, since it outlives the index on purpose: repairing
|
||||
/// an index must not give the server a new attempt with a password it already refused.
|
||||
/// </remarks>
|
||||
private async Task DeleteMailbox(DataSourceMailbox mailbox)
|
||||
{
|
||||
if (mailbox.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Are you sure you want to delete the mailbox '{0}'? Your mails stay on the server as they are. AI Studio only deletes its index of them and the stored password."), mailbox.Name) },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Delete Mailbox"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var deleteSecretResponse = await this.RustService.DeleteSecret(mailbox, SecretStoreType.DATA_SOURCE);
|
||||
if (!deleteSecretResponse.Success)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Delete Mailbox"),
|
||||
string.Format(T("The password of this mailbox could not be deleted from the operating system, so the mailbox was kept. The issue was: {0}"), deleteSecretResponse.Issue),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.SettingsManager.ConfigurationData.Mailboxes.Remove(mailbox);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.DataSourceEmbeddingService.RemoveDataSourceAsync(mailbox);
|
||||
|
||||
var indexStore = await this.DatabaseClientProvider.GetIndexStoreAsync();
|
||||
await indexStore.ClearMailboxAuthFailureAsync(mailbox.Id, CancellationToken.None);
|
||||
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ShowInformation(IDataSource dataSource)
|
||||
{
|
||||
switch (dataSource)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@using AIStudio.Tools.Mail
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudTreeView T="string" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" SelectedValue="@this.SelectedFolder" SelectedValueChanged="@this.SelectFolder" Hover="@true" Dense="@true" Disabled="@this.Disabled" Class="mb-3">
|
||||
<ItemTemplate Context="item">
|
||||
<MudTreeViewItem T="string" Value="@item.Value" Text="@item.Text" Icon="@item.Icon" @bind-Expanded="@item.Expanded" CanExpand="@item.Expandable" Items="@item.Children" Disabled="@(!this.CanBeSelected(item.Value))"/>
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
|
||||
@if (this.CreateFolder is not null && !this.Disabled)
|
||||
{
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<MudTextField
|
||||
T="string"
|
||||
@bind-Text="@this.newFolderName"
|
||||
Label="@T("New folder")"
|
||||
Immediate="@true"
|
||||
MaxLength="@ImapMailboxConnector.MAX_FOLDER_NAME_LENGTH"
|
||||
HelperText="@this.NewFolderHelperText"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.CreateNewFolder"
|
||||
AdornmentColor="Color.Info"
|
||||
Variant="Variant.Outlined"/>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.CreateNewFolder" Disabled="@(!this.CanCreateFolder)" OnClick="@this.CreateNewFolder">
|
||||
@T("Create")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using AIStudio.Tools.Mail;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the folders of a mailbox as the server lists them, picks one of them, and creates new ones.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The whole mailbox is the root of the tree and stands for no folder at all, i.e., an empty full
|
||||
/// name. The trash and the junk folder cannot be picked: the synchronization leaves them out, so a
|
||||
/// mailbox restricted to one of them would never hold a mail.
|
||||
/// </remarks>
|
||||
public partial class MailFolderPicker : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The folders of the mailbox, as the server lists them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The tree is built anew whenever another list arrives, so pass a new list instead of changing this one.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public IReadOnlyList<MailServerFolder> Folders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The full name of the picked folder, or empty for the whole mailbox.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SelectedFolder { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> SelectedFolderChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a folder on the server, from the full name of the folder to create it in (empty at
|
||||
/// the top level) and the name of the new folder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns the new folder, or null when the server did not create it, in which case the caller
|
||||
/// tells the user why. Without it, there is no way to create a folder here.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public Func<string, string, Task<MailServerFolder?>>? CreateFolder { get; set; }
|
||||
|
||||
private IReadOnlyCollection<TreeItemData<string>> treeItems = [];
|
||||
private IReadOnlyList<MailServerFolder>? treeFolders;
|
||||
private Dictionary<string, MailServerFolder> foldersByName = new(StringComparer.Ordinal);
|
||||
private string newFolderName = string.Empty;
|
||||
private bool isCreatingFolder;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!ReferenceEquals(this.treeFolders, this.Folders))
|
||||
{
|
||||
this.treeFolders = this.Folders;
|
||||
this.foldersByName = IndexByName(this.Folders);
|
||||
this.treeItems = BuildTree(this.Folders, this.SelectedFolder, T("Whole mailbox"));
|
||||
}
|
||||
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private char NewFolderSeparator => this.foldersByName.TryGetValue(this.SelectedFolder, out var parent)
|
||||
? parent.DirectorySeparator
|
||||
: this.Folders.FirstOrDefault()?.DirectorySeparator ?? '\0';
|
||||
|
||||
private bool IsNewFolderNameValid => ImapMailboxConnector.IsValidFolderName(this.newFolderName.Trim(), this.NewFolderSeparator);
|
||||
|
||||
private bool CanCreateFolder => !this.isCreatingFolder && this.IsNewFolderNameValid;
|
||||
|
||||
private string NewFolderHelperText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this.newFolderName) && !this.IsNewFolderNameValid)
|
||||
return T("This name is too long, or it contains a character the server reserves for folder paths.");
|
||||
|
||||
return string.IsNullOrEmpty(this.SelectedFolder)
|
||||
? T("Creates the folder on the server right away, at the top level of the mailbox.")
|
||||
: string.Format(T("Creates the folder on the server right away, inside '{0}'."), this.SelectedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanBeSelected(string? fullName)
|
||||
{
|
||||
if (fullName is null)
|
||||
return false;
|
||||
|
||||
if (fullName.Length == 0)
|
||||
return true;
|
||||
|
||||
return this.foldersByName.TryGetValue(fullName, out var folder) && CanBeSelected(folder);
|
||||
}
|
||||
|
||||
private async Task SelectFolder(string? fullName)
|
||||
{
|
||||
if (!this.CanBeSelected(fullName) || fullName == this.SelectedFolder)
|
||||
return;
|
||||
|
||||
this.SelectedFolder = fullName!;
|
||||
await this.SelectedFolderChanged.InvokeAsync(this.SelectedFolder);
|
||||
}
|
||||
|
||||
private async Task CreateNewFolder()
|
||||
{
|
||||
if (this.CreateFolder is null || !this.CanCreateFolder)
|
||||
return;
|
||||
|
||||
this.isCreatingFolder = true;
|
||||
try
|
||||
{
|
||||
var createdFolder = await this.CreateFolder(this.SelectedFolder, this.newFolderName.Trim());
|
||||
if (createdFolder is null)
|
||||
return;
|
||||
|
||||
this.newFolderName = string.Empty;
|
||||
this.SelectedFolder = createdFolder.FullName;
|
||||
await this.SelectedFolderChanged.InvokeAsync(this.SelectedFolder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isCreatingFolder = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a folder can be picked, cf. the remarks of this component.
|
||||
/// </summary>
|
||||
internal static bool CanBeSelected(MailServerFolder folder) => folder.SpecialUse is not (MailFolderSpecialUse.TRASH or MailFolderSpecialUse.JUNK);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the tree of folders below the whole mailbox, with the way to the picked folder expanded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A folder whose parent the server did not list goes to the top level, so that nothing listed
|
||||
/// gets lost. Each level starts with the inbox, and the other folders follow by name.
|
||||
/// </remarks>
|
||||
/// <param name="folders">The folders as the server lists them.</param>
|
||||
/// <param name="selectedFolder">The full name of the picked folder, or empty for the whole mailbox.</param>
|
||||
/// <param name="wholeMailboxText">The text of the root, which stands for the whole mailbox.</param>
|
||||
/// <returns>The root of the tree, as the only item.</returns>
|
||||
internal static List<TreeItemData<string>> BuildTree(IReadOnlyList<MailServerFolder> folders, string selectedFolder, string wholeMailboxText)
|
||||
{
|
||||
var foldersByName = IndexByName(folders);
|
||||
var childrenByParent = foldersByName.Values
|
||||
.ToLookup(folder => foldersByName.ContainsKey(folder.ParentFullName) && folder.ParentFullName != folder.FullName ? folder.ParentFullName : string.Empty, StringComparer.Ordinal);
|
||||
|
||||
//
|
||||
// Every folder on the way to the picked one is expanded. Should a server list two folders as
|
||||
// each other's parent, the way ends where it meets itself:
|
||||
//
|
||||
var expandedFolders = new HashSet<string>(StringComparer.Ordinal);
|
||||
var current = selectedFolder;
|
||||
while (foldersByName.TryGetValue(current, out var folder) && expandedFolders.Add(folder.ParentFullName))
|
||||
current = folder.ParentFullName;
|
||||
|
||||
var topLevelFolders = BuildChildren(string.Empty);
|
||||
return
|
||||
[
|
||||
new TreeItemData<string>
|
||||
{
|
||||
Value = string.Empty,
|
||||
Text = wholeMailboxText,
|
||||
Icon = Icons.Material.Filled.Mail,
|
||||
Expanded = true,
|
||||
Expandable = topLevelFolders.Count > 0,
|
||||
Children = topLevelFolders,
|
||||
},
|
||||
];
|
||||
|
||||
List<TreeItemData<string>> BuildChildren(string parentFullName) => childrenByParent[parentFullName]
|
||||
.OrderByDescending(folder => folder.IsInbox)
|
||||
.ThenBy(folder => folder.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(folder => folder.FullName, StringComparer.Ordinal)
|
||||
.Select(folder =>
|
||||
{
|
||||
var children = BuildChildren(folder.FullName);
|
||||
return new TreeItemData<string>
|
||||
{
|
||||
Value = folder.FullName,
|
||||
Text = folder.Name,
|
||||
Icon = GetIcon(folder),
|
||||
Expanded = expandedFolders.Contains(folder.FullName),
|
||||
Expandable = children.Count > 0,
|
||||
Children = children,
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The empty full name belongs to the whole mailbox, so a folder listed under it would become
|
||||
/// its own child.
|
||||
/// </remarks>
|
||||
private static Dictionary<string, MailServerFolder> IndexByName(IEnumerable<MailServerFolder> folders)
|
||||
{
|
||||
var foldersByName = new Dictionary<string, MailServerFolder>(StringComparer.Ordinal);
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
if (folder.FullName.Length > 0)
|
||||
foldersByName.TryAdd(folder.FullName, folder);
|
||||
}
|
||||
|
||||
return foldersByName;
|
||||
}
|
||||
|
||||
private static string GetIcon(MailServerFolder folder)
|
||||
{
|
||||
if (folder.IsInbox)
|
||||
return Icons.Material.Filled.Inbox;
|
||||
|
||||
return folder.SpecialUse switch
|
||||
{
|
||||
MailFolderSpecialUse.ALL => Icons.Material.Filled.AllInbox,
|
||||
MailFolderSpecialUse.ARCHIVE => Icons.Material.Filled.Archive,
|
||||
MailFolderSpecialUse.DRAFTS => Icons.Material.Filled.Drafts,
|
||||
MailFolderSpecialUse.FLAGGED => Icons.Material.Filled.Flag,
|
||||
MailFolderSpecialUse.IMPORTANT => Icons.Material.Filled.LabelImportant,
|
||||
MailFolderSpecialUse.JUNK => Icons.Material.Filled.Report,
|
||||
MailFolderSpecialUse.SENT => Icons.Material.Filled.Send,
|
||||
MailFolderSpecialUse.TRASH => Icons.Material.Filled.Delete,
|
||||
|
||||
_ => Icons.Material.Filled.Folder,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,10 @@ public partial class ManagedToolsWarning : MSGComponentBase
|
||||
if (this.ToolIds.Count is 0 || !this.SettingsManager.AreToolsEnabled())
|
||||
return [];
|
||||
|
||||
var collectionIds = this.ToolRegistry.NormalizeSelection(this.ToolIds);
|
||||
return this.availableTools
|
||||
.Where(x => this.ToolIds.Contains(x.Definition.Id) && x.IsActive && !x.ConfigurationState.IsConfigured)
|
||||
.Select(x => x.Implementation.GetDisplayName())
|
||||
.Where(x => collectionIds.Contains(x.Id) && x.IsActive && !x.ConfigurationState.IsConfigured)
|
||||
.Select(x => x.DisplayName)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -82,10 +83,11 @@ public partial class ManagedToolsWarning : MSGComponentBase
|
||||
? ConfidenceLevel.NONE
|
||||
: this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).Level;
|
||||
|
||||
var collectionIds = this.ToolRegistry.NormalizeSelection(this.ToolIds);
|
||||
return this.availableTools
|
||||
.Where(x => this.ToolIds.Contains(x.Definition.Id) && x.IsActive)
|
||||
.Where(x => collectionIds.Contains(x.Id) && x.IsActive)
|
||||
.Where(x => !ToolSelectionRules.IsProviderConfidenceAllowed(providerConfidence, x.MinimumProviderConfidence))
|
||||
.Select(x => x.Implementation.GetDisplayName())
|
||||
.Select(x => x.DisplayName)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudIcon Icon="@context.Implementation.Icon" Color="Color.Info" />
|
||||
<MudIcon Icon="@context.Icon" Color="Color.Info" />
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudText Typo="Typo.body1">@context.Implementation.GetDisplayName()</MudText>
|
||||
<MudText Typo="Typo.body1">@context.DisplayName</MudText>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudText Typo="Typo.body2">@context.Implementation.GetDescription()</MudText>
|
||||
<MudText Typo="Typo.body2">@context.Description</MudText>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)" Disabled="@this.IsToolConfidenceManaged()">
|
||||
@@ -55,9 +55,9 @@
|
||||
<MudTd>
|
||||
<MudStack Row="true" Wrap="Wrap.NoWrap" AlignItems="AlignItems.Center" Spacing="0">
|
||||
<MudTooltip Text="@T("Settings")">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(context.Definition.Id))" aria-label="@T("Settings")" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(context))" aria-label="@T("Settings")" />
|
||||
</MudTooltip>
|
||||
<AdminExportButton OnClick="@(() => this.OpenExport(context.Definition.Id))" />
|
||||
<AdminExportButton OnClick="@(() => this.OpenExport(context))" />
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
|
||||
@@ -21,11 +21,11 @@ public partial class SettingsPanelTools : SettingsPanelBase
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task OpenSettings(string toolId)
|
||||
private async Task OpenSettings(ToolCatalogItem item)
|
||||
{
|
||||
var parameters = new DialogParameters<ToolSettingsDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
{ x => x.CollectionId, item.Id },
|
||||
};
|
||||
|
||||
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
@@ -34,14 +34,14 @@ public partial class SettingsPanelTools : SettingsPanelBase
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OpenExport(string toolId)
|
||||
private async Task OpenExport(ToolCatalogItem item)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
var parameters = new DialogParameters<ToolSettingsExportDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
{ x => x.CollectionId, item.Id },
|
||||
};
|
||||
|
||||
await this.DialogService.ShowAsync<ToolSettingsExportDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
@@ -54,13 +54,18 @@ public partial class SettingsPanelTools : SettingsPanelBase
|
||||
_ => string.Format(this.T("Missing required settings: {0}"), string.Join(", ", item.ConfigurationState.MissingRequiredFields.Select(fieldName => this.GetFieldDisplayName(item, fieldName))))
|
||||
};
|
||||
|
||||
/// <remarks>
|
||||
/// The missing fields are those of the first tool whose settings are incomplete, see
|
||||
/// ToolCatalogItem.ConfigurationState, so that tool names them.
|
||||
/// </remarks>
|
||||
private string GetFieldDisplayName(ToolCatalogItem item, string fieldName)
|
||||
{
|
||||
var fieldDefinition = item.Definition.SettingsSchema.Properties.GetValueOrDefault(fieldName);
|
||||
if (fieldDefinition is null)
|
||||
var tool = item.Tools.FirstOrDefault(tool => !tool.ConfigurationState.IsConfigured);
|
||||
var fieldDefinition = tool?.Definition.SettingsSchema.Properties.GetValueOrDefault(fieldName);
|
||||
if (tool is null || fieldDefinition is null)
|
||||
return fieldName;
|
||||
|
||||
return item.Implementation.GetSettingsFieldLabel(fieldName, fieldDefinition);
|
||||
return tool.Implementation.GetSettingsFieldLabel(fieldName, fieldDefinition);
|
||||
}
|
||||
|
||||
private IEnumerable<ConfidenceLevel> GetSelectableConfidenceLevels() =>
|
||||
@@ -85,7 +90,7 @@ public partial class SettingsPanelTools : SettingsPanelBase
|
||||
|
||||
private async Task ChangeMinimumProviderConfidence(ToolCatalogItem item, ConfidenceLevel confidenceLevel)
|
||||
{
|
||||
this.SettingsManager.SetMinimumProviderConfidenceForTool(item.Definition.Id, confidenceLevel, item.Definition.MinimumProviderConfidence);
|
||||
this.ToolRegistry.SetMinimumProviderConfidence(item.Id, confidenceLevel);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
|
||||
@@ -26,6 +26,13 @@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FolderOpen" Size="Size.Small" OnClick="@(() => this.ShowInFileManager(document))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else if (entry.IsMail)
|
||||
{
|
||||
@* Nothing opens a mail by its address yet, so a link would lead nowhere. *@
|
||||
<MudText Typo="Typo.body1" Inline="true">
|
||||
@entry.Title
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudLink Href="@entry.Link" Target="_blank" Typo="Typo.body1">
|
||||
|
||||
@@ -79,7 +79,7 @@ public partial class SourcesList : MSGComponentBase
|
||||
foreach (var numberedSource in group.Sources)
|
||||
{
|
||||
var document = numberedSource.Source.TryGetDocumentLocation(out var location) ? location : (SourceDocumentLocation?)null;
|
||||
entries.Add(new(numberedSource.Number, numberedSource.Source.Title, numberedSource.Source.URL, document));
|
||||
entries.Add(new(numberedSource.Number, numberedSource.Source.Title, numberedSource.Source.URL, document, numberedSource.Source.IsMailSource()));
|
||||
}
|
||||
|
||||
this.groups.Add(new(group.Heading, entries));
|
||||
@@ -160,5 +160,6 @@ public partial class SourcesList : MSGComponentBase
|
||||
/// <param name="Title">The title of the source.</param>
|
||||
/// <param name="Link">The address of the source, which a web source is opened by.</param>
|
||||
/// <param name="Document">The document the source names, or null when it names none.</param>
|
||||
private readonly record struct SourceEntry(int Number, string Title, string Link, SourceDocumentLocation? Document);
|
||||
/// <param name="IsMail">Whether the source names a mail, which is shown as text.</param>
|
||||
private readonly record struct SourceEntry(int Number, string Title, string Link, SourceDocumentLocation? Document, bool IsMail);
|
||||
}
|
||||
@@ -38,17 +38,17 @@ public partial class ToolDefaultsConfiguration : MSGComponentBase
|
||||
this.Component is not AIStudio.Tools.Components.CHAT &&
|
||||
!this.SettingsManager.IsToolSelectionVisible(this.Component);
|
||||
|
||||
private bool IsToolDisabled(string toolId) => !this.SettingsManager.IsToolActive(toolId);
|
||||
private bool IsToolDisabled(string toolId) => !this.ToolRegistry.IsToolActive(toolId);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.DisplayName, x.Id))
|
||||
.ToList();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private HashSet<string> GetSelectedValues() => this.SettingsManager.GetDefaultToolIds(this.Component);
|
||||
private HashSet<string> GetSelectedValues() => this.ToolRegistry.GetDefaultToolIds(this.Component);
|
||||
|
||||
private void UpdateSelection(HashSet<string> values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..ToolSelectionRules.NormalizeSelection(values)];
|
||||
private void UpdateSelection(HashSet<string> values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..this.ToolRegistry.NormalizeSelection(values)];
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="tool-selection-rows">
|
||||
@foreach (var item in this.catalog)
|
||||
{
|
||||
var isSelected = this.SelectedToolIds.Contains(item.Definition.Id);
|
||||
var isSelected = this.SelectedToolIds.Contains(item.Id);
|
||||
var warningText = this.GetWarningText(item);
|
||||
<div class="tool-selection-row">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1">
|
||||
@@ -66,19 +66,19 @@
|
||||
box and would otherwise swallow the clicks landing in that strip.
|
||||
*@
|
||||
<MudCheckBox T="bool" Size="Size.Small" Dense="@true" Color="Color.Primary" Value="@isSelected" ReadOnly="@true" Disabled="@this.IsRowDisabled(item)" Style="pointer-events: none;" />
|
||||
<MudIcon Icon="@item.Implementation.Icon" Color="Color.Info" />
|
||||
<MudIcon Icon="@item.Icon" Color="Color.Info" />
|
||||
@if (!item.IsActive)
|
||||
{
|
||||
<MudTooltip Text="@T("This tool has been disabled by your organization.")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@item.Implementation.GetDescription()">
|
||||
<MudText Typo="Typo.body1">@item.Implementation.GetDisplayName()</MudText>
|
||||
<MudTooltip Text="@item.Description">
|
||||
<MudText Typo="Typo.body1">@item.DisplayName</MudText>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" Size="Size.Small" OnClick="@(async () => await this.OpenSettings(item.Definition.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings" Size="Size.Small" OnClick="@(async () => await this.OpenSettings(item))" />
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(warningText))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ToolCallingSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -14,6 +15,17 @@ public partial class ToolSelection : MSGComponentBase
|
||||
[Parameter]
|
||||
public required AIStudio.Settings.Provider LLMProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where the chat of this selection may still send data, because of the mailboxes it read from.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The selection shows a tool this keeps back as unavailable, with the reason, the same way it
|
||||
/// does for a provider with too little confidence. Keeping the tool out of the request is the
|
||||
/// business of the tool registry and the tool executor; this only explains it.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public required OutboundDataRequirement RequiredOutboundDataRestriction { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public HashSet<string> SelectedToolIds { get; set; } = [];
|
||||
|
||||
@@ -37,7 +49,7 @@ public partial class ToolSelection : MSGComponentBase
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(this.SelectedToolIds);
|
||||
this.SelectedToolIds = this.ToolRegistry.NormalizeSelection(this.SelectedToolIds);
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
@@ -76,10 +88,11 @@ public partial class ToolSelection : MSGComponentBase
|
||||
/// <remarks>
|
||||
/// The switch and the row click share this, so both agree on when a tool is out of reach: the
|
||||
/// organization disabled it, it is not configured, the provider lacks the confidence it needs,
|
||||
/// a response is running, or the model cannot call tools in the first place.
|
||||
/// the chat read a mailbox which keeps the tool back, a response is running, or the model
|
||||
/// cannot call tools in the first place.
|
||||
/// </remarks>
|
||||
private bool IsRowDisabled(ToolCatalogItem item) => !item.IsActive || !item.ConfigurationState.IsConfigured || this.IsBlockedByProviderConfidence(item) ||
|
||||
this.Disabled || !this.SupportsTools;
|
||||
this.IsBlockedByOutboundDataRestriction(item) || this.Disabled || !this.SupportsTools;
|
||||
|
||||
/// <summary>
|
||||
/// Switches a tool when the user clicks anywhere in its row.
|
||||
@@ -94,21 +107,21 @@ public partial class ToolSelection : MSGComponentBase
|
||||
if (this.IsRowDisabled(item))
|
||||
return;
|
||||
|
||||
await this.ChangeSelection(item.Definition.Id, !this.SelectedToolIds.Contains(item.Definition.Id));
|
||||
await this.ChangeSelection(item.Id, !this.SelectedToolIds.Contains(item.Id));
|
||||
}
|
||||
|
||||
private async Task ChangeSelection(string toolId, bool isSelected)
|
||||
private async Task ChangeSelection(string collectionId, bool isSelected)
|
||||
{
|
||||
if (isSelected && !this.SettingsManager.IsToolActive(toolId))
|
||||
if (isSelected && !this.ToolRegistry.IsToolActive(collectionId))
|
||||
return;
|
||||
|
||||
var updated = new HashSet<string>(this.SelectedToolIds, StringComparer.Ordinal);
|
||||
if (isSelected)
|
||||
updated.Add(toolId);
|
||||
updated.Add(collectionId);
|
||||
else
|
||||
updated.Remove(toolId);
|
||||
updated.Remove(collectionId);
|
||||
|
||||
updated = ToolSelectionRules.NormalizeSelection(updated);
|
||||
updated = this.ToolRegistry.NormalizeSelection(updated);
|
||||
this.SelectedToolIds = updated;
|
||||
await this.SelectedToolIdsChanged.InvokeAsync(updated);
|
||||
}
|
||||
@@ -118,6 +131,15 @@ public partial class ToolSelection : MSGComponentBase
|
||||
|
||||
private bool IsBlockedByProviderConfidence(ToolCatalogItem item) => !ToolSelectionRules.IsProviderConfidenceAllowed(this.ProviderConfidence, GetMinimumProviderConfidence(item));
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mailboxes the chat read from keep every tool of this entry back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only then is there nothing left to select. Should a mailbox keep back some tools of a
|
||||
/// collection, the others still run, and the request leaves out the rest on its own.
|
||||
/// </remarks>
|
||||
private bool IsBlockedByOutboundDataRestriction(ToolCatalogItem item) => item.Tools.All(tool => !ToolSelectionRules.IsOutboundDataAllowed(this.RequiredOutboundDataRestriction.Restriction, tool.Implementation));
|
||||
|
||||
private string? GetProviderConfidenceHint(ToolCatalogItem item)
|
||||
{
|
||||
if (!this.IsBlockedByProviderConfidence(item))
|
||||
@@ -138,7 +160,7 @@ public partial class ToolSelection : MSGComponentBase
|
||||
/// </remarks>
|
||||
private string GetWarningText(ToolCatalogItem item)
|
||||
{
|
||||
var warnings = new List<string>(3);
|
||||
var warnings = new List<string>(4);
|
||||
if (!item.ConfigurationState.IsConfigured)
|
||||
warnings.Add(string.IsNullOrWhiteSpace(item.ConfigurationState.Message) ? T("Required settings are missing. Configure this tool before enabling it.") : item.ConfigurationState.Message);
|
||||
|
||||
@@ -149,14 +171,17 @@ public partial class ToolSelection : MSGComponentBase
|
||||
if (!string.IsNullOrWhiteSpace(providerConfidenceHint))
|
||||
warnings.Add(providerConfidenceHint);
|
||||
|
||||
if (this.IsBlockedByOutboundDataRestriction(item))
|
||||
warnings.Add(this.RequiredOutboundDataRestriction.GetToolBlockedMessage(this.SettingsManager.ConfigurationData.Mailboxes));
|
||||
|
||||
return string.Join(' ', warnings);
|
||||
}
|
||||
|
||||
private async Task OpenSettings(string toolId)
|
||||
private async Task OpenSettings(ToolCatalogItem item)
|
||||
{
|
||||
var parameters = new DialogParameters<ToolSettingsDialog>
|
||||
{
|
||||
{ x => x.ToolId, toolId },
|
||||
{ x => x.CollectionId, item.Id },
|
||||
};
|
||||
|
||||
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
|
||||
|
||||
@@ -51,27 +51,27 @@ public partial class ToolSelectionField : MSGComponentBase
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// Like ToolSelection, the field shows the tools which will actually run, also when the
|
||||
// selection is read-only. See ToolSelectionRules.NormalizeSelection:
|
||||
this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(this.SelectedToolIds);
|
||||
// Like ToolSelection, the field shows the collections which will actually run, also when the
|
||||
// selection is read-only. See ToolRegistry.NormalizeSelection:
|
||||
this.SelectedToolIds = this.ToolRegistry.NormalizeSelection(this.SelectedToolIds);
|
||||
base.OnParametersSet();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.DisplayName, x.Id))
|
||||
.ToList();
|
||||
|
||||
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private bool IsToolLocked(string toolId) => !this.SettingsManager.IsToolActive(toolId);
|
||||
private bool IsToolLocked(string toolId) => !this.ToolRegistry.IsToolActive(toolId);
|
||||
|
||||
private async Task OptionChangedAsync(HashSet<string> updatedToolIds)
|
||||
{
|
||||
this.SelectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
|
||||
this.SelectedToolIds = this.ToolRegistry.NormalizeSelection(updatedToolIds);
|
||||
await this.SelectedToolIdsChanged.InvokeAsync(this.SelectedToolIds);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public partial class ToolSelectionField : MSGComponentBase
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.availableTools = (await this.ToolRegistry.GetCatalogAsync(this.Component))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.Implementation.GetDisplayName(), x.Definition.Id))
|
||||
.Select(x => new ConfigurationSelectData<string>(x.DisplayName, x.Id))
|
||||
.ToList();
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
Reference in new issue
Block a user