Erste Version des Tool Callings von Codex

This commit is contained in:
Peer Schütt 2026-04-09 15:37:53 +02:00
parent f024de8322
commit 447fe9d712
69 changed files with 1536 additions and 265 deletions

View File

@ -151,6 +151,11 @@
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
}
@if (this.SettingsManager.IsToolSelectionVisible(this.Component))
{
<ToolSelection Component="@this.Component" LLMProvider="@this.providerSettings" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isProcessing" />
}
<MudSpacer />
<HalluzinationReminder ContainerClass="my-0 ml-2"/>
</MudStack>

View File

@ -93,6 +93,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected ChatThread? chatThread;
protected IContent? lastUserPrompt;
protected CancellationTokenSource? cancellationTokenSource;
protected HashSet<string> selectedToolIds = [];
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
@ -124,6 +125,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(this.Component);
}
protected override async Task OnParametersSetAsync()
@ -223,6 +225,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
ChatId = Guid.NewGuid(),
Name = string.Format(this.TB("Assistant - {0}"), this.Title),
Blocks = [],
RuntimeComponent = this.Component,
};
}
@ -239,6 +242,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
ChatId = chatId,
Name = name,
Blocks = [],
RuntimeComponent = this.Component,
};
return chatId;
@ -250,6 +254,12 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
}
protected Task SelectedToolIdsChanged(HashSet<string> updatedToolIds)
{
this.selectedToolIds = updatedToolIds;
return Task.CompletedTask;
}
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false, params List<FileAttachment> attachments)
{
@ -297,6 +307,10 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
{
this.chatThread.Blocks.Add(this.resultingContentBlock);
this.chatThread.SelectedProvider = this.providerSettings.Id;
this.chatThread.RuntimeComponent = this.Component;
this.chatThread.RuntimeSelectedToolIds = this.SettingsManager.IsToolSelectionVisible(this.Component)
? [..this.selectedToolIds]
: [];
}
this.isProcessing = true;

View File

@ -1,8 +1,10 @@
using System.Globalization;
using System.Text.Json.Serialization;
using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Chat;
@ -79,6 +81,12 @@ public sealed record ChatThread
/// The content blocks of the chat thread.
/// </summary>
public List<ContentBlock> Blocks { get; init; } = [];
[JsonIgnore]
public AIStudio.Tools.Components RuntimeComponent { get; set; } = AIStudio.Tools.Components.CHAT;
[JsonIgnore]
public HashSet<string> RuntimeSelectedToolIds { get; set; } = [];
private bool allowProfile = true;
@ -287,4 +295,4 @@ public sealed record ChatThread
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
}
}
}

View File

@ -115,6 +115,59 @@
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
}
</div>
@if (this.Role is ChatRole.AI && !string.IsNullOrWhiteSpace(textContent.ToolRuntimeStatus.Message))
{
<MudAlert Dense="@true" Severity="Severity.Info" Variant="Variant.Outlined" Class="mt-4">
@textContent.ToolRuntimeStatus.Message
</MudAlert>
}
@if (this.Role is ChatRole.AI && textContent.ToolInvocations.Count > 0)
{
<MudText Typo="Typo.subtitle2" Class="mt-4 mb-2">
@string.Format(T("Tool Calls ({0})"), textContent.ToolInvocations.Count)
</MudText>
<MudExpansionPanels MultiExpansion="@true" Class="mt-4">
@foreach (var invocation in textContent.ToolInvocations.OrderBy(x => x.Order))
{
<ExpansionPanel HeaderIcon="@invocation.ToolIcon" HeaderText="@($"{invocation.Order}. {invocation.ToolName}")">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
<MudChip T="string" Color="@ContentBlockComponent.GetTraceColor(invocation.Status)" Size="Size.Small" Variant="Variant.Outlined">
@this.GetTraceStatusText(invocation)
</MudChip>
</MudStack>
@if (!string.IsNullOrWhiteSpace(invocation.StatusMessage))
{
<MudText Typo="Typo.body2" Color="Color.Warning" Class="mb-3">@invocation.StatusMessage</MudText>
}
<MudText Typo="Typo.subtitle2">@T("Arguments")</MudText>
@if (invocation.Arguments.Count == 0)
{
<MudText Typo="Typo.body2" Class="mb-3">@T("No arguments")</MudText>
}
else
{
<MudList T="string" Dense="@true" Class="mb-3">
@foreach (var argument in invocation.Arguments)
{
<MudListItem T="string">
<MudText Typo="Typo.body2"><strong>@argument.Key:</strong> @argument.Value</MudText>
</MudListItem>
}
</MudList>
}
<MudText Typo="Typo.subtitle2">@T("Result")</MudText>
<MudPaper Class="pa-3 mt-2">
<MudText Typo="Typo.body2" Style="white-space: pre-wrap;">@invocation.Result</MudText>
</MudPaper>
</ExpansionPanel>
}
</MudExpansionPanels>
}
}
}
}

View File

@ -1,6 +1,7 @@
using AIStudio.Components;
using AIStudio.Dialogs;
using AIStudio.Tools.Services;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Chat;
@ -199,6 +200,23 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
hash.Add(textValue.Length);
hash.Add(textValue.GetHashCode(StringComparison.Ordinal));
hash.Add(text.Sources.Count);
hash.Add(text.ToolInvocations.Count);
hash.Add(text.ToolRuntimeStatus.IsRunning);
hash.Add(text.ToolRuntimeStatus.Message);
foreach (var invocation in text.ToolInvocations)
{
hash.Add(invocation.Order);
hash.Add(invocation.ToolId);
hash.Add(invocation.Status);
hash.Add(invocation.StatusMessage);
hash.Add(invocation.Result);
hash.Add(invocation.Arguments.Count);
foreach (var argument in invocation.Arguments)
{
hash.Add(argument.Key);
hash.Add(argument.Value);
}
}
break;
case ContentImage image:
@ -216,6 +234,22 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
private static Color GetTraceColor(ToolInvocationTraceStatus status) => status switch
{
ToolInvocationTraceStatus.SUCCESS => Color.Success,
ToolInvocationTraceStatus.ERROR => Color.Error,
ToolInvocationTraceStatus.BLOCKED => Color.Warning,
_ => Color.Default,
};
private string GetTraceStatusText(ToolInvocationTrace trace) => trace.Status switch
{
ToolInvocationTraceStatus.SUCCESS => this.T("Executed"),
ToolInvocationTraceStatus.ERROR => this.T("Failed"),
ToolInvocationTraceStatus.BLOCKED => this.T("Blocked"),
_ => this.T("Unknown"),
};
private MudMarkdownStyling MarkdownStyling => new()
{
CodeBlock = { Theme = this.CodeColorPalette },

View File

@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.RAG.RAGProcesses;
using AIStudio.Tools.ToolCallingSystem;
namespace AIStudio.Chat;
@ -44,6 +45,11 @@ public sealed class ContentText : IContent
/// <inheritdoc />
public List<FileAttachment> FileAttachments { get; set; } = [];
public List<ToolInvocationTrace> ToolInvocations { get; set; } = [];
[JsonIgnore]
public ToolRuntimeStatus ToolRuntimeStatus { get; set; } = new();
/// <inheritdoc />
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default)
{
@ -145,6 +151,19 @@ public sealed class ContentText : IContent
IsStreaming = this.IsStreaming,
Sources = [..this.Sources],
FileAttachments = [..this.FileAttachments],
ToolInvocations = [..this.ToolInvocations.Select(x => new ToolInvocationTrace
{
Order = x.Order,
ToolId = x.ToolId,
ToolName = x.ToolName,
ToolIcon = x.ToolIcon,
ToolCallId = x.ToolCallId,
Status = x.Status,
WasExecuted = x.WasExecuted,
StatusMessage = x.StatusMessage,
Arguments = new Dictionary<string, string>(x.Arguments, StringComparer.Ordinal),
Result = x.Result,
})],
};
#endregion
@ -214,4 +233,4 @@ public sealed class ContentText : IContent
/// The text content.
/// </summary>
public string Text { get; set; } = string.Empty;
}
}

