Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/MindWork AI Studio/Assistants/AssistantBase.razor
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
15 changes: 15 additions & 0 deletions app/MindWork AI Studio/Assistants/AssistantBase.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using AIStudio.Settings;
using AIStudio.Dialogs.Settings;
using AIStudio.Tools.Services;
using AIStudio.Tools.ToolCallingSystem;

using Microsoft.AspNetCore.Components;

Expand Down Expand Up @@ -93,6 +94,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));

Expand Down Expand Up @@ -124,6 +126,7 @@ protected override async Task OnInitializedAsync()
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()
Expand Down Expand Up @@ -223,6 +226,7 @@ protected void CreateChatThread()
ChatId = Guid.NewGuid(),
Name = string.Format(this.TB("Assistant - {0}"), this.Title),
Blocks = [],
RuntimeComponent = this.Component,
};
}

Expand All @@ -239,6 +243,7 @@ protected Guid CreateChatThread(Guid workspaceId, string name)
ChatId = chatId,
Name = name,
Blocks = [],
RuntimeComponent = this.Component,
};

return chatId;
Expand All @@ -250,6 +255,12 @@ protected virtual void ResetProviderAndProfileSelection()
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
}

protected Task SelectedToolIdsChanged(HashSet<string> updatedToolIds)
{
this.selectedToolIds = ToolSelectionRules.NormalizeSelection(updatedToolIds);
return Task.CompletedTask;
}

protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false, params List<FileAttachment> attachments)
{
Expand Down Expand Up @@ -297,6 +308,10 @@ protected async Task<string> AddAIResponseAsync(DateTimeOffset time, bool hideCo
{
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)
? ToolSelectionRules.NormalizeSelection(this.selectedToolIds)
: [];
}

this.isProcessing = true;
Expand Down
260 changes: 241 additions & 19 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua

Large diffs are not rendered by default.

44 changes: 43 additions & 1 deletion app/MindWork AI Studio/Chat/ChatThread.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Globalization;
using System.Text.Json.Serialization;

using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.ERIClient.DataModel;

namespace AIStudio.Chat;
Expand Down Expand Up @@ -79,6 +82,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;

Expand Down Expand Up @@ -185,6 +194,17 @@ public string PrepareSystemPrompt(SettingsManager settingsManager)
}

LOGGER.LogInformation(logMessage);

var toolPolicy = this.BuildToolPolicyPrompt();
if (!string.IsNullOrWhiteSpace(toolPolicy))
{
systemPromptText = $"""
{systemPromptText}

{toolPolicy}
""";
}

if(!this.IncludeDateTime)
return systemPromptText;

