Improved recompile logic to use AI Studio export pipeline

This commit is contained in:
Thorsten Sommer 2026-07-30 21:52:09 +02:00
parent a921cd9305
commit 868a8b1cf3
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
4 changed files with 84 additions and 44 deletions

View File

@ -182,7 +182,7 @@
</span>
</MudTooltip>
<MudTooltip Text="@(this.SelectedVersionSupportsEdits
? T("Recompile this version with the current AI Studio compiler and runtime without model calls.")
? T("Recompile this version with the current AI Studio version without AI model calls.")
: T("This version has no compatible semantic artifacts. Rebuild the briefing instead."))">
<span>
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Code" OnClick="@(() => this.RecompileAsync())" Disabled="@this.CannotRecompile">@T("Recompile briefing")</MudButton>

View File

@ -193,7 +193,7 @@ public partial class VisualBriefingAssistant
}
/// <summary>
/// Recompiles the selected immutable revision with the current compiler and runtime.
/// Recompiles the selected immutable revision with the current AI Studio export pipeline.
/// </summary>
/// <param name="parentRevisionOverride">An optional parent used while resuming a persisted operation.</param>
private async Task RecompileAsync(Guid? parentRevisionOverride = null)
@ -234,13 +234,6 @@ public partial class VisualBriefingAssistant
this.lastBuildDiagnostics = result.Diagnostics;
this.latestBuild = this.BuildProgressService.GetLatest(briefingId) ?? (await this.Store.ListBuildsAsync(briefingId, cancellation.Token)).FirstOrDefault();
if (result.FailureCode is VisualBriefingFailureCode.NO_CHANGES)
{
this.Snackbar.Add(T("The selected briefing version already uses the current compiler and runtime."), Severity.Info);
terminalStatus = AssistantSessionStatus.COMPLETED;
return;
}
if (!result.Success || result.Version is null)
{
terminalIssue = result.Issue;
@ -265,7 +258,7 @@ public partial class VisualBriefingAssistant
];
}
this.Snackbar.Add(T("The briefing was recompiled with the current AI Studio runtime."), Severity.Success);
this.Snackbar.Add(T("The briefing was recompiled with the current AI Studio version."), Severity.Success);
terminalStatus = AssistantSessionStatus.COMPLETED;
}
catch (OperationCanceledException)

View File

