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

- 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:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -1,27 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using StellaOps.AdvisoryAI.Guardrails;
using StellaOps.AdvisoryAI.Orchestration;
using StellaOps.AdvisoryAI.Outputs;
namespace StellaOps.AdvisoryAI.WebService.Contracts;
public sealed record AdvisoryOutputResponse(
internal sealed record AdvisoryOutputResponse(
string CacheKey,
AdvisoryTaskType TaskType,
string TaskType,
string Profile,
string OutputHash,
bool GuardrailBlocked,
IReadOnlyCollection<AdvisoryGuardrailViolationResponse> GuardrailViolations,
IReadOnlyDictionary<string, string> GuardrailMetadata,
string Prompt,
IReadOnlyCollection<AdvisoryCitationResponse> Citations,
IReadOnlyList<AdvisoryOutputCitation> Citations,
IReadOnlyDictionary<string, string> Metadata,
AdvisoryOutputGuardrail Guardrail,
AdvisoryOutputProvenance Provenance,
DateTimeOffset GeneratedAtUtc,
bool PlanFromCache);
public sealed record AdvisoryGuardrailViolationResponse(string Code, string Message)
bool PlanFromCache)
{
public static AdvisoryGuardrailViolationResponse From(AdvisoryGuardrailViolation violation)
=> new(violation.Code, violation.Message);
public static AdvisoryOutputResponse FromDomain(AdvisoryPipelineOutput output)
=> new(
output.CacheKey,
output.TaskType.ToString(),
output.Profile,
output.Prompt,
output.Citations
.Select(citation => new AdvisoryOutputCitation(citation.Index, citation.DocumentId, citation.ChunkId))
.ToList(),
output.Metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal),
AdvisoryOutputGuardrail.FromDomain(output.Guardrail),
AdvisoryOutputProvenance.FromDomain(output.Provenance),
output.GeneratedAtUtc,
output.PlanFromCache);
}
public sealed record AdvisoryCitationResponse(int Index, string DocumentId, string ChunkId);
internal sealed record AdvisoryOutputCitation(int Index, string DocumentId, string ChunkId);
internal sealed record AdvisoryOutputGuardrail(
bool Blocked,
string SanitizedPrompt,
IReadOnlyList<AdvisoryOutputGuardrailViolation> Violations,
IReadOnlyDictionary<string, string> Metadata)
{
public static AdvisoryOutputGuardrail FromDomain(AdvisoryGuardrailResult result)
=> new(
result.Blocked,
result.SanitizedPrompt,
result.Violations
.Select(violation => new AdvisoryOutputGuardrailViolation(violation.Code, violation.Message))
.ToList(),
result.Metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal));
}
internal sealed record AdvisoryOutputGuardrailViolation(string Code, string Message);
internal sealed record AdvisoryOutputProvenance(
string InputDigest,
string OutputHash,
IReadOnlyList<string> Signatures)
{
public static AdvisoryOutputProvenance FromDomain(AdvisoryDsseProvenance provenance)
=> new(
provenance.InputDigest,
provenance.OutputHash,
provenance.Signatures.ToArray());
}

View File

@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.RateLimiting;
@@ -9,10 +10,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Caching;
using StellaOps.AdvisoryAI.Diagnostics;
using StellaOps.AdvisoryAI.Hosting;
using StellaOps.AdvisoryAI.Metrics;
using StellaOps.AdvisoryAI.Outputs;
using StellaOps.AdvisoryAI.Orchestration;
using StellaOps.AdvisoryAI.Queue;
using StellaOps.AdvisoryAI.WebService.Contracts;
var builder = WebApplication.CreateBuilder(args);
@@ -72,6 +76,9 @@ app.MapPost("/v1/advisory-ai/pipeline/{taskType}", HandleSinglePlan)
app.MapPost("/v1/advisory-ai/pipeline:batch", HandleBatchPlans)
.RequireRateLimiting("advisory-ai");
app.MapGet("/v1/advisory-ai/outputs/{cacheKey}", HandleGetOutput)
.RequireRateLimiting("advisory-ai");
app.Run();
static async Task<IResult> HandleSinglePlan(
@@ -85,6 +92,10 @@ static async Task<IResult> HandleSinglePlan(
AdvisoryPipelineMetrics pipelineMetrics,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.plan_request", ActivityKind.Server);
activity?.SetTag("advisory.task_type", taskType);
activity?.SetTag("advisory.advisory_key", request.AdvisoryKey);
if (!Enum.TryParse<AdvisoryTaskType>(taskType, ignoreCase: true, out var parsedType))
{
return Results.BadRequest(new { error = $"Unknown task type '{taskType}'." });
@@ -103,6 +114,7 @@ static async Task<IResult> HandleSinglePlan(
var normalizedRequest = request with { TaskType = parsedType };
var taskRequest = normalizedRequest.ToTaskRequest();
var plan = await orchestrator.CreatePlanAsync(taskRequest, cancellationToken).ConfigureAwait(false);
activity?.SetTag("advisory.plan_cache_key", plan.CacheKey);
await planCache.SetAsync(plan.CacheKey, plan, cancellationToken).ConfigureAwait(false);
await taskQueue.EnqueueAsync(new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request), cancellationToken).ConfigureAwait(false);
@@ -125,6 +137,9 @@ static async Task<IResult> HandleBatchPlans(
AdvisoryPipelineMetrics pipelineMetrics,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.plan_batch", ActivityKind.Server);
activity?.SetTag("advisory.batch_size", batchRequest.Requests.Count);
if (batchRequest.Requests.Count == 0)
{
return Results.BadRequest(new { error = "At least one request must be supplied." });
@@ -153,6 +168,12 @@ static async Task<IResult> HandleBatchPlans(
var normalizedRequest = item with { TaskType = parsedType };
var taskRequest = normalizedRequest.ToTaskRequest();
var plan = await orchestrator.CreatePlanAsync(taskRequest, cancellationToken).ConfigureAwait(false);
activity?.AddEvent(new ActivityEvent("advisory.plan.created", tags: new ActivityTagsCollection
{
{ "advisory.task_type", plan.Request.TaskType.ToString() },
{ "advisory.advisory_key", plan.Request.AdvisoryKey },
{ "advisory.plan_cache_key", plan.CacheKey }
}));
await planCache.SetAsync(plan.CacheKey, plan, cancellationToken).ConfigureAwait(false);
await taskQueue.EnqueueAsync(new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request), cancellationToken).ConfigureAwait(false);
@@ -167,6 +188,37 @@ static async Task<IResult> HandleBatchPlans(
return Results.Ok(results);
}
static async Task<IResult> HandleGetOutput(
HttpContext httpContext,
string cacheKey,
string taskType,
string? profile,
IAdvisoryOutputStore outputStore,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(outputStore);
if (!Enum.TryParse<AdvisoryTaskType>(taskType, ignoreCase: true, out var parsedTaskType))
{
return Results.BadRequest(new { error = $"Unknown task type '{taskType}'." });
}
if (!EnsureAuthorized(httpContext, parsedTaskType))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
var resolvedProfile = string.IsNullOrWhiteSpace(profile) ? "default" : profile!.Trim();
var output = await outputStore.TryGetAsync(cacheKey, parsedTaskType, resolvedProfile, cancellationToken)
.ConfigureAwait(false);
if (output is null)
{
return Results.NotFound(new { error = "Output not found." });
}
return Results.Ok(AdvisoryOutputResponse.FromDomain(output));
}
static bool EnsureAuthorized(HttpContext context, AdvisoryTaskType taskType)
{
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))