View File

@ -123,6 +123,8 @@
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
<ToolSelection Component="Components.CHAT" LLMProvider="@this.Provider" SelectedToolIds="@this.selectedToolIds" SelectedToolIdsChanged="@this.SelectedToolIdsChanged" Disabled="@this.isStreaming" />
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{

View File

@ -64,6 +64,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private bool mustLoadChat;
private LoadChat loadChat;
private bool autoSaveEnabled;
private HashSet<string> selectedToolIds = [];
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private Guid currentChatThreadId = Guid.Empty;
@ -91,6 +92,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Get the preselected chat template:
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT);
// Apply template's file attachments, if any:
foreach (var attachment in this.currentChatTemplate.FileAttachments)
@ -607,6 +609,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
using (this.cancellationTokenSource = new())
{
this.StateHasChanged();
this.ChatThread!.RuntimeComponent = Tools.Components.CHAT;
this.ChatThread.RuntimeSelectedToolIds = [..this.selectedToolIds];
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
@ -636,6 +640,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
}
private Task SelectedToolIdsChanged(HashSet<string> updatedToolIds)
{
this.selectedToolIds = updatedToolIds;
return Task.CompletedTask;
}
private async Task SaveThread()
{
@ -700,6 +710,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
this.selectedToolIds = this.SettingsManager.GetDefaultToolIds(Tools.Components.CHAT);
//
// Reset the LLM provider considering the user's settings:

View File

@ -0,0 +1,30 @@
@using AIStudio.Tools.ToolCallingSystem
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Build" HeaderText="@T("Tools")">
<MudText Typo="Typo.body1" Class="mb-4">
@T("Configure global settings for each tool. Tool defaults for chat and assistants are configured in the corresponding feature settings.")
</MudText>
<MudTable Items="@this.items" Hover="@true" Dense="@true">
<HeaderContent>
<MudTh>@T("Tool")</MudTh>
<MudTh>@T("State")</MudTh>
<MudTh>@T("Actions")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@context.Definition.Icon" />
<MudText Typo="Typo.body1">@context.Definition.DisplayName</MudText>
</MudStack>
</MudTd>
<MudTd>
@(context.ConfigurationState.IsConfigured ? T("Configured") : T("Configuration required"))
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(context.Definition.Id))" />
</MudTd>
</RowTemplate>
</MudTable>
</ExpansionPanel>

View File

@ -0,0 +1,34 @@
using AIStudio.Dialogs.Settings;
using AIStudio.Tools;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Settings;
public partial class SettingsPanelTools : SettingsPanelBase
{
[Inject]
private ToolRegistry ToolRegistry { get; init; } = null!;
private IReadOnlyList<ToolCatalogItem> items = [];
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
}
private async Task OpenSettings(string toolId)
{
var parameters = new DialogParameters<ToolSettingsDialog>
{
{ x => x.ToolId, toolId },
};
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
await dialog.Result;
this.items = await this.ToolRegistry.GetCatalogAsync(this.ToolRegistry.GetAllDefinitions());
this.StateHasChanged();
}
}

View File

@ -0,0 +1,12 @@
@using AIStudio.Tools
@using AIStudio.Tools.ToolCallingSystem
@inherits MSGComponentBase
@if (this.availableTools.Count > 0)
{
@if (this.Component is not Components.CHAT && this.IncludeVisibilityToggle)
{
<ConfigurationOption OptionDescription="@T("Show tool selection in this assistant?")" LabelOn="@T("Tool selection is visible")" LabelOff="@T("Tool selection is hidden")" State="@(() => this.SettingsManager.IsToolSelectionVisible(this.Component))" StateUpdate="@(value => this.SettingsManager.SetToolSelectionVisibility(this.Component, value))" />
}
<ConfigurationMultiSelect TData="string" OptionDescription="@this.OptionTitle" SelectedValues="@this.GetSelectedValues" Data="@this.availableTools" SelectionUpdate="@this.UpdateSelection" OptionHelp="@this.OptionHelp" />
}

View File

@ -0,0 +1,40 @@
using AIStudio.Settings;
using AIStudio.Tools;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ToolDefaultsConfiguration : MSGComponentBase
{
[Parameter]
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
[Parameter]
public bool IncludeVisibilityToggle { get; set; } = true;
[Inject]
private ToolRegistry ToolRegistry { get; init; } = null!;
private List<ConfigurationSelectData<string>> availableTools = [];
private string OptionTitle => this.Component is AIStudio.Tools.Components.CHAT ? this.T("Default tools for chat") : this.T("Default tools for this assistant");
private string OptionHelp => this.Component is AIStudio.Tools.Components.CHAT
? this.T("Choose which tools should be preselected for new chats.")
: this.T("Choose which tools should be preselected for new runs of this assistant.");
protected override void OnInitialized()
{
this.availableTools = this.ToolRegistry
.GetDefinitionsForComponent(this.Component)
.Select(x => new ConfigurationSelectData<string>(x.DisplayName, x.Id))
.ToList();
base.OnInitialized();
}
private HashSet<string> GetSelectedValues() => this.SettingsManager.GetDefaultToolIds(this.Component);
private void UpdateSelection(HashSet<string> values) => this.SettingsManager.ConfigurationData.Tools.DefaultToolIdsByComponent[this.Component.ToString()] = [..values];
}

View File

