Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added MongoPackRunApprovalStore for managing approval states with MongoDB. - Introduced MongoPackRunArtifactUploader for uploading and storing artifacts. - Created MongoPackRunLogStore to handle logging of pack run events. - Developed MongoPackRunStateStore for persisting and retrieving pack run states. - Implemented unit tests for MongoDB stores to ensure correct functionality. - Added MongoTaskRunnerTestContext for setting up MongoDB test environment. - Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
@@ -24,9 +24,10 @@ using Spectre.Console.Rendering;
|
||||
using StellaOps.Auth.Client;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Prompts;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.Cli.Services;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
using StellaOps.Cli.Services.Models.AdvisoryAi;
|
||||
using StellaOps.Cli.Telemetry;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Kms;
|
||||
|
||||
@@ -426,14 +427,154 @@ internal static class CommandHandlers
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task HandleSourcesIngestAsync(
|
||||
IServiceProvider services,
|
||||
bool dryRun,
|
||||
string source,
|
||||
string input,
|
||||
}
|
||||
|
||||
public static async Task HandleAdviseRunAsync(
|
||||
IServiceProvider services,
|
||||
AdvisoryAiTaskType taskType,
|
||||
string advisoryKey,
|
||||
string? artifactId,
|
||||
string? artifactPurl,
|
||||
string? policyVersion,
|
||||
string profile,
|
||||
IReadOnlyList<string> preferredSections,
|
||||
bool forceRefresh,
|
||||
int timeoutSeconds,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("advise-run");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
var previousLevel = verbosity.MinimumLevel;
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
using var activity = CliActivitySource.Instance.StartActivity("cli.advisory.run", ActivityKind.Client);
|
||||
activity?.SetTag("stellaops.cli.command", "advise run");
|
||||
activity?.SetTag("stellaops.cli.task", taskType.ToString());
|
||||
using var duration = CliMetrics.MeasureCommandDuration("advisory run");
|
||||
activity?.SetTag("stellaops.cli.force_refresh", forceRefresh);
|
||||
|
||||
var outcome = "error";
|
||||
try
|
||||
{
|
||||
var normalizedKey = advisoryKey?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedKey))
|
||||
{
|
||||
throw new ArgumentException("Advisory key is required.", nameof(advisoryKey));
|
||||
}
|
||||
|
||||
activity?.SetTag("stellaops.cli.advisory.key", normalizedKey);
|
||||
var normalizedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile.Trim();
|
||||
activity?.SetTag("stellaops.cli.profile", normalizedProfile);
|
||||
|
||||
var normalizedSections = NormalizeSections(preferredSections);
|
||||
|
||||
var request = new AdvisoryPipelinePlanRequestModel
|
||||
{
|
||||
TaskType = taskType,
|
||||
AdvisoryKey = normalizedKey,
|
||||
ArtifactId = string.IsNullOrWhiteSpace(artifactId) ? null : artifactId!.Trim(),
|
||||
ArtifactPurl = string.IsNullOrWhiteSpace(artifactPurl) ? null : artifactPurl!.Trim(),
|
||||
PolicyVersion = string.IsNullOrWhiteSpace(policyVersion) ? null : policyVersion!.Trim(),
|
||||
Profile = normalizedProfile,
|
||||
PreferredSections = normalizedSections.Length > 0 ? normalizedSections : null,
|
||||
ForceRefresh = forceRefresh
|
||||
};
|
||||
|
||||
logger.LogInformation("Requesting advisory plan for {TaskType} (advisory={AdvisoryKey}).", taskType, normalizedKey);
|
||||
|
||||
var plan = await client.CreateAdvisoryPipelinePlanAsync(taskType, request, cancellationToken).ConfigureAwait(false);
|
||||
activity?.SetTag("stellaops.cli.advisory.cache_key", plan.CacheKey);
|
||||
RenderAdvisoryPlan(plan);
|
||||
logger.LogInformation("Plan {CacheKey} queued with {Chunks} chunks and {Vectors} vectors.",
|
||||
plan.CacheKey,
|
||||
plan.Chunks.Count,
|
||||
plan.Vectors.Count);
|
||||
|
||||
var pollDelay = TimeSpan.FromSeconds(1);
|
||||
var shouldWait = timeoutSeconds > 0;
|
||||
var deadline = shouldWait ? DateTimeOffset.UtcNow + TimeSpan.FromSeconds(timeoutSeconds) : DateTimeOffset.UtcNow;
|
||||
|
||||
AdvisoryPipelineOutputModel? output = null;
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
output = await client
|
||||
.TryGetAdvisoryPipelineOutputAsync(plan.CacheKey, taskType, normalizedProfile, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (output is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!shouldWait || DateTimeOffset.UtcNow >= deadline)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogDebug("Advisory output pending for {CacheKey}; retrying in {DelaySeconds}s.", plan.CacheKey, pollDelay.TotalSeconds);
|
||||
await Task.Delay(pollDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (output is null)
|
||||
{
|
||||
logger.LogError("Timed out after {Timeout}s waiting for advisory output (cache key {CacheKey}).",
|
||||
Math.Max(timeoutSeconds, 0),
|
||||
plan.CacheKey);
|
||||
activity?.SetStatus(ActivityStatusCode.Error, "timeout");
|
||||
outcome = "timeout";
|
||||
Environment.ExitCode = Environment.ExitCode == 0 ? 70 : Environment.ExitCode;
|
||||
return;
|
||||
}
|
||||
|
||||
activity?.SetTag("stellaops.cli.advisory.generated_at", output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture));
|
||||
activity?.SetTag("stellaops.cli.advisory.cache_hit", output.PlanFromCache);
|
||||
logger.LogInformation("Advisory output ready (cache key {CacheKey}).", output.CacheKey);
|
||||
|
||||
RenderAdvisoryOutput(output);
|
||||
|
||||
if (output.Guardrail.Blocked)
|
||||
{
|
||||
logger.LogError("Guardrail blocked advisory output (cache key {CacheKey}).", output.CacheKey);
|
||||
activity?.SetStatus(ActivityStatusCode.Error, "guardrail_blocked");
|
||||
outcome = "blocked";
|
||||
Environment.ExitCode = Environment.ExitCode == 0 ? 65 : Environment.ExitCode;
|
||||
return;
|
||||
}
|
||||
|
||||
activity?.SetStatus(ActivityStatusCode.Ok);
|
||||
outcome = output.PlanFromCache ? "cache-hit" : "ok";
|
||||
Environment.ExitCode = 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
outcome = "cancelled";
|
||||
activity?.SetStatus(ActivityStatusCode.Error, "cancelled");
|
||||
Environment.ExitCode = Environment.ExitCode == 0 ? 130 : Environment.ExitCode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
logger.LogError(ex, "Failed to run advisory task.");
|
||||
outcome = "error";
|
||||
Environment.ExitCode = Environment.ExitCode == 0 ? 1 : Environment.ExitCode;
|
||||
}
|
||||
finally
|
||||
{
|
||||
activity?.SetTag("stellaops.cli.advisory.outcome", outcome);
|
||||
CliMetrics.RecordAdvisoryRun(taskType.ToString(), outcome);
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task HandleSourcesIngestAsync(
|
||||
IServiceProvider services,
|
||||
bool dryRun,
|
||||
string source,
|
||||
string input,
|
||||
string? tenantOverride,
|
||||
string format,
|
||||
bool disableColor,
|
||||
@@ -6137,7 +6278,156 @@ internal static class CommandHandlers
|
||||
["ERR_AOC_007"] = 17
|
||||
};
|
||||
|
||||
private static IDictionary<string, object?> RemoveNullValues(Dictionary<string, object?> source)
|
||||
private static string[] NormalizeSections(IReadOnlyList<string> sections)
|
||||
{
|
||||
if (sections is null || sections.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return sections
|
||||
.Where(section => !string.IsNullOrWhiteSpace(section))
|
||||
.Select(section => section.Trim())
|
||||
.Where(section => section.Length > 0)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static void RenderAdvisoryPlan(AdvisoryPipelinePlanResponseModel plan)
|
||||
{
|
||||
var console = AnsiConsole.Console;
|
||||
|
||||
var summary = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Title("[bold]Advisory Plan[/]");
|
||||
summary.AddColumn("Field");
|
||||
summary.AddColumn("Value");
|
||||
summary.AddRow("Task", Markup.Escape(plan.TaskType));
|
||||
summary.AddRow("Cache Key", Markup.Escape(plan.CacheKey));
|
||||
summary.AddRow("Prompt Template", Markup.Escape(plan.PromptTemplate));
|
||||
summary.AddRow("Chunks", plan.Chunks.Count.ToString(CultureInfo.InvariantCulture));
|
||||
summary.AddRow("Vectors", plan.Vectors.Count.ToString(CultureInfo.InvariantCulture));
|
||||
summary.AddRow("Prompt Tokens", plan.Budget.PromptTokens.ToString(CultureInfo.InvariantCulture));
|
||||
summary.AddRow("Completion Tokens", plan.Budget.CompletionTokens.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
console.Write(summary);
|
||||
|
||||
if (plan.Metadata.Count > 0)
|
||||
{
|
||||
console.Write(CreateKeyValueTable("Plan Metadata", plan.Metadata));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderAdvisoryOutput(AdvisoryPipelineOutputModel output)
|
||||
{
|
||||
var console = AnsiConsole.Console;
|
||||
|
||||
var summary = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Title("[bold]Advisory Output[/]");
|
||||
summary.AddColumn("Field");
|
||||
summary.AddColumn("Value");
|
||||
summary.AddRow("Cache Key", Markup.Escape(output.CacheKey));
|
||||
summary.AddRow("Task", Markup.Escape(output.TaskType));
|
||||
summary.AddRow("Profile", Markup.Escape(output.Profile));
|
||||
summary.AddRow("Generated", output.GeneratedAtUtc.ToString("O", CultureInfo.InvariantCulture));
|
||||
summary.AddRow("Plan From Cache", output.PlanFromCache ? "yes" : "no");
|
||||
summary.AddRow("Citations", output.Citations.Count.ToString(CultureInfo.InvariantCulture));
|
||||
summary.AddRow("Guardrail Blocked", output.Guardrail.Blocked ? "[red]yes[/]" : "no");
|
||||
|
||||
console.Write(summary);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(output.Prompt))
|
||||
{
|
||||
var panel = new Panel(new Markup(Markup.Escape(output.Prompt)))
|
||||
{
|
||||
Header = new PanelHeader("Prompt"),
|
||||
Border = BoxBorder.Rounded,
|
||||
Expand = true
|
||||
};
|
||||
console.Write(panel);
|
||||
}
|
||||
|
||||
if (output.Citations.Count > 0)
|
||||
{
|
||||
var citations = new Table()
|
||||
.Border(TableBorder.Minimal)
|
||||
.Title("[grey]Citations[/]");
|
||||
citations.AddColumn("Index");
|
||||
citations.AddColumn("Document");
|
||||
citations.AddColumn("Chunk");
|
||||
|
||||
foreach (var citation in output.Citations.OrderBy(c => c.Index))
|
||||
{
|
||||
citations.AddRow(
|
||||
citation.Index.ToString(CultureInfo.InvariantCulture),
|
||||
Markup.Escape(citation.DocumentId),
|
||||
Markup.Escape(citation.ChunkId));
|
||||
}
|
||||
|
||||
console.Write(citations);
|
||||
}
|
||||
|
||||
if (output.Metadata.Count > 0)
|
||||
{
|
||||
console.Write(CreateKeyValueTable("Output Metadata", output.Metadata));
|
||||
}
|
||||
|
||||
if (output.Guardrail.Metadata.Count > 0)
|
||||
{
|
||||
console.Write(CreateKeyValueTable("Guardrail Metadata", output.Guardrail.Metadata));
|
||||
}
|
||||
|
||||
if (output.Guardrail.Violations.Count > 0)
|
||||
{
|
||||
var violations = new Table()
|
||||
.Border(TableBorder.Minimal)
|
||||
.Title("[red]Guardrail Violations[/]");
|
||||
violations.AddColumn("Code");
|
||||
violations.AddColumn("Message");
|
||||
|
||||
foreach (var violation in output.Guardrail.Violations)
|
||||
{
|
||||
violations.AddRow(Markup.Escape(violation.Code), Markup.Escape(violation.Message));
|
||||
}
|
||||
|
||||
console.Write(violations);
|
||||
}
|
||||
|
||||
var provenance = new Table()
|
||||
.Border(TableBorder.Minimal)
|
||||
.Title("[grey]Provenance[/]");
|
||||
provenance.AddColumn("Field");
|
||||
provenance.AddColumn("Value");
|
||||
|
||||
provenance.AddRow("Input Digest", Markup.Escape(output.Provenance.InputDigest));
|
||||
provenance.AddRow("Output Hash", Markup.Escape(output.Provenance.OutputHash));
|
||||
|
||||
var signatures = output.Provenance.Signatures.Count == 0
|
||||
? "none"
|
||||
: string.Join(Environment.NewLine, output.Provenance.Signatures.Select(Markup.Escape));
|
||||
provenance.AddRow("Signatures", signatures);
|
||||
|
||||
console.Write(provenance);
|
||||
}
|
||||
|
||||
private static Table CreateKeyValueTable(string title, IReadOnlyDictionary<string, string> entries)
|
||||
{
|
||||
var table = new Table()
|
||||
.Border(TableBorder.Minimal)
|
||||
.Title($"[grey]{Markup.Escape(title)}[/]");
|
||||
table.AddColumn("Key");
|
||||
table.AddColumn("Value");
|
||||
|
||||
foreach (var kvp in entries.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
table.AddRow(Markup.Escape(kvp.Key), Markup.Escape(kvp.Value));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private static IDictionary<string, object?> RemoveNullValues(Dictionary<string, object?> source)
|
||||
{
|
||||
foreach (var key in source.Where(kvp => kvp.Value is null).Select(kvp => kvp.Key).ToList())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user