mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 10:57:59 +00:00
Allow assistants to run in the background (#828)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
fc53278c60
commit
508c7f9701
52 files changed
+2464
-168
No files matched your search
@@ -0,0 +1,34 @@
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies one logical assistant session slot.
|
||||
/// </summary>
|
||||
public readonly record struct AssistantSessionKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new assistant session key.
|
||||
/// </summary>
|
||||
/// <param name="component">The application component the assistant belongs to.</param>
|
||||
/// <param name="instanceId">The assistant-specific instance ID, such as a component type or plugin ID.</param>
|
||||
public AssistantSessionKey(Components component, string instanceId)
|
||||
{
|
||||
this.Component = component;
|
||||
this.InstanceId = instanceId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application component the assistant belongs to.
|
||||
/// </summary>
|
||||
public Components Component { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assistant-specific instance ID, such as a component type or plugin ID.
|
||||
/// </summary>
|
||||
public string InstanceId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the key into a compact diagnostic string.
|
||||
/// </summary>
|
||||
/// <returns>The component and instance ID joined by a colon.</returns>
|
||||
public override string ToString() => $"{this.Component}:{this.InstanceId}";
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using AIStudio.Chat;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps assistant sessions alive while their Blazor components are not mounted.
|
||||
/// </summary>
|
||||
/// <param name="messageBus">The message bus used to publish assistant session changes.</param>
|
||||
public sealed class AssistantSessionService(MessageBus messageBus)
|
||||
{
|
||||
/// <summary>
|
||||
/// Mutable runtime state owned exclusively by <see cref="AssistantSessionService"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type intentionally exists in addition to <see cref="AssistantSessionSnapshot"/>.
|
||||
/// It holds runtime-only data such as the cancellation token source and lock, while
|
||||
/// snapshots are copied DTOs for UI components and message bus payloads.
|
||||
/// </remarks>
|
||||
private sealed class AssistantSessionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies this concrete run of an assistant session.
|
||||
/// </summary>
|
||||
public required Guid SessionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the assistant and logical session slot this run belongs to.
|
||||
/// </summary>
|
||||
public required AssistantSessionKey Key { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the active assistant run.
|
||||
/// </summary>
|
||||
public required CancellationTokenSource CancellationTokenSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores when the session run started.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores when the session state was last changed.
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores when the session reached a terminal state.
|
||||
/// </summary>
|
||||
public DateTimeOffset? FinishedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the user-visible assistant title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the current lifecycle state of the session.
|
||||
/// </summary>
|
||||
public AssistantSessionStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the user-visible error message for failed sessions.
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Stores the current assistant chat thread, including streamed output.
|
||||
/// </summary>
|
||||
public ChatThread? ChatThread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores the assistant component state captured from the running UI instance.
|
||||
/// </summary>
|
||||
public Dictionary<string, IAssistantSessionSnapshotField> State { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Guards mutable fields while snapshots are created or updates are applied.
|
||||
/// </summary>
|
||||
public readonly Lock SyncRoot = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores one assistant session per session key.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<AssistantSessionKey, AssistantSessionState> sessions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether at least one assistant session is still active.
|
||||
/// </summary>
|
||||
public bool HasActiveSessions => this.sessions.Values.Any(session => session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING);
|
||||
|
||||
/// <summary>
|
||||
/// Gets copied snapshots for all known assistant sessions.
|
||||
/// </summary>
|
||||
/// <returns>Session snapshots ordered by newest update first.</returns>
|
||||
public IReadOnlyCollection<AssistantSessionSnapshot> GetSnapshots()
|
||||
{
|
||||
return this.sessions.Values
|
||||
.Select(CreateSnapshot)
|
||||
.OrderByDescending(snapshot => snapshot.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the current snapshot for an assistant session key.
|
||||
/// </summary>
|
||||
/// <param name="key">The assistant session key to look up.</param>
|
||||
/// <returns>The current snapshot, or <c>null</c> when no session exists.</returns>
|
||||
public AssistantSessionSnapshot? TryGetSnapshot(AssistantSessionKey key)
|
||||
{
|
||||
return this.sessions.TryGetValue(key, out var session) ? CreateSnapshot(session) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get and remove an inactive assistant session snapshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method intentionally does not publish a change event. It is used when
|
||||
/// a UI instance consumes a finished session exactly once and should keep the
|
||||
/// restored result locally until the user leaves or resets the assistant.
|
||||
/// </remarks>
|
||||
/// <param name="key">The assistant session key to look up and remove.</param>
|
||||
/// <returns>The removed inactive snapshot, or <c>null</c> when no inactive session exists.</returns>
|
||||
public AssistantSessionSnapshot? TryTakeInactiveSnapshot(AssistantSessionKey key)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(key, out var session))
|
||||
return null;
|
||||
|
||||
AssistantSessionSnapshot snapshot;
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
if (session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING)
|
||||
return null;
|
||||
|
||||
snapshot = CreateSnapshotWithoutLock(session);
|
||||
}
|
||||
|
||||
return ((ICollection<KeyValuePair<AssistantSessionKey, AssistantSessionState>>)this.sessions).Remove(new(key, session)) ? snapshot : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new assistant session when no active session exists for the key.
|
||||
/// </summary>
|
||||
/// <param name="key">The assistant session key.</param>
|
||||
/// <param name="title">The user-visible assistant title.</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source owned by the new runtime session.</param>
|
||||
/// <param name="chatThread">The current assistant chat thread, if one already exists.</param>
|
||||
/// <param name="state">The initial assistant component state.</param>
|
||||
/// <param name="sendingComponent">The component that initiated the session start.</param>
|
||||
/// <returns>The new session snapshot, or the existing active session snapshot.</returns>
|
||||
public async Task<AssistantSessionSnapshot> TryBeginAsync(AssistantSessionKey key, string title, CancellationTokenSource cancellationTokenSource, ChatThread? chatThread, Dictionary<string, IAssistantSessionSnapshotField> state, ComponentBase? sendingComponent = null)
|
||||
{
|
||||
if (this.sessions.TryGetValue(key, out var existing) && existing.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING)
|
||||
return CreateSnapshot(existing);
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
var session = new AssistantSessionState
|
||||
{
|
||||
SessionId = Guid.NewGuid(),
|
||||
Key = key,
|
||||
CancellationTokenSource = cancellationTokenSource,
|
||||
StartedAt = now,
|
||||
UpdatedAt = now,
|
||||
Title = title,
|
||||
Status = AssistantSessionStatus.RUNNING,
|
||||
ChatThread = chatThread,
|
||||
State = state,
|
||||
};
|
||||
|
||||
this.sessions[key] = session;
|
||||
var snapshot = CreateSnapshot(session);
|
||||
await this.NotifyChangedAsync(session, sendingComponent);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a running assistant session with the latest UI and chat state.
|
||||
/// </summary>
|
||||
/// <param name="key">The assistant session key.</param>
|
||||
/// <param name="sessionId">The concrete run ID that is allowed to write the checkpoint.</param>
|
||||
/// <param name="title">The current user-visible assistant title.</param>
|
||||
/// <param name="chatThread">The current assistant chat thread.</param>
|
||||
/// <param name="state">The current assistant component state.</param>
|
||||
/// <param name="sendingComponent">The component that initiated the checkpoint.</param>
|
||||
public async Task CheckpointAsync(AssistantSessionKey key, Guid sessionId, string title, ChatThread? chatThread, Dictionary<string, IAssistantSessionSnapshotField> state, ComponentBase? sendingComponent = null)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(key, out var session))
|
||||
return;
|
||||
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
if (session.SessionId != sessionId)
|
||||
return;
|
||||
|
||||
session.Title = title;
|
||||
session.ChatThread = chatThread;
|
||||
session.State = state;
|
||||
session.UpdatedAt = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
await this.NotifyChangedAsync(session, sendingComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests cancellation for an active assistant session.
|
||||
/// </summary>
|
||||
/// <param name="key">The assistant session key to cancel.</param>
|
||||
/// <param name="sendingComponent">The component that initiated the cancellation.</param>
|
||||
public async Task CancelAsync(AssistantSessionKey key, ComponentBase? sendingComponent = null)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(key, out var session))
|
||||
return;
|
||||
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
if (session.Status is not AssistantSessionStatus.RUNNING)
|
||||
return;
|
||||
|
||||
session.Status = AssistantSessionStatus.CANCELING;
|
||||
session.UpdatedAt = DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!session.CancellationTokenSource.IsCancellationRequested)
|
||||
await session.CancellationTokenSource.CancelAsync();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this.NotifyChangedAsync(session, sendingComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves an assistant session into a terminal state and publishes completion.
|
||||
/// </summary>
|
||||
/// <param name="key">The assistant session key.</param>
|
||||
/// <param name="sessionId">The concrete run ID that is allowed to complete the session.</param>
|
||||
/// <param name="status">The terminal status to store.</param>
|
||||
/// <param name="errorMessage">The user-visible error message for failed sessions.</param>
|
||||
/// <param name="chatThread">The final assistant chat thread.</param>
|
||||
/// <param name="state">The final assistant component state.</param>
|
||||
/// <param name="sendingComponent">The component that initiated the completion.</param>
|
||||
public async Task CompleteAsync(AssistantSessionKey key, Guid sessionId, AssistantSessionStatus status, string errorMessage, ChatThread? chatThread, Dictionary<string, IAssistantSessionSnapshotField> state, ComponentBase? sendingComponent = null)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(key, out var session))
|
||||
return;
|
||||
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
if (session.SessionId != sessionId)
|
||||
return;
|
||||
|
||||
session.Status = status;
|
||||
session.ErrorMessage = errorMessage;
|
||||
session.ChatThread = chatThread;
|
||||
session.State = state;
|
||||
session.UpdatedAt = DateTimeOffset.Now;
|
||||
session.FinishedAt = session.UpdatedAt;
|
||||
}
|
||||
|
||||
await this.NotifyChangedAsync(session, sendingComponent);
|
||||
await messageBus.SendMessage(sendingComponent, Event.ASSISTANT_SESSION_FINISHED, CreateSnapshot(session));
|
||||
|
||||
try
|
||||
{
|
||||
session.CancellationTokenSource.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears an inactive assistant session.
|
||||
/// </summary>
|
||||
/// <param name="key">The assistant session key to clear.</param>
|
||||
public async Task ClearAsync(AssistantSessionKey key)
|
||||
{
|
||||
if (!this.sessions.TryGetValue(key, out var session))
|
||||
return;
|
||||
|
||||
if (session.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING)
|
||||
return;
|
||||
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
session.Status = AssistantSessionStatus.NONE;
|
||||
session.ChatThread = null;
|
||||
session.State = new(StringComparer.Ordinal);
|
||||
session.UpdatedAt = DateTimeOffset.Now;
|
||||
session.FinishedAt = session.UpdatedAt;
|
||||
}
|
||||
|
||||
this.sessions.TryRemove(key, out _);
|
||||
await messageBus.SendMessage(null, Event.ASSISTANT_SESSION_CHANGED, CreateSnapshot(session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all inactive sessions for a component.
|
||||
/// </summary>
|
||||
/// <param name="component">The component whose inactive sessions should be cleared.</param>
|
||||
public async Task ClearInactiveSessionsForComponentAsync(Components component)
|
||||
{
|
||||
var matchingKeys = this.sessions
|
||||
.Where(pair => pair.Key.Component == component && pair.Value.Status is not AssistantSessionStatus.RUNNING and not AssistantSessionStatus.CANCELING)
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in matchingKeys)
|
||||
await this.ClearAsync(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an assistant session change event.
|
||||
/// </summary>
|
||||
/// <param name="session">The runtime session whose copied snapshot should be published.</param>
|
||||
/// <param name="sendingComponent">The component that initiated the session change.</param>
|
||||
private async Task NotifyChangedAsync(AssistantSessionState session, ComponentBase? sendingComponent = null)
|
||||
{
|
||||
await messageBus.SendMessage(sendingComponent, Event.ASSISTANT_SESSION_CHANGED, CreateSnapshot(session));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copied, external snapshot from the internal runtime state.
|
||||
/// </summary>
|
||||
/// <param name="session">The runtime session to copy.</param>
|
||||
/// <returns>A snapshot safe to send to UI components.</returns>
|
||||
private static AssistantSessionSnapshot CreateSnapshot(AssistantSessionState session)
|
||||
{
|
||||
lock (session.SyncRoot)
|
||||
{
|
||||
return CreateSnapshotWithoutLock(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copied, external snapshot while the caller already holds the session lock.
|
||||
/// </summary>
|
||||
/// <param name="session">The runtime session to copy.</param>
|
||||
/// <returns>A snapshot safe to send to UI components.</returns>
|
||||
private static AssistantSessionSnapshot CreateSnapshotWithoutLock(AssistantSessionState session)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
SessionId = session.SessionId,
|
||||
Key = session.Key,
|
||||
Title = session.Title,
|
||||
Status = session.Status,
|
||||
StartedAt = session.StartedAt,
|
||||
UpdatedAt = session.UpdatedAt,
|
||||
FinishedAt = session.FinishedAt,
|
||||
ErrorMessage = session.ErrorMessage,
|
||||
ChatThread = session.ChatThread,
|
||||
State = new Dictionary<string, IAssistantSessionSnapshotField>(session.State, StringComparer.Ordinal),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable-style view of an assistant session for UI consumers and message bus events.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The service creates snapshots by copying its internal runtime state. Consumers must
|
||||
/// treat the contained chat thread and state objects as read-only views of the session.
|
||||
/// </remarks>
|
||||
public sealed record AssistantSessionSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the concrete run represented by this snapshot.
|
||||
/// </summary>
|
||||
public required Guid SessionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the assistant and logical session slot represented by this snapshot.
|
||||
/// </summary>
|
||||
public required AssistantSessionKey Key { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user-visible assistant title.
|
||||
/// </summary>
|
||||
public required string Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current lifecycle status.
|
||||
/// </summary>
|
||||
public required AssistantSessionStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when the session run started.
|
||||
/// </summary>
|
||||
public required DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when the session was last changed.
|
||||
/// </summary>
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when the session reached a terminal state.
|
||||
/// </summary>
|
||||
public DateTimeOffset? FinishedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user-visible error message for failed sessions.
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assistant chat thread captured for this session.
|
||||
/// </summary>
|
||||
public ChatThread? ChatThread { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assistant component state captured for this session.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IAssistantSessionSnapshotField> State { get; init; } = new Dictionary<string, IAssistantSessionSnapshotField>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the session is still running or canceling.
|
||||
/// </summary>
|
||||
public bool IsActive => this.Status is AssistantSessionStatus.RUNNING or AssistantSessionStatus.CANCELING;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Stores one typed value in an assistant session snapshot.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The captured value type.</typeparam>
|
||||
public sealed record AssistantSessionSnapshotField<T> : IAssistantSessionSnapshotField
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new typed snapshot field.
|
||||
/// </summary>
|
||||
/// <param name="value">The captured value.</param>
|
||||
public AssistantSessionSnapshotField(T value)
|
||||
{
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the captured value.
|
||||
/// </summary>
|
||||
public T Value { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type ValueType => typeof(T);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryRead<TValue>(out TValue? value)
|
||||
{
|
||||
if (this.Value is TValue typedValue)
|
||||
{
|
||||
value = typedValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.Value is null && default(TValue) is null)
|
||||
{
|
||||
value = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a typed assistant session state value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type stored for this key.</typeparam>
|
||||
public readonly record struct AssistantSessionStateKey<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new assistant session state key.
|
||||
/// </summary>
|
||||
/// <param name="name">The stable dictionary name used in assistant session snapshots.</param>
|
||||
public AssistantSessionStateKey(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stable dictionary name used in assistant session snapshots.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stable dictionary name.
|
||||
/// </summary>
|
||||
/// <returns>The stable dictionary name.</returns>
|
||||
public override string ToString() => this.Name;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Restores typed assistant session state values from a snapshot.
|
||||
/// </summary>
|
||||
/// <param name="fields">The captured snapshot fields.</param>
|
||||
/// <param name="assistantTitle">The user-visible assistant title.</param>
|
||||
public sealed class AssistantSessionStateReader(IReadOnlyDictionary<string, IAssistantSessionSnapshotField> fields, string assistantTitle)
|
||||
{
|
||||
private static readonly ILogger<AssistantSessionStateReader> LOG = Program.LOGGER_FACTORY.CreateLogger<AssistantSessionStateReader>();
|
||||
|
||||
/// <summary>
|
||||
/// Restores a typed value when it exists in the snapshot.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="apply">The action that applies the restored value.</param>
|
||||
public void Restore<T>(AssistantSessionStateKey<T> key, Action<T> apply)
|
||||
{
|
||||
if (this.TryRead(key, out var value))
|
||||
apply(value!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a list into an existing list instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The list item type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="target">The existing list to update.</param>
|
||||
public void RestoreList<T>(AssistantSessionStateKey<List<T>> key, List<T> target)
|
||||
{
|
||||
this.Restore(key, values =>
|
||||
{
|
||||
target.Clear();
|
||||
target.AddRange(values);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a hash set into an existing hash set instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The set item type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="target">The existing hash set to update.</param>
|
||||
public void RestoreHashSet<T>(AssistantSessionStateKey<HashSet<T>> key, HashSet<T> target)
|
||||
{
|
||||
this.Restore(key, values =>
|
||||
{
|
||||
target.Clear();
|
||||
target.UnionWith(values);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a dictionary into an existing dictionary instance.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The dictionary key type.</typeparam>
|
||||
/// <typeparam name="TValue">The dictionary value type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="target">The existing dictionary to update.</param>
|
||||
public void RestoreDictionary<TKey, TValue>(AssistantSessionStateKey<Dictionary<TKey, TValue>> key, Dictionary<TKey, TValue> target) where TKey : notnull
|
||||
{
|
||||
this.Restore(key, values =>
|
||||
{
|
||||
target.Clear();
|
||||
foreach (var (itemKey, itemValue) in values)
|
||||
target[itemKey] = itemValue;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores text into an existing string builder instance.
|
||||
/// </summary>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="target">The existing string builder to update.</param>
|
||||
public void RestoreStringBuilder(AssistantSessionStateKey<string> key, StringBuilder target)
|
||||
{
|
||||
this.Restore(key, value =>
|
||||
{
|
||||
target.Clear();
|
||||
target.Append(value);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read a typed value from the snapshot.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The requested value type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="value">The restored value when reading succeeds.</param>
|
||||
/// <returns><c>true</c> when a value exists and matches the requested type; otherwise, <c>false</c>.</returns>
|
||||
private bool TryRead<T>(AssistantSessionStateKey<T> key, out T? value)
|
||||
{
|
||||
value = default;
|
||||
if (!fields.TryGetValue(key.Name, out var field))
|
||||
return false;
|
||||
|
||||
if (field.TryRead(out value))
|
||||
return true;
|
||||
|
||||
LOG.LogWarning(
|
||||
"Could not restore assistant session field '{FieldStateKey}' for assistant '{AssistantTitle}'. ExpectedType='{ExpectedType}', CapturedValueType='{CapturedValueType}'. The current value is kept.",
|
||||
key.Name,
|
||||
assistantTitle,
|
||||
typeof(T).FullName,
|
||||
field.ValueType.FullName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Collects typed assistant session state values for a snapshot.
|
||||
/// </summary>
|
||||
public sealed class AssistantSessionStateWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores captured fields by their stable dictionary names.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IAssistantSessionSnapshotField> fields = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a typed state value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="value">The captured value.</param>
|
||||
public void Set<T>(AssistantSessionStateKey<T> key, T value)
|
||||
{
|
||||
this.fields[key.Name] = new AssistantSessionSnapshotField<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a list copy.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The list item type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="values">The values to copy.</param>
|
||||
public void SetList<T>(AssistantSessionStateKey<List<T>> key, IEnumerable<T> values)
|
||||
{
|
||||
this.Set(key, values.ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a hash set copy.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The set item type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="values">The values to copy.</param>
|
||||
public void SetHashSet<T>(AssistantSessionStateKey<HashSet<T>> key, IEnumerable<T> values)
|
||||
{
|
||||
this.Set(key, values.ToHashSet());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a dictionary copy.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The dictionary key type.</typeparam>
|
||||
/// <typeparam name="TValue">The dictionary value type.</typeparam>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="values">The values to copy.</param>
|
||||
public void SetDictionary<TKey, TValue>(AssistantSessionStateKey<Dictionary<TKey, TValue>> key, IDictionary<TKey, TValue> values) where TKey : notnull
|
||||
{
|
||||
this.Set(key, new Dictionary<TKey, TValue>(values));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the current text from a string builder.
|
||||
/// </summary>
|
||||
/// <param name="key">The typed state key.</param>
|
||||
/// <param name="value">The string builder to read.</param>
|
||||
public void SetStringBuilder(AssistantSessionStateKey<string> key, StringBuilder value)
|
||||
{
|
||||
this.Set(key, value.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the captured fields as a dictionary.
|
||||
/// </summary>
|
||||
/// <returns>A copied dictionary containing the captured fields.</returns>
|
||||
public Dictionary<string, IAssistantSessionSnapshotField> ToDictionary()
|
||||
{
|
||||
return new(this.fields, StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the lifecycle state of an assistant session.
|
||||
/// </summary>
|
||||
public enum AssistantSessionStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// No session state is available.
|
||||
/// </summary>
|
||||
NONE,
|
||||
|
||||
/// <summary>
|
||||
/// The assistant session is running.
|
||||
/// </summary>
|
||||
RUNNING,
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation was requested and the assistant is shutting down.
|
||||
/// </summary>
|
||||
CANCELING,
|
||||
|
||||
/// <summary>
|
||||
/// The assistant session completed successfully.
|
||||
/// </summary>
|
||||
COMPLETED,
|
||||
|
||||
/// <summary>
|
||||
/// The assistant session was canceled.
|
||||
/// </summary>
|
||||
CANCELED,
|
||||
|
||||
/// <summary>
|
||||
/// The assistant session failed.
|
||||
/// </summary>
|
||||
FAILED,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace AIStudio.Tools.AssistantSessions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a typed value stored in an assistant session snapshot.
|
||||
/// </summary>
|
||||
public interface IAssistantSessionSnapshotField
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type used when the value was captured.
|
||||
/// </summary>
|
||||
Type ValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the captured value as the requested type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The requested value type.</typeparam>
|
||||
/// <param name="value">The typed value when reading succeeds.</param>
|
||||
/// <returns><c>true</c> when the captured value matches <typeparamref name="T"/>; otherwise, <c>false</c>.</returns>
|
||||
bool TryRead<T>(out T? value);
|
||||
}
|
||||
@@ -144,6 +144,16 @@ public enum Event
|
||||
/// Notifies receivers that chat generation state changed.
|
||||
/// </summary>
|
||||
CHAT_GENERATION_CHANGED,
|
||||
|
||||
/// <summary>
|
||||
/// Notifies receivers that an assistant session changed.
|
||||
/// </summary>
|
||||
ASSISTANT_SESSION_CHANGED,
|
||||
|
||||
/// <summary>
|
||||
/// Notifies receivers that an assistant session finished.
|
||||
/// </summary>
|
||||
ASSISTANT_SESSION_FINISHED,
|
||||
|
||||
// Workspace events:
|
||||
/// <summary>
|
||||
|
||||
@@ -9,4 +9,14 @@ public static class MudThemeExtensions
|
||||
true => theme.PaletteDark,
|
||||
false => theme.PaletteLight,
|
||||
};
|
||||
|
||||
public static string GetActivityIndicatorColor(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch
|
||||
{
|
||||
true => theme.GetActivityIndicatorDarkColor(),
|
||||
false => theme.GetActivityIndicatorLightColor(),
|
||||
};
|
||||
|
||||
public static string GetActivityIndicatorLightColor(this MudTheme theme) => theme.PaletteLight.Info.Value;
|
||||
|
||||
public static string GetActivityIndicatorDarkColor(this MudTheme theme) => theme.PaletteDark.InfoLighten;
|
||||
}
|
||||
@@ -30,6 +30,49 @@ public sealed class AssistantState
|
||||
this.Times.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all dynamic assistant state values from another state instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The state instance to copy from.</param>
|
||||
public void CopyFrom(AssistantState other)
|
||||
{
|
||||
this.Clear();
|
||||
CopyDictionary(other.Text, this.Text);
|
||||
CopyDictionary(other.SingleSelect, this.SingleSelect);
|
||||
CopyDictionary(other.MultiSelect, this.MultiSelect);
|
||||
CopyDictionary(other.Booleans, this.Booleans);
|
||||
CopyDictionary(other.WebContent, this.WebContent);
|
||||
CopyDictionary(other.FileContent, this.FileContent);
|
||||
CopyDictionary(other.Colors, this.Colors);
|
||||
CopyDictionary(other.Dates, this.Dates);
|
||||
CopyDictionary(other.DateRanges, this.DateRanges);
|
||||
CopyDictionary(other.Times, this.Times);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the dynamic assistant state.
|
||||
/// </summary>
|
||||
/// <returns>A copied assistant state instance.</returns>
|
||||
public AssistantState Clone()
|
||||
{
|
||||
var clone = new AssistantState();
|
||||
clone.CopyFrom(this);
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all entries from one dictionary into another dictionary.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The dictionary key type.</typeparam>
|
||||
/// <typeparam name="TValue">The dictionary value type.</typeparam>
|
||||
/// <param name="source">The source dictionary.</param>
|
||||
/// <param name="target">The target dictionary.</param>
|
||||
private static void CopyDictionary<TKey, TValue>(Dictionary<TKey, TValue> source, Dictionary<TKey, TValue> target) where TKey : notnull
|
||||
{
|
||||
foreach (var (key, value) in source)
|
||||
target[key] = value;
|
||||
}
|
||||
|
||||
public bool TryApplyValue(string fieldName, LuaValue value, out string expectedType)
|
||||
{
|
||||
expectedType = string.Empty;
|
||||
|
||||
Reference in new issue
Block a user