@ -5,8 +5,8 @@ namespace AIStudio.Assistants.VisualBriefing;
internal sealed partial class VisualBriefingBuildOrchestrator
{
/// <summary>
/// Recompiles one immutable revision with the current deterministic compiler and standalone
/// runtime without accessing sources or calling a model.
/// Recompiles one immutable revision with the current deterministic export pipeline without
/// accessing sources or calling a model.
/// </summary>
/// <param name="manifest">The current local briefing manifest.</param>
/// <param name="parentRevisionId">The revision whose semantic artifacts are reused.</param>
@ -144,6 +144,8 @@ internal sealed partial class VisualBriefingBuildOrchestrator
compiled.TemplateHtml, compiled.Css,
content.Charts.Count > 0));
var contributions = await this.ResolveRecompileModelContributionsAsync(manifest.BriefingId, parentVersion, evidence, plan, content, previousPresentation, token);
var presentationModel = contributions.First(contribution => contribution.Role is VisualBriefingModelRole.DESIGN).Model;
var presentation = new VisualBriefingPresentationArtifact
{
ArtifactId = Guid.NewGuid(),
@ -160,7 +162,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
Css = compiled.Css,
TemplateHash = compiled.TemplateHash,
CssHash = compiled.CssHash,
Model = previousPresentation.Model,
Model = presentationModel,
};
await this.store.WritePresentationArtifactAsync(manifest.BriefingId, presentation, token);
@ -204,7 +206,6 @@ internal sealed partial class VisualBriefingBuildOrchestrator
await this.store.SaveBuildAsync(build, token);
this.progressService.Publish(build);
var contributions = parentVersion.ModelContributions.ToList();
var revision = await this.store.AddRevisionAsync(new(
manifest.BriefingId,
parentRevisionId,
@ -230,35 +231,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
var commitStage = GetStage(build, VisualBriefingBuildStage.COMMIT);
if (!revision.Success || revision.Version is null)
{
if (revision.Issue.Contains("did not change", StringComparison.OrdinalIgnoreCase))
{
assemblyStage.Status = VisualBriefingBuildStageStatus.COMPLETED;
assemblyStage.FinishedAtUtc = DateTimeOffset.UtcNow;
assemblyStage.OutputHash = parentVersion.PayloadHash;
MarkSkipped(build, VisualBriefingBuildStage.COMMIT, parentVersion.PayloadHash);
build.Status = VisualBriefingBuildStatus.COMPLETED;
build.Failure = null;
build.UpdatedAtUtc = DateTimeOffset.UtcNow;
await this.store.SaveBuildAsync(build, token);
this.progressService.Publish(build);
diagnostics.FailureCode = VisualBriefingFailureCode.NO_CHANGES;
diagnostics.FinishedAtUtc = DateTimeOffset.UtcNow;
return new(
false,
null,
"The selected briefing version already uses the current compiler and runtime.",
VisualBriefingFailureCode.NO_CHANGES,
diagnostics,
false);
}
throw new VisualBriefingBuildException(
VisualBriefingFailureCode.STORE_FAILED,
VisualBriefingBuildStage.COMMIT,
revision.Issue,
"The immutable recompiled revision commit was rejected.");
}
throw new VisualBriefingBuildException(VisualBriefingFailureCode.STORE_FAILED, VisualBriefingBuildStage.COMMIT, revision.Issue, "The immutable recompiled revision commit was rejected.");
assemblyStage.Status = VisualBriefingBuildStageStatus.COMPLETED;
assemblyStage.FinishedAtUtc = DateTimeOffset.UtcNow;
@ -341,4 +314,77 @@ internal sealed partial class VisualBriefingBuildOrchestrator
gate.Release();
}
}
/// <summary>
/// Reconstructs the most specific model attribution available for each reused semantic artifact.
/// </summary>
private async Task<List<VisualBriefingModelContribution>> ResolveRecompileModelContributionsAsync(Guid briefingId, VisualBriefingVersion parentVersion,
VisualBriefingEvidenceArtifact evidence, VisualBriefingPlanArtifact plan, VisualBriefingContentArtifact content, VisualBriefingPresentationArtifact presentation,
CancellationToken token)
{
var builds = await this.store.ListBuildsAsync(briefingId, token);
return
[
new(
VisualBriefingModelRole.EVIDENCE,
ResolveRecompileModelLabel(
builds,
build => build.EvidenceArtifactId,
evidence.ArtifactId,
VisualBriefingBuildStage.EVIDENCE,
ExistingModelLabel(parentVersion, VisualBriefingModelRole.EVIDENCE, evidence.Model))),
new(
VisualBriefingModelRole.PLAN,
ResolveRecompileModelLabel(
builds,
build => build.PlanArtifactId,
plan.ArtifactId,
VisualBriefingBuildStage.PLAN,
ExistingModelLabel(parentVersion, VisualBriefingModelRole.PLAN, plan.Model))),
new(
VisualBriefingModelRole.CONTENT,
ResolveRecompileModelLabel(
builds,
build => build.ContentArtifactId,
content.ArtifactId,
VisualBriefingBuildStage.CONTENT,
ExistingModelLabel(parentVersion, VisualBriefingModelRole.CONTENT, content.Model))),
new(
VisualBriefingModelRole.DESIGN,
ResolveRecompileModelLabel(
builds,
build => build.PresentationArtifactId,
presentation.ArtifactId,
VisualBriefingBuildStage.DESIGN,
ExistingModelLabel(parentVersion, VisualBriefingModelRole.DESIGN, presentation.Model))),
];
}
/// <summary>
/// Resolves the provider and model that originally produced one immutable artifact.
/// </summary>
private static string ResolveRecompileModelLabel(IReadOnlyList<VisualBriefingBuildRecord> builds, Func<VisualBriefingBuildRecord, Guid?> artifactId,
Guid expectedArtifactId, VisualBriefingBuildStage stage, string fallback)
{
var producingBuild = builds.FirstOrDefault(build =>
artifactId(build) == expectedArtifactId &&
!string.IsNullOrWhiteSpace(build.ProviderFamily) &&
!string.IsNullOrWhiteSpace(build.Model) &&
build.Stages.Any(candidate => candidate.Stage == stage && candidate.Status is VisualBriefingBuildStageStatus.COMPLETED));
return producingBuild is null ? fallback : VisualBriefingModelNames.ExportLabel(producingBuild.ProviderFamily, producingBuild.Model);
}
/// <summary>
/// Returns the persisted role attribution, falling back to the immutable artifact label.
/// </summary>
private static string ExistingModelLabel(VisualBriefingVersion parentVersion, VisualBriefingModelRole role, string artifactModel)
{
var contribution = parentVersion.ModelContributions.FirstOrDefault(candidate => candidate.Role == role && !string.IsNullOrWhiteSpace(candidate.Model));
return contribution?.Model ?? artifactModel;
}
}

View File

@ -86,7 +86,8 @@ public sealed partial class VisualBriefingStore
!string.Equals(parent.AssetHash, hashes.AssetHash, StringComparison.Ordinal)))
return VisualBriefingRevisionResult.Failure("A recompile attempted to modify semantic artifacts or embedded assets.");
if (string.Equals(parent.DataHash, hashes.DataHash, StringComparison.Ordinal) &&
if (request.EditMode is not VisualBriefingEditMode.RECOMPILE &&
string.Equals(parent.DataHash, hashes.DataHash, StringComparison.Ordinal) &&
string.Equals(parent.AssetHash, hashes.AssetHash, StringComparison.Ordinal) &&
string.Equals(parent.TemplateHash, hashes.TemplateHash, StringComparison.Ordinal) &&
string.Equals(parent.CssHash, hashes.CssHash, StringComparison.Ordinal) &&