mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 06:47:22 +00:00
Added a Send to menu to every chat message and kept restrictions when sending content on (#1124)
This commit is contained in:
1 parent
d06688ca08
commit
bf54b8cd0d
73 files changed
+1923
-390
No files matched your search
@@ -4,6 +4,7 @@ using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Dialogs.Settings;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.AssistantLaunch;
|
||||
using AIStudio.Tools.AssistantSessions;
|
||||
using AIStudio.Tools.Media;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -48,6 +49,9 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
[Inject]
|
||||
protected AssistantSessionService AssistantSessionService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantLaunchService AssistantLaunchService { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the job service used to run assistant-created chats independently from the assistant UI.
|
||||
/// </summary>
|
||||
@@ -162,6 +166,44 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
protected virtual ChatThread ConvertToChatThread => this.CreateSendToChatThread();
|
||||
|
||||
/// <summary>
|
||||
/// Takes over a text which another assistant or a chat sent to this assistant.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every assistant which content may be sent to puts the text into its main field, see
|
||||
/// ComponentsExtensions.AllowSendTo. It comes before the preselected values, so a field which those only fill
|
||||
/// when empty keeps the text. An assistant which allows sending to it but takes nothing would leave the text
|
||||
/// lost without a word, which is why a test checks that each one overrides this.
|
||||
/// </remarks>
|
||||
/// <param name="payload">The text sent.</param>
|
||||
protected virtual void ApplyTextLaunchPayload(TextLaunchPayload payload) => this.Logger.LogError("The assistant '{Component}' allows sending text to it, but it does not take the text.", this.Component);
|
||||
|
||||
/// <summary>
|
||||
/// What the text sent to this assistant demands, because of where it came from.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An answer written from mails demands here what it demanded in its chat. The thread of the
|
||||
/// assistant is created anew with every run, so the restrictions live here and go onto every thread
|
||||
/// this assistant runs, see AssignRuntimeIdentity, and onto every tool list, see GetRunnableToolIds.
|
||||
/// They stay until the form is reset, which removes the text as well.
|
||||
/// </remarks>
|
||||
private ChatRestrictions transferredRestrictions = ChatRestrictions.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// What everything this assistant holds demands: the text sent to it and what its current thread read.
|
||||
/// </summary>
|
||||
private ChatRestrictions CurrentRestrictions => this.ChatThread?.CaptureRestrictions().StricterOf(this.transferredRestrictions) ?? this.transferredRestrictions;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps what the current result demands, before the assistant puts that result into its own input.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call this before clearing the thread: the result was written from what this thread read, and the
|
||||
/// web addresses in it were written by a model. Both stay with the input until the form is reset.
|
||||
/// </remarks>
|
||||
/// <param name="resultText">The result which becomes the new input.</param>
|
||||
protected void TakeOverRestrictionsOfResult(string resultText) => this.transferredRestrictions = this.CurrentRestrictions.WithWebAddressesWrittenByModelIn(resultText);
|
||||
|
||||
private protected virtual RenderFragment? HeaderActions => null;
|
||||
|
||||
private protected virtual RenderFragment? AfterResultContent => null;
|
||||
@@ -213,6 +255,13 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// A text sent here comes first, so that the preselected values below only fill what it left empty:
|
||||
if (AssistantLaunchService.TakeTextPayload(this.Component) is { } textLaunchPayload)
|
||||
{
|
||||
this.transferredRestrictions = textLaunchPayload.Restrictions;
|
||||
this.ApplyTextLaunchPayload(textLaunchPayload);
|
||||
}
|
||||
|
||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
@@ -276,25 +325,30 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
private string SubmitButtonStyle => this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence ? this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).StyleBorder(this.SettingsManager) : string.Empty;
|
||||
|
||||
private IReadOnlyList<Tools.Components> VisibleSendToAssistants => Enum.GetValues<AIStudio.Tools.Components>()
|
||||
.Where(this.CanSendToAssistant)
|
||||
.OrderBy(component => component.Name().Length)
|
||||
.ToArray();
|
||||
private IReadOnlyList<Tools.Components> VisibleSendToAssistants => this.AssistantLaunchService.GetDestinationsFor(this.Component);
|
||||
|
||||
protected string? ValidatingProvider(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
if(provider.UsedLLMProvider == LLMProviders.NONE)
|
||||
return this.TB("Please select a provider.");
|
||||
|
||||
|
||||
// A provider which nothing else rules out may still be kept from the text sent here, because of where it came from:
|
||||
if (!this.transferredRestrictions.IsLLMProviderAllowed(provider) && ChatRestrictions.NONE.IsLLMProviderAllowed(provider))
|
||||
return this.TB("This provider may not see the text sent to this assistant, because of where the text came from. Please select another provider.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task Start()
|
||||
{
|
||||
//
|
||||
// A provider which may not see the text sent here would fail the run without a word, see
|
||||
// ContentText.CreateFromProviderAsync, so the user hears about it before the run starts:
|
||||
//
|
||||
await this.RefreshProviderSelectionFromConfigurationAsync();
|
||||
if (this.ProviderSettings == Settings.Provider.NONE)
|
||||
if (this.ProviderSettings == Settings.Provider.NONE || this.ValidatingProvider(this.ProviderSettings) is not null)
|
||||
{
|
||||
await this.ReportMissingProviderAsync();
|
||||
await this.ReportProviderIssueAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,16 +421,17 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells the user that the assistant cannot start without a provider, together with whatever else is missing.
|
||||
/// Tells the user that the assistant cannot start with the selected provider, together with whatever else is missing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Most assistants validate their form only in their submit action, which never runs without a
|
||||
/// provider. Validating here marks the provider selection and lists the other issues as well.
|
||||
/// The form does not report the missing provider when it holds no provider selection, as with an
|
||||
/// assistant plugin without one, or when its selection still shows the provider which the refresh
|
||||
/// at the start has just dropped. Then the issue is added on its own.
|
||||
/// Either no provider is selected, or the selected one may not see the text sent to this assistant,
|
||||
/// see ValidatingProvider. Most assistants validate their form only in their submit action, which
|
||||
/// never runs without a usable provider. Validating here marks the provider selection and lists the
|
||||
/// other issues as well. The form does not report the issue when it holds no provider selection, as
|
||||
/// with an assistant plugin without one, or when its selection still shows the provider which the
|
||||
/// refresh at the start has just dropped. Then the issue is added on its own.
|
||||
/// </remarks>
|
||||
private async Task ReportMissingProviderAsync()
|
||||
private async Task ReportProviderIssueAsync()
|
||||
{
|
||||
if (this.Form is not null)
|
||||
await this.Form.Validate();
|
||||
@@ -466,16 +521,20 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
/// <summary>
|
||||
/// Sets who runs the given thread: this assistant's component and, for an assistant plugin, its name.
|
||||
/// It also puts the restrictions of a text sent to this assistant on the thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Both decide which tools the thread may use and what its requests to self-hosted servers name. Every
|
||||
/// way an assistant creates or sends a thread goes through here, so that none of them can forget one.
|
||||
/// That is why the restrictions of a text sent here go onto the thread here, too: every thread of this
|
||||
/// assistant may hold that text, its private threads for research included.
|
||||
/// </remarks>
|
||||
/// <param name="chatThread">The thread this assistant runs.</param>
|
||||
protected void AssignRuntimeIdentity(ChatThread chatThread)
|
||||
{
|
||||
chatThread.RuntimeComponent = this.Component;
|
||||
chatThread.RuntimeAssistantName = this.RuntimeAssistantName;
|
||||
chatThread.RequireRestrictions(this.transferredRestrictions);
|
||||
}
|
||||
|
||||
private Task RefreshProviderSelectionFromConfigurationAsync()
|
||||
@@ -515,9 +574,12 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
/// the user a tool for good. The same goes for a tool which a mailbox the thread read from
|
||||
/// keeps back.
|
||||
/// </remarks>
|
||||
/// <param name="outboundDataRestriction">Where the thread the tools run in may still send data, see ChatThread.RequiredOutboundDataRestriction.</param>
|
||||
/// <param name="outboundDataRestriction">Where the thread the tools run in may still send data, see ChatThread.RequiredOutboundDataRestriction. A text sent to this assistant may demand more, and then that counts.</param>
|
||||
protected HashSet<string> GetRunnableToolIds(OutboundDataRestriction outboundDataRestriction)
|
||||
{
|
||||
// Every thread of this assistant may hold the text sent to it, so what that text demands holds whoever asks:
|
||||
outboundDataRestriction = outboundDataRestriction.StricterOf(this.transferredRestrictions.OutboundData.Restriction);
|
||||
|
||||
if (this.AssistantManagedToolIds is not null)
|
||||
return this.ToolRegistry.FilterToolIdsForProvider(this.ProviderSettings, this.AssistantManagedToolIds, outboundDataRestriction);
|
||||
|
||||
@@ -834,7 +896,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
|
||||
protected async Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
|
||||
{
|
||||
if (!this.CanSendToAssistant(destination))
|
||||
if (!this.VisibleSendToAssistants.Contains(destination))
|
||||
return;
|
||||
|
||||
var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch
|
||||
@@ -847,57 +909,33 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
},
|
||||
};
|
||||
|
||||
var sendToData = destination.GetData();
|
||||
if (destination.HasSingleSessionSlot() && this.AssistantSessionService.GetSnapshots().Any(snapshot => snapshot.IsActive && snapshot.Key.Component == destination))
|
||||
// The chat starts from the conversation of this assistant, unless it only takes the text for the next message:
|
||||
if (destination is Tools.Components.CHAT && !sendToButton.SendToChatAsInput)
|
||||
{
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Apps, this.TB("This assistant is already running. AI Studio opens the running session instead.")));
|
||||
this.NavigationManager.NavigateTo(sendToData.Route);
|
||||
var convertedChatThread = this.ConvertToChatThread;
|
||||
convertedChatThread = convertedChatThread with { SelectedProvider = this.ProviderSettings.Id };
|
||||
|
||||
//
|
||||
// The chat holds what this assistant held, whichever thread the assistant builds it from: some
|
||||
// assistants start it from a new thread, which has read nothing, or from none at all. The copy
|
||||
// above is the chat's own, so tightening it leaves the thread of this assistant as it is:
|
||||
//
|
||||
convertedChatThread.RequireRestrictions(this.CurrentRestrictions);
|
||||
|
||||
// Some assistants start the chat under an id of its own, so what their answers received goes along. A chat without an id received nothing:
|
||||
if (this.ChatThread is { } assistantChat && convertedChatThread.ChatId != Guid.Empty && assistantChat.ChatId != convertedChatThread.ChatId)
|
||||
await ChatProvenance.CopyChatAsync(assistantChat.ChatId, convertedChatThread.ChatId);
|
||||
|
||||
MessageBus.INSTANCE.DeferMessage(this, Event.SEND_TO_CHAT, new ChatStartRequest(convertedChatThread));
|
||||
this.NavigationManager.NavigateTo(Routes.CHAT);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only components with a single session slot may be cleared as a group. The visual briefing
|
||||
// assistant keys its sessions per briefing, so clearing by component would discard the
|
||||
// status of every stored briefing instead of the one we are about to open.
|
||||
if (destination.HasSingleSessionSlot())
|
||||
await this.AssistantSessionService.ClearInactiveSessionsForComponentAsync(destination);
|
||||
|
||||
switch (destination)
|
||||
{
|
||||
case Tools.Components.CHAT:
|
||||
if (sendToButton.SendToChatAsInput)
|
||||
MessageBus.INSTANCE.DeferMessage(this, Event.SEND_TO_CHAT_INPUT, contentToSend);
|
||||
else
|
||||
{
|
||||
var convertedChatThread = this.ConvertToChatThread;
|
||||
convertedChatThread = convertedChatThread with { SelectedProvider = this.ProviderSettings.Id };
|
||||
|
||||
// Some assistants start the chat under an id of its own, so what their answers received goes along. A chat without an id received nothing:
|
||||
if (this.ChatThread is { } assistantChat && convertedChatThread.ChatId != Guid.Empty && assistantChat.ChatId != convertedChatThread.ChatId)
|
||||
await ChatProvenance.CopyChatAsync(assistantChat.ChatId, convertedChatThread.ChatId);
|
||||
|
||||
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, new ChatStartRequest(convertedChatThread));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, contentToSend);
|
||||
break;
|
||||
}
|
||||
|
||||
this.NavigationManager.NavigateTo(sendToData.Route);
|
||||
// A model wrote what an assistant sends, even when it falls back to the input, so it gets filtered, and none of its addresses counts as given:
|
||||
var payload = await this.AssistantLaunchService.CreateTextPayloadAsync(contentToSend, writtenByModel: true, this.CurrentRestrictions, this.Title);
|
||||
await this.AssistantLaunchService.LaunchAsync(destination, payload);
|
||||
}
|
||||
|
||||
private bool CanSendToAssistant(Tools.Components component)
|
||||
{
|
||||
if (!component.AllowSendTo())
|
||||
return false;
|
||||
|
||||
return this.SettingsManager.IsAssistantVisible(
|
||||
component,
|
||||
withLogging: false,
|
||||
requiredPreviewFeature: component.RequiredPreviewFeature());
|
||||
}
|
||||
|
||||
private async Task InnerResetForm()
|
||||
{
|
||||
if ((this.AssistantSessionService.TryGetSnapshot(this.assistantSessionKey)?.IsActive ?? false)
|
||||
@@ -908,6 +946,9 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
this.MediaTranscriptionService.ClearOwnerState(this.CurrentMediaImportOwner);
|
||||
this.assistantSessionId = null;
|
||||
this.ClearConversationState();
|
||||
|
||||
// The text sent here is gone with the form, and what it demanded goes with it:
|
||||
this.transferredRestrictions = ChatRestrictions.NONE;
|
||||
this.ProviderSettings = Settings.Provider.NONE;
|
||||
|
||||
await this.JsRuntime.ClearDiv(BEFORE_RESULT_DIV_ID);
|
||||
@@ -1149,6 +1190,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
state.Set(INPUT_ISSUES_STATE_KEY, this.InputIssues);
|
||||
state.Set(IS_PROCESSING_STATE_KEY, this.IsProcessing);
|
||||
state.Set(SELECTED_TOOL_IDS_STATE_KEY, this.SelectedToolIds);
|
||||
state.Set(TRANSFERRED_RESTRICTIONS_STATE_KEY, this.transferredRestrictions);
|
||||
this.CaptureCustomAssistantSessionState(state);
|
||||
|
||||
return state.ToDictionary();
|
||||
@@ -1177,6 +1219,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
reader.Restore(INPUT_ISSUES_STATE_KEY, value => this.InputIssues = value);
|
||||
reader.Restore(IS_PROCESSING_STATE_KEY, value => this.IsProcessing = value);
|
||||
reader.Restore(SELECTED_TOOL_IDS_STATE_KEY, value => this.SelectedToolIds = this.ToolRegistry.NormalizeSelection(value));
|
||||
reader.Restore(TRANSFERRED_RESTRICTIONS_STATE_KEY, value => this.transferredRestrictions = value);
|
||||
this.RestoreCustomAssistantSessionState(reader);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user