@ -0,0 +1,64 @@
@using AIStudio.Settings
@using AIStudio.Tools.ToolCallingSystem
@inherits MSGComponentBase
<div class="d-flex">
<MudTooltip Text="@T("Select tools")" Placement="Placement.Top">
<MudIconButton Icon="@Icons.Material.Filled.Build" Class="@this.PopoverButtonClasses" OnClick="@this.ToggleSelection"/>
</MudTooltip>
<MudPopover Open="@this.showSelection" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" DropShadow="@true" Class="border-solid border-4 rounded-lg">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">@T("Tool Selection")</MudText>
<MudSpacer />
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Style="min-width: 28em; max-height: 60vh; max-width: 48vw; overflow: auto;">
@if (!this.SupportsTools)
{
<MudText Typo="Typo.body1">@T("The selected provider or model does not support tool calling.")</MudText>
}
else if (this.Disabled)
{
<MudAlert Dense="@true" Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
@T("Tool changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.")
</MudAlert>
}
else if (this.catalog.Count == 0)
{
<MudText Typo="Typo.body1">@T("No tools are available in this context.")</MudText>
}
@if (this.SupportsTools && this.catalog.Count > 0)
{
@foreach (var item in this.catalog)
{
var isSelected = this.SelectedToolIds.Contains(item.Definition.Id);
var isConfigured = item.ConfigurationState.IsConfigured;
<MudPaper Class="pa-2 mb-2 border rounded-lg">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudSwitch T="bool" Value="@isSelected" ValueChanged="@(value => this.ChangeSelection(item.Definition.Id, value))" Disabled="@(!isConfigured || this.Disabled || !this.SupportsTools)" />
<MudIcon Icon="@item.Definition.Icon" />
<MudText Typo="Typo.body1">@item.Definition.DisplayName</MudText>
</MudStack>
<MudIconButton Icon="@Icons.Material.Filled.Settings" OnClick="@(async () => await this.OpenSettings(item.Definition.Id))" />
</MudStack>
@if (!isConfigured)
{
<MudText Typo="Typo.caption" Color="Color.Warning">@T("Required settings are missing. Configure this tool before enabling it.")</MudText>
}
</MudPaper>
}
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@this.Hide">@T("Close")</MudButton>
</MudCardActions>
</MudCard>
</MudPopover>
</div>

View File

@ -0,0 +1,78 @@
using AIStudio.Dialogs.Settings;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ToolSelection : MSGComponentBase
{
[Parameter]
public AIStudio.Tools.Components Component { get; set; } = AIStudio.Tools.Components.CHAT;
[Parameter]
public required AIStudio.Settings.Provider LLMProvider { get; set; }
[Parameter]
public HashSet<string> SelectedToolIds { get; set; } = [];
[Parameter]
public EventCallback<HashSet<string>> SelectedToolIdsChanged { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string PopoverButtonClasses { get; set; } = string.Empty;
[Inject]
private ToolRegistry ToolRegistry { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private bool showSelection;
private IReadOnlyList<ToolCatalogItem> catalog = [];
private bool SupportsTools =>
this.LLMProvider != AIStudio.Settings.Provider.NONE &&
this.LLMProvider.GetModelCapabilities().Contains(Capability.CHAT_COMPLETION_API) &&
this.LLMProvider.GetModelCapabilities().Contains(Capability.FUNCTION_CALLING);
private async Task ToggleSelection()
{
this.showSelection = !this.showSelection;
if (this.showSelection)
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
}
private void Hide() => this.showSelection = false;
private async Task ChangeSelection(string toolId, bool isSelected)
{
var updated = new HashSet<string>(this.SelectedToolIds, StringComparer.Ordinal);
if (isSelected)
updated.Add(toolId);
else
updated.Remove(toolId);
this.SelectedToolIds = updated;
await this.SelectedToolIdsChanged.InvokeAsync(updated);
}
private async Task OpenSettings(string toolId)
{
var parameters = new DialogParameters<ToolSettingsDialog>
{
{ x => x.ToolId, toolId },
};
var dialog = await this.DialogService.ShowAsync<ToolSettingsDialog>(null, parameters, Dialogs.DialogOptions.FULLSCREEN);
await dialog.Result;
this.catalog = await this.ToolRegistry.GetCatalogAsync(this.Component);
this.StateHasChanged();
}
}

View File

@ -36,6 +36,7 @@
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.AGENDA_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">

View File

@ -32,6 +32,7 @@
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
</MudPaper>
</MudField>
<ToolDefaultsConfiguration Component="Components.BIAS_DAY_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">

View File

@ -22,6 +22,8 @@
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.CHAT" IncludeVisibilityToggle="@false" />
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
<DataSourceSelection SelectionMode="DataSourceSelectionMode.CONFIGURATION_MODE" AutoSaveAppSettings="@true" @bind-DataSourceOptions="@this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions" ConfigurationHeaderMessage="@T("You can set default data sources and options for new chats. You can change these settings later for each individual chat.")"/>

View File

@ -22,6 +22,7 @@
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Coding.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.CODING_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">Close</MudButton>

View File

@ -19,10 +19,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.GRAMMAR_SPELLING_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.GRAMMAR_SPELLING_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -19,10 +19,11 @@
<ConfigurationSelect OptionDescription="@T("Language plugin used for comparision")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.I18N.PreselectedLanguagePluginId = selectedValue)" OptionHelp="@T("Select the language plugin used for comparision.")"/>
<ConfigurationProviderSelection Component="Components.I18N_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.I18N.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.I18N.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.I18N.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.I18N_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -15,10 +15,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.ICON_FINDER_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.IconFinder.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.ICON_FINDER_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -26,10 +26,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.JOB_POSTING_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.JobPostings.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.JobPostings.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.JOB_POSTING_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -17,6 +17,7 @@
<ConfigurationProviderSelection Component="Components.LEGAL_CHECK_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.LEGAL_CHECK_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">

View File

@ -20,6 +20,7 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.MY_TASKS_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">

View File

@ -21,10 +21,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.REWRITE_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.REWRITE_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -25,6 +25,7 @@
<ConfigurationProviderSelection Component="Components.SLIDE_BUILDER_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.SLIDE_BUILDER_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">

View File

@ -19,10 +19,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.SYNONYMS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Synonyms.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.SYNONYMS_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -29,10 +29,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.TEXT_SUMMARIZER_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.TEXT_SUMMARIZER_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -23,10 +23,11 @@
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.TRANSLATION_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Translation.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Translation.PreselectedProvider = selectedValue)"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.TRANSLATION_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -23,6 +23,7 @@
<ConfigurationProviderSelection Component="Components.EMAIL_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.EMail.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.EMAIL_ASSISTANT" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">

View File

@ -0,0 +1,46 @@
@using AIStudio.Tools.ToolCallingSystem
@inherits SettingsDialogBase
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6" Class="d-flex align-center">
<MudIcon Icon="@this.toolDefinition?.Icon" Class="mr-2" />
@(this.toolDefinition?.DisplayName ?? T("Tool Settings"))
</MudText>
</TitleContent>
<DialogContent>
@if (this.toolDefinition is null)
{
<MudText Typo="Typo.body1">@T("The selected tool could not be loaded.")</MudText>
}
else
{
@foreach (var property in this.toolDefinition.SettingsSchema.Properties)
{
var fieldName = property.Key;
var field = property.Value;
if (field.EnumValues.Count > 0)
{
<MudSelect T="string" Label="@field.Title" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3">
@foreach (var option in field.EnumValues)
{
<MudSelectItem T="string" Value="@option">@option</MudSelectItem>
}
</MudSelect>
}
else
{
<MudTextField T="string" Label="@field.Title" Value="@this.GetValue(fieldName)" ValueChanged="@(value => this.UpdateValue(fieldName, value))" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3" HelperText="@field.Description" InputType="@(field.Secret ? InputType.Password : InputType.Text)" />
}
}
}
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Text">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Save" Variant="Variant.Filled" Disabled="@(this.toolDefinition is null)">
@T("Save")
</MudButton>
</DialogActions>
</MudDialog>

View File

@ -0,0 +1,41 @@
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs.Settings;
public partial class ToolSettingsDialog : SettingsDialogBase
{
[Parameter]
public string ToolId { get; set; } = string.Empty;
[Inject]
private ToolRegistry ToolRegistry { get; init; } = null!;
[Inject]
private ToolSettingsService ToolSettingsService { get; init; } = null!;
private ToolDefinition? toolDefinition;
private Dictionary<string, string> values = new(StringComparer.Ordinal);
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
this.toolDefinition = this.ToolRegistry.GetDefinition(this.ToolId);
if (this.toolDefinition is not null)
this.values = await this.ToolSettingsService.GetSettingsAsync(this.toolDefinition);
}
private string GetValue(string fieldName) => this.values.GetValueOrDefault(fieldName, string.Empty);
private void UpdateValue(string fieldName, string? value) => this.values[fieldName] = value ?? string.Empty;
private async Task Save()
{
if (this.toolDefinition is null)
return;
await this.ToolSettingsService.SaveSettingsAsync(this.toolDefinition, this.values);
this.MudDialog.Close();
}
}

View File

@ -21,6 +21,7 @@
}
<SettingsPanelApp AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
<SettingsPanelTools />
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
@ -31,4 +32,4 @@
<SettingsPanelAgentContentCleaner AvailableLLMProvidersFunc="@(() => this.availableLLMProviders)"/>
</MudExpansionPanels>
</InnerScrolling>
</div>
</div>