Expand All @@ -205,6 +225,28 @@ public string PrepareSystemPrompt(SettingsManager settingsManager)
""";
}

private string BuildToolPolicyPrompt()
{
var normalizedToolIds = ToolSelectionRules.NormalizeSelection(this.RuntimeSelectedToolIds);
var hasWebSearch = normalizedToolIds.Contains(ToolSelectionRules.WEB_SEARCH_TOOL_ID);
var hasReadWebPage = normalizedToolIds.Contains(ToolSelectionRules.READ_WEB_PAGE_TOOL_ID);

if (hasWebSearch && hasReadWebPage)
return """
Tool usage policy for web search:
- Use the `web_search`-tool to discover relevant candidate URLs.
- Do not answer substantive web questions from search snippets alone when `read_web_page` is available.
- Search snippets alone are only sufficient for simple link-finding or very high-level orientation.
- After `web_search`, use the `read_web_page`-tool on at least one relevant result before answering questions that require facts, summaries, comparisons, current information, or other page-level details.
- Prefer answering from the extracted page content when it is available.
- Summarize tool results in natural language.
- Treat `read_web_page` results as working material for synthesis, not as final answer text.
- Add a sources-section to the end of your answer, where you link the sources that you used.
""";

return string.Empty;
}

/// <summary>
/// Removes a content block from this chat thread.
/// </summary>
Expand Down Expand Up @@ -287,4 +329,4 @@ public void Remove(IContent content, bool removeForRegenerate = false)

return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
}
}
}
92 changes: 89 additions & 3 deletions app/MindWork AI Studio/Chat/ContentBlockComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,27 @@
</MudAvatar>
</CardHeaderAvatar>
<CardHeaderContent>
<MudText Typo="Typo.body1">
@this.Role.ToName() (@this.Time.LocalDateTime)
</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudText Typo="Typo.body1">
@this.Role.ToName() (@this.Time.LocalDateTime)
</MudText>
@if (this.HasToolTrace)
{
<MudTooltip Text="@this.GetToolTraceTooltip()" Placement="Placement.Bottom">
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
Size="Size.Small"
Class="px-2 py-1 rounded-pill"
Style="min-width:auto; border-width:1px; text-transform:none;"
OnClick="@this.ToggleToolTrace">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Build" Color="Color.Default" Size="Size.Small" />
<MudIcon Icon="@(this.showToolTrace ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)" Size="Size.Small" />
</MudStack>
</MudButton>
</MudTooltip>
}
</MudStack>
</CardHeaderContent>
<CardHeaderActions>
@if (this.Content.FileAttachments.Count > 0)
Expand Down Expand Up @@ -96,6 +114,67 @@
}
else
{
@if (this.HasToolTrace && this.showToolTrace)
{
<MudPaper Class="pa-3 mb-3 border rounded-lg" Style="border-width:1px;">
<MudText Typo="Typo.subtitle2" Class="mb-2">
@string.Format(T("Tool Calls ({0})"), textContent.ToolInvocations.Count)
</MudText>
@foreach (var invocation in textContent.ToolInvocations.OrderBy(x => x.Order))
{
<MudPaper Class="pa-3 mb-3 border rounded-lg" Style="border-width:1px;">
<MudButton Variant="Variant.Text"
Color="Color.Default"
FullWidth="@true"
Class="px-0 py-0 justify-space-between"
Style="min-width:auto; text-transform:none;"
OnClick="@(() => this.ToggleToolInvocation(invocation.Order))">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="w-100">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@invocation.ToolIcon" Color="Color.Info" />
<MudText Typo="Typo.subtitle1">@($"{invocation.Order}. {invocation.ToolName}")</MudText>
<MudChip T="string" Color="@ContentBlockComponent.GetTraceColor(invocation.Status)" Size="Size.Small" Variant="Variant.Outlined">
@this.GetTraceStatusText(invocation)
</MudChip>
</MudStack>
<MudIcon Icon="@(this.IsToolInvocationExpanded(invocation.Order) ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)" Size="Size.Small" />
</MudStack>
</MudButton>

@if (this.IsToolInvocationExpanded(invocation.Order))
{
@if (!string.IsNullOrWhiteSpace(invocation.StatusMessage))
{
<MudText Typo="Typo.body2" Color="Color.Warning" Class="mt-3 mb-3">@invocation.StatusMessage</MudText>
}

<MudText Typo="Typo.subtitle2">@T("Result")</MudText>
<MudPaper Class="pa-3 mt-2 mb-3">
<MudText Typo="Typo.body2" Style="white-space: pre-wrap;">@this.GetToolInvocationResult(invocation)</MudText>
</MudPaper>

<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-0">
@foreach (var argument in invocation.Arguments)
{
<MudListItem T="string">
<MudText Typo="Typo.body2"><strong>@argument.Key:</strong> @argument.Value</MudText>
</MudListItem>
}
</MudList>
}
}
</MudPaper>
}
</MudPaper>
}

var renderPlan = this.GetMarkdownRenderPlan(textContent.Text);
<div @ref="this.mathContentContainer" class="chat-math-container">
@foreach (var segment in renderPlan.Segments)
Expand All @@ -115,6 +194,13 @@
<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>
}
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using AIStudio.Components;
using AIStudio.Dialogs;
using AIStudio.Tools.Services;
using AIStudio.Tools.ToolCallingSystem;
using Microsoft.AspNetCore.Components;
using MudBlazor;

namespace AIStudio.Chat;

Expand Down Expand Up @@ -103,6 +105,8 @@ public partial class ContentBlockComponent : MSGComponentBase, IAsyncDisposable
private string lastMathRenderSignature = string.Empty;
private bool hasActiveMathContainer;
private bool isDisposed;
private bool showToolTrace;
private readonly HashSet<int> expandedToolInvocations = [];

#region Overrides of ComponentBase

Expand Down Expand Up @@ -199,6 +203,27 @@ private int CreateRenderHash()
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);
hash.Add(this.showToolTrace);
hash.Add(this.expandedToolInvocations.Count);
foreach (var expandedInvocation in this.expandedToolInvocations.Order())
hash.Add(expandedInvocation);
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:
Expand All @@ -214,8 +239,55 @@ private int CreateRenderHash()

private string CardClasses => $"my-2 rounded-lg {this.Class}";

private bool HasToolTrace => this.Role is ChatRole.AI && this.GetToolInvocations().Count > 0;

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 IReadOnlyList<ToolInvocationTrace> GetToolInvocations() => this.Content is ContentText textContent
? textContent.ToolInvocations.OrderBy(x => x.Order).ToList()
: [];

private string GetToolTraceTooltip()
{
var invocations = this.GetToolInvocations();
return invocations.Count switch
{
0 => this.T("No tool calls"),
1 => string.Format(this.T("Show tool call for {0}"), invocations[0].ToolName),
_ => string.Format(this.T("Show {0} tool calls"), invocations.Count),
};
}

private void ToggleToolTrace() => this.showToolTrace = !this.showToolTrace;

private bool IsToolInvocationExpanded(int order) => this.expandedToolInvocations.Contains(order);

private void ToggleToolInvocation(int order)
{
if (!this.expandedToolInvocations.Add(order))
this.expandedToolInvocations.Remove(order);
}

private string GetToolInvocationResult(ToolInvocationTrace invocation) => string.IsNullOrWhiteSpace(invocation.Result)
? this.T("No result")
: invocation.Result;

private MudMarkdownStyling MarkdownStyling => new()
{
CodeBlock = { Theme = this.CodeColorPalette },
Expand Down
Loading