Added the Model Comparison assistant for blind comparisons of two models (#1009)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Dominic NeuburgandThorsten Sommer authored and GitHub committed 2026-11-14 14:11:34 +01:00
1 parent 12f26445ba
commit b7255694cd
221 files changed
+19989 -90

No files matched your search

@@ -1,4 +1,5 @@
using AIStudio.Assistants.BeyondFactsLab;
using AIStudio.Assistants.ModelComparison;
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools.DataBackup;
@@ -11,10 +12,12 @@ namespace AIStudio.Tools.Services;
/// One-shot startup service that removes from the provenance store what no stored chat refers to, see ProvenanceCleanup.
/// </summary>
/// <remarks>
/// It knows the chats of the workspaces, the temporary chats, and the chats of the BeyondFacts Lab. It reads the
/// latter also while the lab is switched off: otherwise, they would lose what they received at every start.
/// It knows the chats of the workspaces, the temporary chats, the chats of the BeyondFacts Lab, and those of the
/// comparison series of the Model Comparison assistant. It reads the chats of the lab also while the lab is switched
/// off, and those of the series also while the assistant is hidden: otherwise, they would lose what they received at
/// every start.
/// </remarks>
public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClientProvider, DataRestoreService dataRestoreService, BeyondFactsLabStore labStore, ILogger<ProvenanceCleanupService> logger) : BackgroundService
public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClientProvider, DataRestoreService dataRestoreService, BeyondFactsLabStore labStore, ModelComparisonStore comparisonStore, ILogger<ProvenanceCleanupService> logger) : BackgroundService
{
/// <summary>
/// When this run of the app started. Entries stored since belong to it and stay.
@@ -103,6 +106,9 @@ public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClie
foreach (var (chatId, labThread) in labStore.ReadAllThreadLocations())
locations[chatId] = StoredChatLocation.InLab(labThread);
foreach (var (chatId, seriesId) in comparisonStore.ReadAllSeriesChatLocations())
locations[chatId] = StoredChatLocation.InComparisonSeries(seriesId);
return locations;
}
@@ -114,17 +120,19 @@ public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClie
/// <returns>The chat, or null when it cannot be read.</returns>
private async Task<ChatThread?> LoadChatAsync(StoredChatLocation location, Guid chatId)
{
if (location.LabThread is not { } labThread)
if (location.LabThread is null && location.ComparisonSeriesId is null)
return await WorkspaceBehaviour.LoadChatAsync(new(location.WorkspaceId, chatId));
try
{
return await labStore.LoadThreadAsync(labThread);
return location.LabThread is { } labThread
? await labStore.LoadThreadAsync(labThread)
: await comparisonStore.LoadSeriesChatAsync(location.ComparisonSeriesId!.Value, chatId);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException)
{
// A chat which cannot be read keeps everything it received:
logger.LogDebug(exception, "The provenance startup cleanup could not read a chat of the BeyondFacts Lab.");
logger.LogDebug(exception, "The provenance startup cleanup could not read a chat of an assistant.");
return null;
}
}
@@ -135,7 +143,11 @@ public sealed class ProvenanceCleanupService(DatabaseClientProvider databaseClie
/// <param name="location">The place of the chat.</param>
/// <param name="chatId">The ID of the chat.</param>
/// <returns>The time, or null when it is not known.</returns>
private DateTimeOffset? GetChatWrittenAt(StoredChatLocation location, Guid chatId) => location.LabThread is { } labThread
? labStore.GetThreadWrittenAt(labThread)
: WorkspaceBehaviour.GetChatWrittenAt(location.WorkspaceId, chatId);
private DateTimeOffset? GetChatWrittenAt(StoredChatLocation location, Guid chatId) => location switch
{
{ LabThread: { } labThread } => labStore.GetThreadWrittenAt(labThread),
{ ComparisonSeriesId: { } seriesId } => comparisonStore.GetSeriesChatWrittenAt(seriesId, chatId),
_ => WorkspaceBehaviour.GetChatWrittenAt(location.WorkspaceId, chatId),
};
}
@@ -4,14 +4,15 @@ namespace AIStudio.Tools.Services;
/// <summary>
/// Where the cleanup of the provenance store at the start finds a chat: in a workspace, among the temporary chats,
/// or in the BeyondFacts Lab.
/// in the BeyondFacts Lab, or in a comparison series of the Model Comparison assistant.
/// </summary>
/// <remarks>
/// Create it with InWorkspace or InLab.
/// Create it with InWorkspace, InLab, or InComparisonSeries.
/// </remarks>
/// <param name="WorkspaceId">The workspace of a chat outside the lab, Guid.Empty for a temporary chat.</param>
/// <param name="WorkspaceId">The workspace of a chat in a workspace, Guid.Empty for a temporary chat or a chat of an assistant.</param>
/// <param name="LabThread">Where a chat of the lab belongs, or null for any other chat.</param>
public readonly record struct StoredChatLocation(Guid WorkspaceId, BflThreadLocation? LabThread)
/// <param name="ComparisonSeriesId">The comparison series of a chat of the Model Comparison assistant, or null for any other chat.</param>
public readonly record struct StoredChatLocation(Guid WorkspaceId, BflThreadLocation? LabThread, Guid? ComparisonSeriesId = null)
{
/// <summary>
/// Gets the place of a chat in a workspace or among the temporary chats.
@@ -24,4 +25,10 @@ public readonly record struct StoredChatLocation(Guid WorkspaceId, BflThreadLoca
/// </summary>
/// <param name="thread">Where the chat belongs in the lab.</param>
public static StoredChatLocation InLab(BflThreadLocation thread) => new(Guid.Empty, thread);
/// <summary>
/// Gets the place of a chat of a comparison series of the Model Comparison assistant.
/// </summary>
/// <param name="seriesId">The ID of the series.</param>
public static StoredChatLocation InComparisonSeries(Guid seriesId) => new(Guid.Empty, null, seriesId);
}