View File

@ -1,5 +1,6 @@
using AIStudio.Agents;
using AIStudio.Settings;
using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.Qdrant;
using AIStudio.Tools.PluginSystem;
@ -168,6 +169,10 @@ internal sealed class Program
builder.Services.AddSingleton(rust);
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ToolSettingsService>();
builder.Services.AddSingleton<IToolImplementation, GetCurrentWeatherTool>();
builder.Services.AddSingleton<ToolRegistry>();
builder.Services.AddSingleton<ToolExecutor>();
builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddSingleton<VoiceRecordingAvailabilityService>();
builder.Services.AddSingleton<DataSourceService>();

View File

@ -22,29 +22,22 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"AlibabaCloud",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -10,11 +10,14 @@ using AIStudio.Provider.Anthropic;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
using AIStudio.Settings;
using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.MIME;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.Extensions.DependencyInjection;
using Host = AIStudio.Provider.SelfHosted.Host;
namespace AIStudio.Provider;
@ -572,6 +575,7 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <param name="chatModel">The selected chat model.</param>
/// <param name="chatThread">The current chat thread.</param>
/// <param name="settingsManager">The settings manager.</param>
/// <param name="messagesFactory">Builds the provider-specific base messages.</param>
/// <param name="requestFactory">Builds the provider-specific request body.</param>
/// <param name="storeType">The secret store type.</param>
/// <param name="isTryingSecret">Whether the API key is optional.</param>
@ -579,16 +583,16 @@ public abstract class BaseProvider : IProvider, ISecretId
/// <param name="requestPath">The request path, relative to the provider base URL.</param>
/// <param name="headersAction">Optional additional headers to add.</param>
/// <param name="token">The cancellation token.</param>
/// <typeparam name="TRequest">The request DTO type.</typeparam>
/// <typeparam name="TDelta">The delta stream line type.</typeparam>
/// <typeparam name="TAnnotation">The annotation stream line type.</typeparam>
/// <returns>The streamed content chunks.</returns>
protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TRequest, TDelta, TAnnotation>(
protected async IAsyncEnumerable<ContentStreamChunk> StreamOpenAICompatibleChatCompletion<TDelta, TAnnotation>(
string providerName,
Model chatModel,
ChatThread chatThread,
SettingsManager settingsManager,
Func<TextMessage, IDictionary<string, object>, Task<TRequest>> requestFactory,
Func<Task<IList<IMessageBase>>> messagesFactory,
Func<TextMessage, IList<IMessageBase>, IDictionary<string, object>, bool, IList<object>?, Task<ChatCompletionAPIRequest>> requestFactory,
SecretStoreType storeType = SecretStoreType.LLM_PROVIDER,
bool isTryingSecret = false,
string systemPromptRole = "system",
@ -613,8 +617,114 @@ public abstract class BaseProvider : IProvider, ISecretId
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters();
var baseMessages = await messagesFactory();
var toolRegistry = Program.SERVICE_PROVIDER.GetService<ToolRegistry>();
var toolExecutor = Program.SERVICE_PROVIDER.GetService<ToolExecutor>();
var currentAssistantContent = chatThread.Blocks.LastOrDefault(x => x.Role is ChatRole.AI)?.Content as ContentText;
currentAssistantContent?.ToolInvocations.Clear();
if (toolRegistry is not null && toolExecutor is not null)
{
var runnableTools = await toolRegistry.GetRunnableToolsAsync(
chatThread.RuntimeComponent,
chatThread.RuntimeSelectedToolIds,
this.Provider.GetModelCapabilities(chatModel),
settingsManager.IsToolSelectionVisible(chatThread.RuntimeComponent));
if (runnableTools.Count > 0)
{
var providerTools = runnableTools.Select(x => (object)new
{
type = "function",
function = new
{
name = x.Definition.Function.Name,
description = x.Definition.Function.Description,
parameters = x.Definition.Function.Parameters,
strict = x.Definition.Function.Strict,
}
}).ToList();
var internalMessages = new List<IMessageBase>();
var toolCallCount = 0;
while (true)
{
var requestDto = await requestFactory(systemPrompt, [..baseMessages, ..internalMessages], apiParameters, false, providerTools);
var response = await this.ExecuteChatCompletionRequest(requestDto, requestPath, requestedSecret, headersAction, token);
var responseMessage = response?.Choices.FirstOrDefault()?.Message;
if (responseMessage is null)
yield break;
if (responseMessage.ToolCalls.Count == 0)
{
currentAssistantContent!.ToolRuntimeStatus = new();
if (!string.IsNullOrWhiteSpace(responseMessage.Content))
yield return new ContentStreamChunk(responseMessage.Content, []);
yield break;
}
currentAssistantContent!.ToolRuntimeStatus = new ToolRuntimeStatus
{
IsRunning = true,
ToolNames = responseMessage.ToolCalls
.Select(x => runnableTools.FirstOrDefault(tool => tool.Definition.Function.Name.Equals(x.Function.Name, StringComparison.Ordinal)).Definition?.DisplayName ?? x.Function.Name)
.ToList(),
};
await currentAssistantContent.StreamingEvent();
internalMessages.Add(new AssistantToolCallMessage
{
Content = responseMessage.Content,
ToolCalls = responseMessage.ToolCalls,
});
foreach (var toolCall in responseMessage.ToolCalls)
{
toolCallCount++;
if (toolCallCount > 10)
{
var limitMessage = "Tool calling stopped because the maximum of 10 tool calls was reached.";
currentAssistantContent.ToolInvocations.Add(new ToolInvocationTrace
{
Order = toolCallCount,
ToolId = toolCall.Function.Name,
ToolName = toolCall.Function.Name,
ToolCallId = toolCall.Id,
Status = ToolInvocationTraceStatus.BLOCKED,
StatusMessage = limitMessage,
Result = limitMessage,
});
currentAssistantContent.ToolRuntimeStatus = new();
await currentAssistantContent.StreamingEvent();
yield return new ContentStreamChunk(limitMessage, []);
yield break;
}
var (toolContent, trace) = await toolExecutor.ExecuteAsync(
toolCall.Id,
toolCall.Function.Name,
toolCall.Function.Arguments,
runnableTools,
toolCallCount,
token);
currentAssistantContent.ToolInvocations.Add(trace);
internalMessages.Add(new ToolResultMessage
{
Content = toolContent,
ToolCallId = toolCall.Id,
Name = toolCall.Function.Name,
});
}
await currentAssistantContent.StreamingEvent();
}
}
}
// Prepare the provider HTTP chat request:
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, apiParameters), JSON_SERIALIZER_OPTIONS);
var providerChatRequest = JsonSerializer.Serialize(await requestFactory(systemPrompt, baseMessages, apiParameters, true, null), JSON_SERIALIZER_OPTIONS);
async Task<HttpRequestMessage> RequestBuilder()
{
@ -637,6 +747,27 @@ public abstract class BaseProvider : IProvider, ISecretId
yield return content;
}
private async Task<ChatCompletionResponse?> ExecuteChatCompletionRequest(
ChatCompletionAPIRequest requestDto,
string requestPath,
RequestedSecret requestedSecret,
Action<HttpRequestHeaders>? headersAction,
CancellationToken token)
{
using var request = new HttpRequestMessage(HttpMethod.Post, requestPath);
if (requestedSecret.Success)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
headersAction?.Invoke(request.Headers);
request.Content = new StringContent(JsonSerializer.Serialize(requestDto, JSON_SERIALIZER_OPTIONS), Encoding.UTF8, "application/json");
using var response = await this.httpClient.SendAsync(request, token);
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadFromJsonAsync<ChatCompletionResponse>(JSON_SERIALIZER_OPTIONS, token);
}
protected async Task<string> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
{
try

View File

@ -22,29 +22,22 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"DeepSeek",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -21,30 +21,22 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ResponseStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ResponseStreamLine, ChatCompletionAnnotationStreamLine>(
"Fireworks",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -22,29 +22,22 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"GWDG",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -24,30 +24,22 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"Google",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -22,32 +22,26 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"Groq",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
{
if (TryPopIntParameter(apiParameters, "seed", out var parsedSeed))
apiParameters["seed"] = parsedSeed;
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
return Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
});
},
token: token))
yield return content;

