feat: Enhance Authority Identity Provider Registry with Bootstrap Capability
- Added support for bootstrap providers in AuthorityIdentityProviderRegistry. - Introduced a new property for bootstrap providers and updated AggregateCapabilities. - Updated relevant methods to handle bootstrap capabilities during provider registration. feat: Introduce Sealed Mode Status in OpenIddict Handlers - Added SealedModeStatusProperty to AuthorityOpenIddictConstants. - Enhanced ValidateClientCredentialsHandler, ValidatePasswordGrantHandler, and ValidateRefreshTokenGrantHandler to validate sealed mode evidence. - Implemented logic to handle airgap seal confirmation requirements. feat: Update Program Configuration for Sealed Mode - Registered IAuthoritySealedModeEvidenceValidator in Program.cs. - Added logging for bootstrap capabilities in identity provider plugins. - Implemented checks for bootstrap support in API endpoints. chore: Update Tasks and Documentation - Marked AUTH-MTLS-11-002 as DONE in TASKS.md. - Updated documentation to reflect changes in sealed mode and bootstrap capabilities. fix: Improve CLI Command Handlers Output - Enhanced output formatting for command responses and prompts in CommandHandlers.cs. feat: Extend Advisory AI Models - Added Response property to AdvisoryPipelineOutputModel for better output handling. fix: Adjust Concelier Web Service Authentication - Improved JWT token handling in Concelier Web Service to ensure proper token extraction and logging. test: Enhance Web Service Endpoints Tests - Added detailed logging for authentication failures in WebServiceEndpointsTests. - Enabled PII logging for better debugging of authentication issues. feat: Introduce Air-Gap Configuration Options - Added AuthorityAirGapOptions and AuthoritySealedModeOptions to StellaOpsAuthorityOptions. - Implemented validation logic for air-gap configurations to ensure proper setup.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
@@ -15,6 +16,8 @@ public sealed class AdvisoryAiServiceOptions
|
||||
|
||||
public AdvisoryAiStorageOptions Storage { get; set; } = new();
|
||||
|
||||
public AdvisoryAiInferenceOptions Inference { get; set; } = new();
|
||||
|
||||
internal string ResolveQueueDirectory(string contentRoot)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(contentRoot);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
@@ -52,6 +53,24 @@ internal static class AdvisoryAiServiceOptionsValidator
|
||||
options.Storage.OutputDirectory = Path.Combine("data", "advisory-ai", "outputs");
|
||||
}
|
||||
|
||||
options.Inference ??= new AdvisoryAiInferenceOptions();
|
||||
options.Inference.Remote ??= new AdvisoryAiRemoteInferenceOptions();
|
||||
|
||||
if (options.Inference.Mode == AdvisoryAiInferenceMode.Remote)
|
||||
{
|
||||
var remote = options.Inference.Remote;
|
||||
if (remote.BaseAddress is null || !remote.BaseAddress.IsAbsoluteUri)
|
||||
{
|
||||
error = "AdvisoryAI:Inference:Remote:BaseAddress must be an absolute URI when remote mode is enabled.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remote.Timeout <= TimeSpan.Zero)
|
||||
{
|
||||
remote.Timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ internal sealed class FileSystemAdvisoryOutputStore : IAdvisoryOutputStore
|
||||
AdvisoryTaskType TaskType,
|
||||
string Profile,
|
||||
string Prompt,
|
||||
string Response,
|
||||
List<AdvisoryPromptCitation> Citations,
|
||||
Dictionary<string, string> Metadata,
|
||||
GuardrailEnvelope Guardrail,
|
||||
@@ -114,6 +115,7 @@ internal sealed class FileSystemAdvisoryOutputStore : IAdvisoryOutputStore
|
||||
output.TaskType,
|
||||
output.Profile,
|
||||
output.Prompt,
|
||||
output.Response,
|
||||
output.Citations.ToList(),
|
||||
output.Metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal),
|
||||
GuardrailEnvelope.FromResult(output.Guardrail),
|
||||
@@ -132,6 +134,7 @@ internal sealed class FileSystemAdvisoryOutputStore : IAdvisoryOutputStore
|
||||
TaskType,
|
||||
Profile,
|
||||
Prompt,
|
||||
Response,
|
||||
citations,
|
||||
metadata,
|
||||
guardrail,
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Caching;
|
||||
using StellaOps.AdvisoryAI.DependencyInjection;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Outputs;
|
||||
using StellaOps.AdvisoryAI.Providers;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
using StellaOps.AdvisoryAI.Outputs;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAdvisoryAiCore(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
Action<AdvisoryAiServiceOptions>? configure = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services.AddOptions<AdvisoryAiServiceOptions>()
|
||||
.Bind(configuration.GetSection("AdvisoryAI"))
|
||||
public static IServiceCollection AddAdvisoryAiCore(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
Action<AdvisoryAiServiceOptions>? configure = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
services.AddOptions<AdvisoryAiServiceOptions>()
|
||||
.Bind(configuration.GetSection("AdvisoryAI"))
|
||||
.PostConfigure(options =>
|
||||
{
|
||||
configure?.Invoke(options);
|
||||
AdvisoryAiServiceOptionsValidator.Validate(options);
|
||||
})
|
||||
.ValidateOnStart();
|
||||
|
||||
|
||||
services.AddOptions<SbomContextClientOptions>()
|
||||
.Configure<IOptions<AdvisoryAiServiceOptions>>((target, source) =>
|
||||
{
|
||||
@@ -40,6 +43,45 @@ public static class ServiceCollectionExtensions
|
||||
})
|
||||
.Validate(opt => opt.BaseAddress is null || opt.BaseAddress.IsAbsoluteUri, "SBOM base address must be absolute when provided.");
|
||||
|
||||
services.AddOptions<AdvisoryAiInferenceOptions>()
|
||||
.Configure<IOptions<AdvisoryAiServiceOptions>>((target, source) =>
|
||||
{
|
||||
var inference = source.Value.Inference ?? new AdvisoryAiInferenceOptions();
|
||||
target.Mode = inference.Mode;
|
||||
target.Remote = inference.Remote ?? new AdvisoryAiRemoteInferenceOptions();
|
||||
});
|
||||
|
||||
services.AddHttpClient<RemoteAdvisoryInferenceClient>((provider, client) =>
|
||||
{
|
||||
var inference = provider.GetRequiredService<IOptions<AdvisoryAiInferenceOptions>>().Value ?? new AdvisoryAiInferenceOptions();
|
||||
var remote = inference.Remote ?? new AdvisoryAiRemoteInferenceOptions();
|
||||
|
||||
if (remote.BaseAddress is not null)
|
||||
{
|
||||
client.BaseAddress = remote.BaseAddress;
|
||||
}
|
||||
|
||||
if (remote.Timeout > TimeSpan.Zero)
|
||||
{
|
||||
client.Timeout = remote.Timeout;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(remote.ApiKey))
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", remote.ApiKey);
|
||||
}
|
||||
});
|
||||
|
||||
services.TryAddSingleton<LocalAdvisoryInferenceClient>();
|
||||
services.TryAddSingleton<RemoteAdvisoryInferenceClient>();
|
||||
services.AddSingleton<IAdvisoryInferenceClient>(provider =>
|
||||
{
|
||||
var inference = provider.GetRequiredService<IOptions<AdvisoryAiInferenceOptions>>().Value ?? new AdvisoryAiInferenceOptions();
|
||||
return inference.Mode == AdvisoryAiInferenceMode.Remote
|
||||
? provider.GetRequiredService<RemoteAdvisoryInferenceClient>()
|
||||
: provider.GetRequiredService<LocalAdvisoryInferenceClient>();
|
||||
});
|
||||
|
||||
services.AddSbomContext();
|
||||
services.AddAdvisoryPipeline();
|
||||
services.AddAdvisoryPipelineInfrastructure();
|
||||
|
||||
@@ -10,6 +10,7 @@ internal sealed record AdvisoryOutputResponse(
|
||||
string TaskType,
|
||||
string Profile,
|
||||
string Prompt,
|
||||
string Response,
|
||||
IReadOnlyList<AdvisoryOutputCitation> Citations,
|
||||
IReadOnlyDictionary<string, string> Metadata,
|
||||
AdvisoryOutputGuardrail Guardrail,
|
||||
@@ -23,6 +24,7 @@ internal sealed record AdvisoryOutputResponse(
|
||||
output.TaskType.ToString(),
|
||||
output.Profile,
|
||||
output.Prompt,
|
||||
output.Response,
|
||||
output.Citations
|
||||
.Select(citation => new AdvisoryOutputCitation(citation.Index, citation.DocumentId, citation.ChunkId))
|
||||
.ToList(),
|
||||
|
||||
@@ -23,7 +23,7 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI_");
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI__");
|
||||
|
||||
builder.Services.AddAdvisoryAiCore(builder.Configuration);
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
@@ -10,7 +10,7 @@ var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(args);
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI_");
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI__");
|
||||
|
||||
builder.Services.AddAdvisoryAiCore(builder.Configuration);
|
||||
builder.Services.AddHostedService<AdvisoryTaskWorker>();
|
||||
|
||||
@@ -8,6 +8,7 @@ using StellaOps.AdvisoryAI.Orchestration;
|
||||
using StellaOps.AdvisoryAI.Prompting;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Execution;
|
||||
|
||||
@@ -27,6 +28,7 @@ internal sealed class AdvisoryPipelineExecutor : IAdvisoryPipelineExecutor
|
||||
private readonly IAdvisoryOutputStore _outputStore;
|
||||
private readonly AdvisoryPipelineMetrics _metrics;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IAdvisoryInferenceClient _inferenceClient;
|
||||
private readonly ILogger<AdvisoryPipelineExecutor>? _logger;
|
||||
|
||||
public AdvisoryPipelineExecutor(
|
||||
@@ -35,6 +37,7 @@ internal sealed class AdvisoryPipelineExecutor : IAdvisoryPipelineExecutor
|
||||
IAdvisoryOutputStore outputStore,
|
||||
AdvisoryPipelineMetrics metrics,
|
||||
TimeProvider timeProvider,
|
||||
IAdvisoryInferenceClient inferenceClient,
|
||||
ILogger<AdvisoryPipelineExecutor>? logger = null)
|
||||
{
|
||||
_promptAssembler = promptAssembler ?? throw new ArgumentNullException(nameof(promptAssembler));
|
||||
@@ -42,6 +45,7 @@ internal sealed class AdvisoryPipelineExecutor : IAdvisoryPipelineExecutor
|
||||
_outputStore = outputStore ?? throw new ArgumentNullException(nameof(outputStore));
|
||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_inferenceClient = inferenceClient ?? throw new ArgumentNullException(nameof(inferenceClient));
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -87,8 +91,9 @@ internal sealed class AdvisoryPipelineExecutor : IAdvisoryPipelineExecutor
|
||||
prompt.Citations.Length,
|
||||
plan.StructuredChunks.Length);
|
||||
|
||||
var inferenceResult = await _inferenceClient.GenerateAsync(plan, prompt, guardrailResult, cancellationToken).ConfigureAwait(false);
|
||||
var generatedAt = _timeProvider.GetUtcNow();
|
||||
var output = AdvisoryPipelineOutput.Create(plan, prompt, guardrailResult, generatedAt, planFromCache);
|
||||
var output = AdvisoryPipelineOutput.Create(plan, prompt, guardrailResult, inferenceResult, generatedAt, planFromCache);
|
||||
await _outputStore.SaveAsync(output, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_metrics.RecordOutputStored(plan.Request.TaskType, planFromCache, guardrailResult.Blocked);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Orchestration;
|
||||
using StellaOps.AdvisoryAI.Prompting;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Inference;
|
||||
|
||||
public interface IAdvisoryInferenceClient
|
||||
{
|
||||
Task<AdvisoryInferenceResult> GenerateAsync(
|
||||
AdvisoryTaskPlan plan,
|
||||
AdvisoryPrompt prompt,
|
||||
AdvisoryGuardrailResult guardrailResult,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record AdvisoryInferenceResult(
|
||||
string Content,
|
||||
string? ModelId,
|
||||
int? PromptTokens,
|
||||
int? CompletionTokens,
|
||||
ImmutableDictionary<string, string> Metadata)
|
||||
{
|
||||
public static AdvisoryInferenceResult FromLocal(string content)
|
||||
=> new(
|
||||
content,
|
||||
"local.prompt-preview",
|
||||
null,
|
||||
null,
|
||||
ImmutableDictionary.Create<string, string>(StringComparer.Ordinal));
|
||||
|
||||
public static AdvisoryInferenceResult FromFallback(string content, string reason, string? details = null)
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
builder["inference.fallback_reason"] = reason;
|
||||
if (!string.IsNullOrWhiteSpace(details))
|
||||
{
|
||||
builder["inference.fallback_details"] = details!;
|
||||
}
|
||||
|
||||
return new AdvisoryInferenceResult(
|
||||
content,
|
||||
"remote.fallback",
|
||||
null,
|
||||
null,
|
||||
builder.ToImmutable());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LocalAdvisoryInferenceClient : IAdvisoryInferenceClient
|
||||
{
|
||||
public Task<AdvisoryInferenceResult> GenerateAsync(
|
||||
AdvisoryTaskPlan plan,
|
||||
AdvisoryPrompt prompt,
|
||||
AdvisoryGuardrailResult guardrailResult,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(prompt);
|
||||
ArgumentNullException.ThrowIfNull(guardrailResult);
|
||||
|
||||
var sanitized = guardrailResult.SanitizedPrompt ?? prompt.Prompt ?? string.Empty;
|
||||
return Task.FromResult(AdvisoryInferenceResult.FromLocal(sanitized));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RemoteAdvisoryInferenceClient : IAdvisoryInferenceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IOptions<AdvisoryAiInferenceOptions> _options;
|
||||
private readonly ILogger<RemoteAdvisoryInferenceClient>? _logger;
|
||||
|
||||
public RemoteAdvisoryInferenceClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<AdvisoryAiInferenceOptions> options,
|
||||
ILogger<RemoteAdvisoryInferenceClient>? logger = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AdvisoryInferenceResult> GenerateAsync(
|
||||
AdvisoryTaskPlan plan,
|
||||
AdvisoryPrompt prompt,
|
||||
AdvisoryGuardrailResult guardrailResult,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plan);
|
||||
ArgumentNullException.ThrowIfNull(prompt);
|
||||
ArgumentNullException.ThrowIfNull(guardrailResult);
|
||||
|
||||
var sanitized = guardrailResult.SanitizedPrompt ?? prompt.Prompt ?? string.Empty;
|
||||
var inferenceOptions = _options.Value ?? new AdvisoryAiInferenceOptions();
|
||||
var remote = inferenceOptions.Remote ?? new AdvisoryAiRemoteInferenceOptions();
|
||||
|
||||
if (remote.BaseAddress is null)
|
||||
{
|
||||
_logger?.LogWarning("Remote inference is enabled but no base address was configured. Falling back to local prompt output.");
|
||||
return AdvisoryInferenceResult.FromLocal(sanitized);
|
||||
}
|
||||
|
||||
var endpoint = string.IsNullOrWhiteSpace(remote.Endpoint)
|
||||
? "/v1/inference"
|
||||
: remote.Endpoint;
|
||||
|
||||
var request = new RemoteInferenceRequest(
|
||||
TaskType: plan.Request.TaskType.ToString(),
|
||||
Profile: plan.Request.Profile,
|
||||
Prompt: sanitized,
|
||||
Metadata: prompt.Metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal),
|
||||
Citations: prompt.Citations
|
||||
.Select(citation => new RemoteInferenceCitation(citation.Index, citation.DocumentId, citation.ChunkId))
|
||||
.ToArray());
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.PostAsJsonAsync(endpoint, request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger?.LogWarning(
|
||||
"Remote inference request failed with status {StatusCode}. Response body: {Body}",
|
||||
response.StatusCode,
|
||||
body);
|
||||
return AdvisoryInferenceResult.FromFallback(sanitized, $"remote_http_{(int)response.StatusCode}", body);
|
||||
}
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<RemoteInferenceResponse>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (payload is null || string.IsNullOrWhiteSpace(payload.Content))
|
||||
{
|
||||
_logger?.LogWarning("Remote inference response was empty. Falling back to sanitized prompt.");
|
||||
return AdvisoryInferenceResult.FromFallback(sanitized, "remote_empty_response");
|
||||
}
|
||||
|
||||
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
if (payload.Metadata is not null)
|
||||
{
|
||||
foreach (var pair in payload.Metadata)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.Key) && pair.Value is not null)
|
||||
{
|
||||
metadataBuilder[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AdvisoryInferenceResult(
|
||||
payload.Content,
|
||||
payload.ModelId,
|
||||
payload.Usage?.PromptTokens,
|
||||
payload.Usage?.CompletionTokens,
|
||||
metadataBuilder.ToImmutable());
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger?.LogWarning("Remote inference timed out before completion. Returning sanitized prompt.");
|
||||
return AdvisoryInferenceResult.FromFallback(sanitized, "remote_timeout");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Remote inference HTTP request failed. Returning sanitized prompt.");
|
||||
return AdvisoryInferenceResult.FromFallback(sanitized, "remote_http_exception", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record RemoteInferenceRequest(
|
||||
string TaskType,
|
||||
string Profile,
|
||||
string Prompt,
|
||||
IReadOnlyDictionary<string, string> Metadata,
|
||||
IReadOnlyList<RemoteInferenceCitation> Citations);
|
||||
|
||||
private sealed record RemoteInferenceCitation(int Index, string DocumentId, string ChunkId);
|
||||
|
||||
private sealed record RemoteInferenceResponse(
|
||||
[property: JsonPropertyName("content")] string Content,
|
||||
[property: JsonPropertyName("modelId")] string? ModelId,
|
||||
[property: JsonPropertyName("usage")] RemoteInferenceUsage? Usage,
|
||||
[property: JsonPropertyName("metadata")] Dictionary<string, string>? Metadata);
|
||||
|
||||
private sealed record RemoteInferenceUsage(
|
||||
[property: JsonPropertyName("promptTokens")] int? PromptTokens,
|
||||
[property: JsonPropertyName("completionTokens")] int? CompletionTokens);
|
||||
}
|
||||
|
||||
public sealed class AdvisoryAiInferenceOptions
|
||||
{
|
||||
public AdvisoryAiInferenceMode Mode { get; set; } = AdvisoryAiInferenceMode.Local;
|
||||
|
||||
public AdvisoryAiRemoteInferenceOptions Remote { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class AdvisoryAiRemoteInferenceOptions
|
||||
{
|
||||
public Uri? BaseAddress { get; set; }
|
||||
|
||||
public string Endpoint { get; set; } = "/v1/inference";
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
public enum AdvisoryAiInferenceMode
|
||||
{
|
||||
Local,
|
||||
Remote
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Prompting;
|
||||
using StellaOps.AdvisoryAI.Orchestration;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Outputs;
|
||||
|
||||
@@ -22,6 +24,7 @@ public sealed class AdvisoryPipelineOutput
|
||||
AdvisoryTaskType taskType,
|
||||
string profile,
|
||||
string prompt,
|
||||
string response,
|
||||
ImmutableArray<AdvisoryPromptCitation> citations,
|
||||
ImmutableDictionary<string, string> metadata,
|
||||
AdvisoryGuardrailResult guardrail,
|
||||
@@ -33,6 +36,7 @@ public sealed class AdvisoryPipelineOutput
|
||||
TaskType = taskType;
|
||||
Profile = string.IsNullOrWhiteSpace(profile) ? throw new ArgumentException(nameof(profile)) : profile;
|
||||
Prompt = prompt ?? throw new ArgumentNullException(nameof(prompt));
|
||||
Response = response ?? throw new ArgumentNullException(nameof(response));
|
||||
Citations = citations;
|
||||
Metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
|
||||
Guardrail = guardrail ?? throw new ArgumentNullException(nameof(guardrail));
|
||||
@@ -49,6 +53,8 @@ public sealed class AdvisoryPipelineOutput
|
||||
|
||||
public string Prompt { get; }
|
||||
|
||||
public string Response { get; }
|
||||
|
||||
public ImmutableArray<AdvisoryPromptCitation> Citations { get; }
|
||||
|
||||
public ImmutableDictionary<string, string> Metadata { get; }
|
||||
@@ -65,15 +71,21 @@ public sealed class AdvisoryPipelineOutput
|
||||
AdvisoryTaskPlan plan,
|
||||
AdvisoryPrompt prompt,
|
||||
AdvisoryGuardrailResult guardrail,
|
||||
AdvisoryInferenceResult inference,
|
||||
DateTimeOffset generatedAtUtc,
|
||||
bool planFromCache)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plan);
|
||||
ArgumentNullException.ThrowIfNull(prompt);
|
||||
ArgumentNullException.ThrowIfNull(guardrail);
|
||||
ArgumentNullException.ThrowIfNull(inference);
|
||||
|
||||
var promptContent = guardrail.SanitizedPrompt ?? prompt.Prompt ?? string.Empty;
|
||||
var outputHash = ComputeHash(promptContent);
|
||||
var responseContent = string.IsNullOrWhiteSpace(inference.Content)
|
||||
? promptContent
|
||||
: inference.Content;
|
||||
var metadata = MergeMetadata(prompt.Metadata, inference);
|
||||
var outputHash = ComputeHash(responseContent);
|
||||
var provenance = new AdvisoryDsseProvenance(plan.CacheKey, outputHash, ImmutableArray<string>.Empty);
|
||||
|
||||
return new AdvisoryPipelineOutput(
|
||||
@@ -81,14 +93,52 @@ public sealed class AdvisoryPipelineOutput
|
||||
plan.Request.TaskType,
|
||||
plan.Request.Profile,
|
||||
promptContent,
|
||||
responseContent,
|
||||
prompt.Citations,
|
||||
prompt.Metadata,
|
||||
metadata,
|
||||
guardrail,
|
||||
provenance,
|
||||
generatedAtUtc,
|
||||
planFromCache);
|
||||
}
|
||||
|
||||
private static ImmutableDictionary<string, string> MergeMetadata(
|
||||
ImmutableDictionary<string, string> metadata,
|
||||
AdvisoryInferenceResult inference)
|
||||
{
|
||||
var builder = metadata is { Count: > 0 }
|
||||
? metadata.ToBuilder()
|
||||
: ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(inference.ModelId))
|
||||
{
|
||||
builder["inference.model_id"] = inference.ModelId!;
|
||||
}
|
||||
|
||||
if (inference.PromptTokens.HasValue)
|
||||
{
|
||||
builder["inference.prompt_tokens"] = inference.PromptTokens.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (inference.CompletionTokens.HasValue)
|
||||
{
|
||||
builder["inference.completion_tokens"] = inference.CompletionTokens.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (inference.Metadata is not null && inference.Metadata.Count > 0)
|
||||
{
|
||||
foreach (var pair in inference.Metadata)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.Key) && pair.Value is not null)
|
||||
{
|
||||
builder[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static string ComputeHash(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| AIAI-31-005 | DONE (2025-11-04) | Advisory AI Guild, Security Guild | AIAI-31-004 | Implement guardrails (redaction, injection defense, output validation, citation enforcement) and fail-safe handling. | Guardrails block adversarial inputs; output validator enforces schemas; security tests pass. |
|
||||
| AIAI-31-006 | DONE (2025-11-04) | Advisory AI Guild | AIAI-31-004..005 | Expose REST API endpoints (`/advisory/ai/*`) with RBAC, rate limits, OpenAPI schemas, and batching support. | Endpoints deployed with schema validation; rate limits enforced; integration tests cover error codes. |
|
||||
| AIAI-31-007 | DONE (2025-11-06) | Advisory AI Guild, Observability Guild | AIAI-31-004..006 | Instrument metrics (`advisory_ai_latency`, `guardrail_blocks`, `validation_failures`, `citation_coverage`), logs, and traces; publish dashboards/alerts. | Telemetry live; dashboards approved; alerts configured. |
|
||||
| AIAI-31-008 | TODO | Advisory AI Guild, DevOps Guild | AIAI-31-006..007 | Package inference on-prem container, remote inference toggle, Helm/Compose manifests, scaling guidance, offline kit instructions. | Deployment docs merged; smoke deploy executed; offline kit updated; feature flags documented. |
|
||||
| AIAI-31-008 | DOING (2025-11-08) | Advisory AI Guild, DevOps Guild | AIAI-31-006..007 | Package inference on-prem container, remote inference toggle, Helm/Compose manifests, scaling guidance, offline kit instructions. | Deployment docs merged; smoke deploy executed; offline kit updated; feature flags documented. |
|
||||
| AIAI-31-010 | DONE (2025-11-02) | Advisory AI Guild | CONCELIER-VULN-29-001, EXCITITOR-VULN-29-001 | Implement Concelier advisory raw document provider mapping CSAF/OSV payloads into structured chunks for retrieval. | Provider resolves content format, preserves metadata, and passes unit tests covering CSAF/OSV cases. |
|
||||
| AIAI-31-011 | DONE (2025-11-02) | Advisory AI Guild | EXCITITOR-LNM-21-201, EXCITITOR-CORE-AOC-19-002 | Implement Excititor VEX document provider to surface structured VEX statements for vector retrieval. | Provider returns conflict-aware VEX chunks with deterministic metadata and tests for representative statements. |
|
||||
| AIAI-31-009 | DONE (2025-11-08) | Advisory AI Guild, QA Guild | AIAI-31-001..006 | Develop unit/golden/property/perf tests, injection harness, and regression suite; ensure determinism with seeded caches. | Test suite green; golden outputs stored; injection tests pass; perf targets documented. |
|
||||
|
||||
@@ -14,6 +14,7 @@ using StellaOps.AdvisoryAI.Prompting;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Tools;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Tests;
|
||||
@@ -30,7 +31,8 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
var guardrail = new StubGuardrailPipeline(blocked: false);
|
||||
var store = new InMemoryAdvisoryOutputStore();
|
||||
using var metrics = new AdvisoryPipelineMetrics(_meterFactory);
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System);
|
||||
var inference = new StubInferenceClient();
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System, inference);
|
||||
|
||||
var message = new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request);
|
||||
await executor.ExecuteAsync(plan, message, planFromCache: false, CancellationToken.None);
|
||||
@@ -43,6 +45,7 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
saved.Provenance.InputDigest.Should().Be(plan.CacheKey);
|
||||
saved.Provenance.OutputHash.Should().NotBeNullOrWhiteSpace();
|
||||
saved.Prompt.Should().Be("{\"prompt\":\"value\"}");
|
||||
saved.Response.Should().Be("{\"prompt\":\"value\"}");
|
||||
saved.Guardrail.Metadata.Should().ContainKey("prompt_length");
|
||||
}
|
||||
|
||||
@@ -54,7 +57,8 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
var guardrail = new StubGuardrailPipeline(blocked: true);
|
||||
var store = new InMemoryAdvisoryOutputStore();
|
||||
using var metrics = new AdvisoryPipelineMetrics(_meterFactory);
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System);
|
||||
var inference = new StubInferenceClient();
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System, inference);
|
||||
|
||||
var message = new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request);
|
||||
await executor.ExecuteAsync(plan, message, planFromCache: true, CancellationToken.None);
|
||||
@@ -84,12 +88,12 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
|
||||
listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
doubleMeasurements.Add((instrument.Name, measurement, tags));
|
||||
doubleMeasurements.Add((instrument.Name, measurement, tags.ToArray()));
|
||||
});
|
||||
|
||||
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
longMeasurements.Add((instrument.Name, measurement, tags));
|
||||
longMeasurements.Add((instrument.Name, measurement, tags.ToArray()));
|
||||
});
|
||||
|
||||
listener.Start();
|
||||
@@ -99,7 +103,8 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
var guardrail = new StubGuardrailPipeline(blocked: true);
|
||||
var store = new InMemoryAdvisoryOutputStore();
|
||||
using var metrics = new AdvisoryPipelineMetrics(_meterFactory);
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System);
|
||||
var inference = new StubInferenceClient();
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System, inference);
|
||||
|
||||
var message = new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request);
|
||||
await executor.ExecuteAsync(plan, message, planFromCache: false, CancellationToken.None);
|
||||
@@ -135,7 +140,7 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
|
||||
listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
doubleMeasurements.Add((instrument.Name, measurement, tags));
|
||||
doubleMeasurements.Add((instrument.Name, measurement, tags.ToArray()));
|
||||
});
|
||||
|
||||
listener.Start();
|
||||
@@ -145,7 +150,8 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
var guardrail = new StubGuardrailPipeline(blocked: false);
|
||||
var store = new InMemoryAdvisoryOutputStore();
|
||||
using var metrics = new AdvisoryPipelineMetrics(_meterFactory);
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System);
|
||||
var inference = new StubInferenceClient();
|
||||
var executor = new AdvisoryPipelineExecutor(assembler, guardrail, store, metrics, TimeProvider.System, inference);
|
||||
|
||||
var message = new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request);
|
||||
await executor.ExecuteAsync(plan, message, planFromCache: false, CancellationToken.None);
|
||||
@@ -289,6 +295,18 @@ public sealed class AdvisoryPipelineExecutorTests : IDisposable
|
||||
=> Task.FromResult(_result);
|
||||
}
|
||||
|
||||
private sealed class StubInferenceClient : IAdvisoryInferenceClient
|
||||
{
|
||||
public AdvisoryInferenceResult Result { get; set; } = AdvisoryInferenceResult.FromLocal("{\"prompt\":\"value\"}");
|
||||
|
||||
public Task<AdvisoryInferenceResult> GenerateAsync(
|
||||
AdvisoryTaskPlan plan,
|
||||
AdvisoryPrompt prompt,
|
||||
AdvisoryGuardrailResult guardrailResult,
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.FromResult(Result);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_meterFactory.Dispose();
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class ConcelierAdvisoryDocumentProviderTests
|
||||
|
||||
public Task<IReadOnlyList<AdvisoryRawRecord>> FindByAdvisoryKeyAsync(
|
||||
string tenant,
|
||||
IReadOnlyCollection<string> searchValues,
|
||||
string advisoryKey,
|
||||
IReadOnlyCollection<string> sourceVendors,
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.FromResult(_records);
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Abstractions;
|
||||
using StellaOps.AdvisoryAI.Caching;
|
||||
using StellaOps.AdvisoryAI.Context;
|
||||
using StellaOps.AdvisoryAI.Documents;
|
||||
@@ -14,6 +15,7 @@ using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Hosting;
|
||||
using StellaOps.AdvisoryAI.Outputs;
|
||||
using StellaOps.AdvisoryAI.Orchestration;
|
||||
using StellaOps.AdvisoryAI.Prompting;
|
||||
using StellaOps.AdvisoryAI.Tools;
|
||||
using Xunit;
|
||||
|
||||
@@ -67,11 +69,13 @@ public sealed class FileSystemAdvisoryPersistenceTests : IDisposable
|
||||
var plan = CreatePlan("cache-abc");
|
||||
var prompt = "{\"prompt\":\"value\"}";
|
||||
var guardrail = AdvisoryGuardrailResult.Allowed(prompt);
|
||||
var response = "response-text";
|
||||
var output = new AdvisoryPipelineOutput(
|
||||
plan.CacheKey,
|
||||
plan.Request.TaskType,
|
||||
plan.Request.Profile,
|
||||
prompt,
|
||||
response,
|
||||
ImmutableArray.Create(new AdvisoryPromptCitation(1, "doc-1", "chunk-1")),
|
||||
ImmutableDictionary<string, string>.Empty.Add("advisory_key", plan.Request.AdvisoryKey),
|
||||
guardrail,
|
||||
@@ -84,6 +88,7 @@ public sealed class FileSystemAdvisoryPersistenceTests : IDisposable
|
||||
|
||||
reloaded.Should().NotBeNull();
|
||||
reloaded!.Prompt.Should().Be(prompt);
|
||||
reloaded.Response.Should().Be(response);
|
||||
reloaded.Metadata.Should().ContainKey("advisory_key").WhoseValue.Should().Be(plan.Request.AdvisoryKey);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user