mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-05-20 15:52:15 +00:00
Erste Version des Tool Callings von Codex
This commit is contained in:
parent
f024de8322
commit
447fe9d712
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
{
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
64
app/MindWork AI Studio/Components/ToolSelection.razor
Normal file
64
app/MindWork AI Studio/Components/ToolSelection.razor
Normal 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>
|
||||
78
app/MindWork AI Studio/Components/ToolSelection.razor.cs
Normal file
78
app/MindWork AI Studio/Components/ToolSelection.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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.")"/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; } = [];
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; } = [];
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
12
app/MindWork AI Studio/Provider/OpenAI/ToolResultMessage.cs
Normal file
12
app/MindWork AI Studio/Provider/OpenAI/ToolResultMessage.cs
Normal 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;
|
||||
}
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
10
app/MindWork AI Studio/Settings/DataModel/DataTools.cs
Normal file
10
app/MindWork AI Studio/Settings/DataModel/DataTools.cs
Normal 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; } = [];
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; } = [];
|
||||
}
|
||||
107
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs
Normal file
107
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolExecutor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
135
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs
Normal file
135
app/MindWork AI Studio/Tools/ToolCallingSystem/ToolRegistry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user