View File

@ -22,29 +22,22 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"Helmholtz",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -26,29 +26,22 @@ public sealed class ProviderHuggingFace : BaseProvider
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"HuggingFace",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -20,12 +20,13 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"Mistral",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
() => chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
{
if (TryPopBoolParameter(apiParameters, "safe_prompt", out var parsedSafePrompt))
apiParameters["safe_prompt"] = parsedSafePrompt;
@ -33,22 +34,15 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
if (TryPopIntParameter(apiParameters, "random_seed", out var parsedRandomSeed))
apiParameters["random_seed"] = parsedRandomSeed;
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
return Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
});
},
token: token))
yield return content;

View File

@ -0,0 +1,10 @@
namespace AIStudio.Provider.OpenAI;
public sealed record AssistantToolCallMessage : IMessageBase
{
public string Role { get; init; } = "assistant";
public string? Content { get; init; }
public IList<ChatCompletionToolCall> ToolCalls { get; init; } = [];
}

View File

@ -17,8 +17,12 @@ public record ChatCompletionAPIRequest(
public ChatCompletionAPIRequest() : this(string.Empty, [], true)
{
}
public IList<object>? Tools { get; init; }
public bool? ParallelToolCalls { get; init; }
// Attention: The "required" modifier is not supported for [JsonExtensionData].
[JsonExtensionData]
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
}
}

View File

@ -0,0 +1,10 @@
namespace AIStudio.Provider.OpenAI;
public sealed record ChatCompletionResponse
{
public string Id { get; init; } = string.Empty;
public string Model { get; init; } = string.Empty;
public IList<ChatCompletionResponseChoice> Choices { get; init; } = [];
}

View File

@ -0,0 +1,10 @@
namespace AIStudio.Provider.OpenAI;
public sealed record ChatCompletionResponseChoice
{
public int Index { get; init; }
public string FinishReason { get; init; } = string.Empty;
public ChatCompletionResponseMessage Message { get; init; } = new();
}

View File

@ -0,0 +1,10 @@
namespace AIStudio.Provider.OpenAI;
public sealed record ChatCompletionResponseMessage
{
public string Role { get; init; } = string.Empty;
public string? Content { get; init; }
public IList<ChatCompletionToolCall> ToolCalls { get; init; } = [];
}

View File

@ -0,0 +1,10 @@
namespace AIStudio.Provider.OpenAI;
public sealed record ChatCompletionToolCall
{
public string Id { get; init; } = string.Empty;
public string Type { get; init; } = "function";
public ChatCompletionToolFunction Function { get; init; } = new();
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Provider.OpenAI;
public sealed record ChatCompletionToolFunction
{
public string Name { get; init; } = string.Empty;
public string Arguments { get; init; } = string.Empty;
}

View File

@ -63,19 +63,18 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
// Check if we are using the Responses API or the Chat Completion API:
var usingResponsesAPI = modelCapabilities.Contains(Capability.RESPONSES_API);
var useChatCompletionsForTools =
chatThread.RuntimeSelectedToolIds.Count > 0 &&
modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) &&
modelCapabilities.Contains(Capability.FUNCTION_CALLING);
if (useChatCompletionsForTools)
usingResponsesAPI = false;
// Prepare the request path based on the API we are using:
var requestPath = usingResponsesAPI ? "responses" : "chat/completions";
LOGGER.LogInformation("Using the system prompt role '{SystemPromptRole}' and the '{RequestPath}' API for model '{ChatModelId}'.", systemPromptRole, requestPath, chatModel.Id);
// Prepare the system prompt:
var systemPrompt = new TextMessage
{
Role = systemPromptRole,
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
//
// Prepare the tools we want to use:
//
@ -89,60 +88,81 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
// Parse the API parameters:
var apiParameters = this.ParseAdditionalApiParameters("input", "store", "tools");
if (!usingResponsesAPI)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"OpenAI",
chatModel,
chatThread,
settingsManager,
() => chatThread.Blocks.BuildMessagesAsync(
this.Provider,
chatModel,
role => role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",
},
text => new SubContentText
{
Text = text,
},
async attachment => new SubContentImageUrlNested
{
ImageUrl = new SubContentImageUrlData
{
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
},
}),
(systemPrompt, messages, apiParameters, stream, tools) => Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
Messages = [systemPrompt, ..messages],
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters,
}),
systemPromptRole: systemPromptRole,
requestPath: "chat/completions",
token: token))
yield return content;
yield break;
}
// Prepare the system prompt:
var systemPrompt = new TextMessage
{
Role = systemPromptRole,
Content = chatThread.PrepareSystemPrompt(settingsManager),
};
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesAsync(
this.Provider, chatModel,
// OpenAI-specific role mapping:
role => role switch
{
ChatRole.USER => "user",
ChatRole.AI => "assistant",
ChatRole.AGENT => "assistant",
ChatRole.SYSTEM => systemPromptRole,
_ => "user",
},
// OpenAI's text sub-content depends on the model, whether we are using
// the Responses API or the Chat Completion API:
text => usingResponsesAPI switch
text => new SubContentInputText
{
// Responses API uses INPUT_TEXT:
true => new SubContentInputText
{
Text = text,
},
// Chat Completion API uses TEXT:
false => new SubContentText
{
Text = text,
},
Text = text,
},
// OpenAI's image sub-content depends on the model as well,
// whether we are using the Responses API or the Chat Completion API:
async attachment => usingResponsesAPI switch
async attachment => new SubContentInputImage
{
// Responses API uses INPUT_IMAGE:
true => new SubContentInputImage
{
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
},
// Chat Completion API uses IMAGE_URL:
false => new SubContentImageUrlNested
{
ImageUrl = new SubContentImageUrlData
{
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
},
}
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content)
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
});
//

View File

@ -0,0 +1,12 @@
namespace AIStudio.Provider.OpenAI;
public sealed record ToolResultMessage : IMessage<string>
{
public string Role { get; init; } = "tool";
public string Content { get; init; } = string.Empty;
public string ToolCallId { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
}

View File

@ -25,30 +25,22 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"OpenRouter",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
headersAction: headers =>
{
// Set custom headers for project identification:

View File

@ -30,28 +30,22 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ResponseStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ResponseStreamLine, NoChatCompletionAnnotationStreamLine>(
"Perplexity",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -23,36 +23,26 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, ChatCompletionAnnotationStreamLine>(
"self-hosted provider",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
() => host switch
{
// Build the list of messages. The image format depends on the host:
// - Ollama uses the direct image URL format: { "type": "image_url", "image_url": "data:..." }
// - LM Studio, vLLM, and llama.cpp use the nested image URL format: { "type": "image_url", "image_url": { "url": "data:..." } }
var messages = host switch
{
Host.OLLAMA => await chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel),
_ => await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
};
return new ChatCompletionAPIRequest
Host.OLLAMA => chatThread.Blocks.BuildMessagesUsingDirectImageUrlAsync(this.Provider, chatModel),
_ => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
},
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
isTryingSecret: true,
requestPath: host.ChatURL(),
token: token))

View File

@ -22,30 +22,22 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
/// <inheritdoc />
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
{
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionAPIRequest, ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
await foreach (var content in this.StreamOpenAICompatibleChatCompletion<ChatCompletionDeltaStreamLine, NoChatCompletionAnnotationStreamLine>(
"xAI",
chatModel,
chatThread,
settingsManager,
async (systemPrompt, apiParameters) =>
{
// Build the list of messages:
var messages = await chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel);
return new ChatCompletionAPIRequest
() => chatThread.Blocks.BuildMessagesUsingNestedImageUrlAsync(this.Provider, chatModel),
(systemPrompt, messages, apiParameters, stream, tools) =>
Task.FromResult(new ChatCompletionAPIRequest
{
Model = chatModel.Id,
// Build the messages:
// - First of all the system prompt
// - Then none-empty user and AI messages
Messages = [systemPrompt, ..messages],
// Right now, we only support streaming completions:
Stream = true,
Stream = stream,
Tools = tools,
ParallelToolCalls = tools is null ? null : true,
AdditionalApiParameters = apiParameters
};
},
}),
token: token))
yield return content;
}

View File

@ -136,4 +136,6 @@ public sealed class Data
public DataBiasOfTheDay BiasOfTheDay { get; init; } = new();
public DataI18N I18N { get; init; } = new();
public DataTools Tools { get; init; } = new();
}

View File

@ -0,0 +1,10 @@
namespace AIStudio.Settings.DataModel;
public sealed class DataTools
{
public Dictionary<string, Dictionary<string, string>> Settings { get; set; } = [];
public Dictionary<string, HashSet<string>> DefaultToolIdsByComponent { get; set; } = [];
public HashSet<string> VisibleToolSelectionComponents { get; set; } = [];
}

View File

@ -4,6 +4,7 @@ using System.Text.Json;
using AIStudio.Provider;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
@ -344,6 +345,33 @@ public sealed class SettingsManager
return preselection ?? ChatTemplate.NO_CHAT_TEMPLATE;
}
public HashSet<string> GetDefaultToolIds(AIStudio.Tools.Components component)
{
var key = component.ToString();
if (this.ConfigurationData.Tools.DefaultToolIdsByComponent.TryGetValue(key, out var toolIds))
return [..toolIds];
return [];
}
public bool IsToolSelectionVisible(AIStudio.Tools.Components component) => component switch
{
AIStudio.Tools.Components.CHAT => true,
_ => this.ConfigurationData.Tools.VisibleToolSelectionComponents.Contains(component.ToString()),
};
public void SetToolSelectionVisibility(AIStudio.Tools.Components component, bool isVisible)
{
if (component is AIStudio.Tools.Components.CHAT)
return;
var key = component.ToString();
if (isVisible)
this.ConfigurationData.Tools.VisibleToolSelectionComponents.Add(key);
else
this.ConfigurationData.Tools.VisibleToolSelectionComponents.Remove(key);
}
public ConfidenceLevel GetConfiguredConfidenceLevel(LLMProviders llmProvider)
{
if(llmProvider is LLMProviders.NONE)

View File

@ -0,0 +1,25 @@
using System.Text.Json;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class GetCurrentWeatherTool : IToolImplementation
{
public string ImplementationKey => "get_current_weather";
public IReadOnlySet<string> SensitiveTraceArgumentNames => new HashSet<string>(StringComparer.Ordinal);
public Task<ToolExecutionResult> ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default)
{
var city = arguments.TryGetProperty("city", out var cityValue) ? cityValue.GetString() ?? string.Empty : string.Empty;
var state = arguments.TryGetProperty("state", out var stateValue) ? stateValue.GetString() ?? string.Empty : string.Empty;
var unit = arguments.TryGetProperty("unit", out var unitValue) ? unitValue.GetString() ?? string.Empty : string.Empty;
if (unit is not ("celsius" or "fahrenheit"))
throw new ArgumentException($"Invalid unit '{unit}'.");
return Task.FromResult(new ToolExecutionResult
{
TextContent = $"The weather in {city}, {state} is 85 degrees {unit}. It is partly cloudy with highs in the 90's.",
});
}
}

View File

@ -0,0 +1,14 @@
using System.Text.Json;
namespace AIStudio.Tools.ToolCallingSystem;
public interface IToolImplementation
{
public string ImplementationKey { get; }
public IReadOnlySet<string> SensitiveTraceArgumentNames { get; }
public Task<ToolExecutionResult> ExecuteAsync(JsonElement arguments, ToolExecutionContext context, CancellationToken token = default);
public string FormatTraceResult(string rawResult) => rawResult;
}

View File

@ -0,0 +1,64 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolDefinition
{
public int SchemaVersion { get; init; } = 1;
public string Id { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public string Icon { get; init; } = Icons.Material.Filled.Build;
public string ImplementationKey { get; init; } = string.Empty;
public ToolVisibilityDefinition VisibleIn { get; init; } = new();
public ToolSettingsSchema SettingsSchema { get; init; } = new();
public ToolFunctionDefinition Function { get; init; } = new();
}
public sealed class ToolVisibilityDefinition
{
public bool Chat { get; init; } = true;
public bool Assistants { get; init; } = true;
}
public sealed class ToolFunctionDefinition
{
public string Name { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public bool Strict { get; init; } = true;
public JsonElement Parameters { get; init; }
}
public sealed class ToolSettingsSchema
{
public string Type { get; init; } = "object";
public Dictionary<string, ToolSettingsFieldDefinition> Properties { get; init; } = [];
public HashSet<string> Required { get; init; } = [];
}
public sealed class ToolSettingsFieldDefinition
{
public string Type { get; init; } = "string";
public string Title { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
[JsonPropertyName("enum")]
public List<string> EnumValues { get; init; } = [];
public bool Secret { get; init; }
}

View File

@ -0,0 +1,94 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Settings;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolExecutionContext
{
public required ToolDefinition Definition { get; init; }
public required SettingsManager SettingsManager { get; init; }
public required IReadOnlyDictionary<string, string> SettingsValues { get; init; }
}
public sealed class ToolExecutionResult
{
public string? TextContent { get; init; }
public JsonNode? JsonContent { get; init; }
public string ToModelContent()
{
if (this.JsonContent is not null)
return this.JsonContent.ToJsonString();
return this.TextContent ?? string.Empty;
}
}
public enum ToolInvocationTraceStatus
{
NONE = 0,
SUCCESS,
ERROR,
BLOCKED,
}
public sealed class ToolInvocationTrace
{
public int Order { get; set; }
public string ToolId { get; set; } = string.Empty;
public string ToolName { get; set; } = string.Empty;
public string ToolIcon { get; set; } = Icons.Material.Filled.Build;
public string ToolCallId { get; set; } = string.Empty;
public ToolInvocationTraceStatus Status { get; set; } = ToolInvocationTraceStatus.NONE;
public bool WasExecuted { get; set; }
public string StatusMessage { get; set; } = string.Empty;
public Dictionary<string, string> Arguments { get; set; } = [];
public string Result { get; set; } = string.Empty;
}
public sealed class ToolRuntimeStatus
{
public bool IsRunning { get; set; }
public List<string> ToolNames { get; set; } = [];
public string Message => this.ToolNames.Count switch
{
0 => string.Empty,
1 => $"Using tool: {this.ToolNames[0]}",
_ => $"Using tools: {string.Join(", ", this.ToolNames)}",
};
}
public sealed class ToolConfigurationState
{
public bool IsConfigured { get; init; }
public List<string> MissingRequiredFields { get; init; } = [];
}
public sealed class ToolCatalogItem
{
public required ToolDefinition Definition { get; init; }
public required ToolConfigurationState ConfigurationState { get; init; }
}
public sealed class ToolSelectionState
{
public HashSet<string> SelectedToolIds { get; init; } = [];
}

View File

@ -0,0 +1,107 @@
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolExecutor(ToolSettingsService toolSettingsService)
{
public async Task<(string Content, ToolInvocationTrace Trace)> ExecuteAsync(
string toolCallId,
string toolName,
string argumentsJson,
IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)> runnableTools,
int order,
CancellationToken token = default)
{
var runnableTool = runnableTools.FirstOrDefault(x => x.Definition.Function.Name.Equals(toolName, StringComparison.Ordinal));
if (runnableTool.Definition is null || runnableTool.Implementation is null)
{
return (this.CreateError(toolName), new ToolInvocationTrace
{
Order = order,
ToolId = toolName,
ToolName = toolName,
ToolCallId = toolCallId,
Status = ToolInvocationTraceStatus.BLOCKED,
StatusMessage = "Tool is not available in the current context.",
Result = this.CreateError(toolName),
});
}
var definition = runnableTool.Definition;
var implementation = runnableTool.Implementation;
try
{
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
var settingsValues = await toolSettingsService.GetSettingsAsync(definition);
var result = await implementation.ExecuteAsync(document.RootElement, new ToolExecutionContext
{
Definition = definition,
SettingsManager = Program.SERVICE_PROVIDER.GetRequiredService<Settings.SettingsManager>(),
SettingsValues = settingsValues,
}, token);
return (result.ToModelContent(), new ToolInvocationTrace
{
Order = order,
ToolId = definition.Id,
ToolName = definition.DisplayName,
ToolIcon = definition.Icon,
ToolCallId = toolCallId,
Status = ToolInvocationTraceStatus.SUCCESS,
WasExecuted = true,
Arguments = FormatArguments(document.RootElement, implementation.SensitiveTraceArgumentNames),
Result = implementation.FormatTraceResult(result.ToModelContent()),
});
}
catch (Exception exception)
{
var error = $"Tool execution failed: {exception.Message}";
Dictionary<string, string> formattedArguments = [];
try
{
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
formattedArguments = FormatArguments(document.RootElement, implementation.SensitiveTraceArgumentNames);
}
catch
{
}
return (error, new ToolInvocationTrace
{
Order = order,
ToolId = definition.Id,
ToolName = definition.DisplayName,
ToolIcon = definition.Icon,
ToolCallId = toolCallId,
Status = ToolInvocationTraceStatus.ERROR,
StatusMessage = error,
Arguments = formattedArguments,
Result = error,
});
}
}
private string CreateError(string toolName) => $"Tool '{toolName}' is not available.";
private static Dictionary<string, string> FormatArguments(JsonElement rootElement, IReadOnlySet<string> sensitiveNames)
{
if (rootElement.ValueKind is not JsonValueKind.Object)
return [];
var arguments = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var property in rootElement.EnumerateObject())
{
arguments[property.Name] = sensitiveNames.Contains(property.Name)
? "*****"
: property.Value.ValueKind switch
{
JsonValueKind.String => property.Value.GetString() ?? string.Empty,
_ => property.Value.ToString(),
};
}
return arguments;
}
}

View File

@ -0,0 +1,135 @@
using System.Text.Json;
using AIStudio.Provider;
using Microsoft.AspNetCore.Hosting;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolRegistry
{
private readonly ILogger<ToolRegistry> logger;
private readonly ToolSettingsService toolSettingsService;
private readonly Dictionary<string, ToolDefinition> definitionsById = new(StringComparer.Ordinal);
private readonly Dictionary<string, IToolImplementation> implementationsByKey = new(StringComparer.Ordinal);
public ToolRegistry(
IWebHostEnvironment webHostEnvironment,
IEnumerable<IToolImplementation> implementations,
ToolSettingsService toolSettingsService,
ILogger<ToolRegistry> logger)
{
this.logger = logger;
this.toolSettingsService = toolSettingsService;
foreach (var implementation in implementations)
this.implementationsByKey[implementation.ImplementationKey] = implementation;
var definitionsDirectory = webHostEnvironment.WebRootFileProvider.GetDirectoryContents("tool_definitions");
if (!definitionsDirectory.Exists)
{
this.logger.LogWarning("The tool definitions directory was not found.");
return;
}
var serializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
foreach (var file in definitionsDirectory.Where(x => !x.IsDirectory && x.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase)))
{
try
{
using var stream = file.CreateReadStream();
var definition = JsonSerializer.Deserialize<ToolDefinition>(stream, serializerOptions);
if (definition is null || string.IsNullOrWhiteSpace(definition.Id))
{
this.logger.LogWarning("Skipping tool definition '{ToolFile}' because it could not be deserialized.", file.Name);
continue;
}
if (!this.implementationsByKey.ContainsKey(definition.ImplementationKey))
{
this.logger.LogWarning("Skipping tool definition '{ToolId}' because implementation key '{ImplementationKey}' is not registered.", definition.Id, definition.ImplementationKey);
continue;
}
this.definitionsById[definition.Id] = definition;
}
catch (Exception exception)
{
this.logger.LogWarning(exception, "Skipping invalid tool definition file '{ToolFile}'.", file.Name);
}
}
}
public IReadOnlyList<ToolDefinition> GetDefinitionsForComponent(AIStudio.Tools.Components component)
{
var isChat = component is AIStudio.Tools.Components.CHAT;
return this.definitionsById.Values
.Where(x => isChat ? x.VisibleIn.Chat : x.VisibleIn.Assistants)
.OrderBy(x => x.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public IReadOnlyList<ToolDefinition> GetAllDefinitions() => this.definitionsById.Values
.OrderBy(x => x.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
public ToolDefinition? GetDefinition(string toolId) => this.definitionsById.GetValueOrDefault(toolId);
public IToolImplementation? GetImplementation(string implementationKey) => this.implementationsByKey.GetValueOrDefault(implementationKey);
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(AIStudio.Tools.Components component)
{
var definitions = this.GetDefinitionsForComponent(component);
return await this.GetCatalogAsync(definitions);
}
public async Task<IReadOnlyList<ToolCatalogItem>> GetCatalogAsync(IEnumerable<ToolDefinition> definitions)
{
var definitionList = definitions.ToList();
var items = new List<ToolCatalogItem>(definitionList.Count);
foreach (var definition in definitionList)
{
items.Add(new ToolCatalogItem
{
Definition = definition,
ConfigurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition),
});
}
return items;
}
public async Task<IReadOnlyList<(ToolDefinition Definition, IToolImplementation Implementation)>> GetRunnableToolsAsync(
AIStudio.Tools.Components component,
IEnumerable<string> selectedToolIds,
IReadOnlyCollection<Capability> modelCapabilities,
bool isToolSelectionVisible)
{
if (!isToolSelectionVisible)
return [];
if (!modelCapabilities.Contains(Capability.CHAT_COMPLETION_API) || !modelCapabilities.Contains(Capability.FUNCTION_CALLING))
return [];
var selectedToolIdSet = selectedToolIds.ToHashSet(StringComparer.Ordinal);
var definitions = this.GetDefinitionsForComponent(component).Where(x => selectedToolIdSet.Contains(x.Id)).ToList();
var result = new List<(ToolDefinition, IToolImplementation)>(definitions.Count);
foreach (var definition in definitions)
{
if (!this.implementationsByKey.TryGetValue(definition.ImplementationKey, out var implementation))
continue;
var configurationState = await this.toolSettingsService.GetConfigurationStateAsync(definition);
if (!configurationState.IsConfigured)
continue;
result.Add((definition, implementation));
}
return result;
}
}

View File

@ -0,0 +1,10 @@
using AIStudio.Tools;
namespace AIStudio.Tools.ToolCallingSystem;
internal sealed record ToolSettingsSecretId(string ToolId, string FieldName) : ISecretId
{
public string SecretId => $"tool::{this.ToolId}";
public string SecretName => this.FieldName;
}

View File

@ -0,0 +1,81 @@
using AIStudio.Settings;
using AIStudio.Tools.Services;
namespace AIStudio.Tools.ToolCallingSystem;
public sealed class ToolSettingsService(SettingsManager settingsManager, RustService rustService)
{
public async Task<Dictionary<string, string>> GetSettingsAsync(ToolDefinition definition)
{
var values = new Dictionary<string, string>(StringComparer.Ordinal);
var storedValues = settingsManager.ConfigurationData.Tools.Settings.GetValueOrDefault(definition.Id);
foreach (var property in definition.SettingsSchema.Properties)
{
var fieldName = property.Key;
var fieldDefinition = property.Value;
if (fieldDefinition.Secret)
{
var response = await rustService.GetSecret(new ToolSettingsSecretId(definition.Id, fieldName), isTrying: true);
if (response.Success)
values[fieldName] = await response.Secret.Decrypt(Program.ENCRYPTION);
continue;
}
if (storedValues?.TryGetValue(fieldName, out var storedValue) is true)
values[fieldName] = storedValue;
}
return values;
}
public async Task<ToolConfigurationState> GetConfigurationStateAsync(ToolDefinition definition)
{
var values = await this.GetSettingsAsync(definition);
var missing = new List<string>();
foreach (var requiredField in definition.SettingsSchema.Required)
{
if (!values.TryGetValue(requiredField, out var value) || string.IsNullOrWhiteSpace(value))
missing.Add(requiredField);
}
return new ToolConfigurationState
{
IsConfigured = missing.Count == 0,
MissingRequiredFields = missing,
};
}
public async Task SaveSettingsAsync(ToolDefinition definition, IReadOnlyDictionary<string, string> values)
{
if (!settingsManager.ConfigurationData.Tools.Settings.TryGetValue(definition.Id, out var storedValues))
{
storedValues = new Dictionary<string, string>(StringComparer.Ordinal);
settingsManager.ConfigurationData.Tools.Settings[definition.Id] = storedValues;
}
foreach (var property in definition.SettingsSchema.Properties)
{
var fieldName = property.Key;
var fieldDefinition = property.Value;
values.TryGetValue(fieldName, out var value);
value ??= string.Empty;
if (fieldDefinition.Secret)
{
var secretId = new ToolSettingsSecretId(definition.Id, fieldName);
if (string.IsNullOrWhiteSpace(value))
await rustService.DeleteSecret(secretId);
else
await rustService.SetSecret(secretId, value);
continue;
}
storedValues[fieldName] = value;
}
await settingsManager.StoreSettings();
await MessageBus.INSTANCE.SendMessage<object?>(null, Event.CONFIGURATION_CHANGED, null);
}
}

View File

@ -0,0 +1,57 @@
{
"schemaVersion": 1,
"id": "get_current_weather",
"displayName": "Current Weather",
"icon": "material-icons:cloud",
"implementationKey": "get_current_weather",
"visibleIn": {
"chat": true,
"assistants": true
},
"settingsSchema": {
"type": "object",
"properties": {
"demoLabel": {
"type": "string",
"title": "Demo Label",
"description": "Required demo setting for validating tool settings in tests.",
"secret": false
}
},
"required": [
"demoLabel"
]
},
"function": {
"name": "get_current_weather",
"description": "Get the current weather in a given location.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to find the weather for, e.g. 'San Francisco'."
},
"state": {
"type": "string",
"description": "The two-letter abbreviation for the state, e.g. 'CA'."
},
"unit": {
"type": "string",
"description": "The unit to fetch the temperature in.",
"enum": [
"celsius",
"fahrenheit"
]
}
},
"required": [
"city",
"state",
"unit"
],
"additionalProperties": false
}
}
}