281 lines
11 KiB
C#
281 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http.Headers;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.AdvisoryAI.Caching;
|
|
using StellaOps.AdvisoryAI.Chat;
|
|
using StellaOps.AdvisoryAI.DependencyInjection;
|
|
using StellaOps.AdvisoryAI.Explanation;
|
|
using StellaOps.AdvisoryAI.Inference;
|
|
using StellaOps.AdvisoryAI.Metrics;
|
|
using StellaOps.AdvisoryAI.Guardrails;
|
|
using StellaOps.AdvisoryAI.Outputs;
|
|
using StellaOps.AdvisoryAI.PolicyStudio;
|
|
using StellaOps.AdvisoryAI.Providers;
|
|
using StellaOps.AdvisoryAI.Queue;
|
|
using StellaOps.AdvisoryAI.Remediation;
|
|
using StellaOps.OpsMemory.Storage;
|
|
|
|
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"))
|
|
.PostConfigure(options =>
|
|
{
|
|
configure?.Invoke(options);
|
|
AdvisoryAiServiceOptionsValidator.Validate(options);
|
|
})
|
|
.ValidateOnStart();
|
|
|
|
services.AddOptions<SbomContextClientOptions>()
|
|
.Configure<IOptions<AdvisoryAiServiceOptions>>((target, source) =>
|
|
{
|
|
var advisoryOptions = source.Value;
|
|
target.BaseAddress = advisoryOptions.SbomBaseAddress;
|
|
target.Tenant = advisoryOptions.SbomTenant;
|
|
target.TenantHeaderName = advisoryOptions.SbomTenantHeaderName;
|
|
})
|
|
.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();
|
|
|
|
services.AddOptions<AdvisoryGuardrailOptions>()
|
|
.Configure<IOptions<AdvisoryAiServiceOptions>, IHostEnvironment>((options, aiOptions, environment) =>
|
|
{
|
|
ApplyGuardrailConfiguration(options, aiOptions.Value.Guardrails, environment);
|
|
});
|
|
|
|
// Register deterministic providers (allow test injection)
|
|
services.TryAddSingleton<IGuidProvider>(SystemGuidProvider.Instance);
|
|
services.TryAddSingleton<StellaOps.Determinism.IGuidProvider>(
|
|
StellaOps.Determinism.SystemGuidProvider.Instance);
|
|
services.TryAddSingleton(TimeProvider.System);
|
|
|
|
services.Replace(ServiceDescriptor.Singleton<IAdvisoryTaskQueue, FileSystemAdvisoryTaskQueue>());
|
|
services.Replace(ServiceDescriptor.Singleton<IAdvisoryPlanCache, FileSystemAdvisoryPlanCache>());
|
|
services.Replace(ServiceDescriptor.Singleton<IAdvisoryOutputStore, FileSystemAdvisoryOutputStore>());
|
|
services.TryAddSingleton<AdvisoryAiMetrics>();
|
|
|
|
// Explanation services (SPRINT_20251226_015_AI_zastava_companion)
|
|
services.TryAddSingleton<IEvidenceRetrievalService, NullEvidenceRetrievalService>();
|
|
services.TryAddSingleton<IExplanationPromptService, DefaultExplanationPromptService>();
|
|
services.TryAddSingleton<IExplanationInferenceClient, NullExplanationInferenceClient>();
|
|
services.TryAddSingleton<ICitationExtractor, NullCitationExtractor>();
|
|
services.TryAddSingleton<IExplanationStore, InMemoryExplanationStore>();
|
|
services.TryAddSingleton<IExplanationGenerator, EvidenceAnchoredExplanationGenerator>();
|
|
|
|
// Remediation services (SPRINT_20251226_016_AI_remedy_autopilot)
|
|
services.TryAddSingleton<IRemediationPlanner, NullRemediationPlanner>();
|
|
|
|
// Policy studio services (SPRINT_20251226_017_AI_policy_copilot)
|
|
services.TryAddSingleton<IPolicyIntentStore, InMemoryPolicyIntentStore>();
|
|
services.TryAddSingleton<IPolicyIntentParser, NullPolicyIntentParser>();
|
|
services.TryAddSingleton<IPolicyRuleGenerator, LatticeRuleGenerator>();
|
|
services.TryAddSingleton<ITestCaseSynthesizer, PropertyBasedTestSynthesizer>();
|
|
|
|
// Chat services (SPRINT_20260107_006_003 CH-005)
|
|
services.AddOptions<ConversationOptions>()
|
|
.Bind(configuration.GetSection("AdvisoryAI:Chat"))
|
|
.ValidateOnStart();
|
|
services.TryAddSingleton<IGuidGenerator, DefaultGuidGenerator>();
|
|
services.TryAddSingleton<IConversationService, ConversationService>();
|
|
services.TryAddSingleton<ConversationContextBuilder>();
|
|
services.TryAddSingleton<ChatPromptAssembler>();
|
|
services.TryAddSingleton<ChatResponseStreamer>();
|
|
services.TryAddSingleton<GroundingValidator>();
|
|
services.TryAddSingleton<ActionProposalParser>();
|
|
|
|
// Action policy gate and audit defaults (SPRINT_20260109_011_004_BE)
|
|
services.AddDefaultActionPolicyIntegration();
|
|
|
|
// Object link resolvers (SPRINT_20260109_011_002 OMCI-005)
|
|
services.TryAddSingleton<ITypedLinkResolver, OpsMemoryLinkResolver>();
|
|
services.TryAddSingleton<IObjectLinkResolver, CompositeObjectLinkResolver>();
|
|
|
|
return services;
|
|
}
|
|
|
|
private static void ApplyGuardrailConfiguration(
|
|
AdvisoryGuardrailOptions target,
|
|
AdvisoryAiGuardrailOptions? source,
|
|
IHostEnvironment? environment)
|
|
{
|
|
if (source is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (source.MaxPromptLength.HasValue && source.MaxPromptLength.Value > 0)
|
|
{
|
|
target.MaxPromptLength = source.MaxPromptLength.Value;
|
|
}
|
|
|
|
target.RequireCitations = source.RequireCitations;
|
|
|
|
if (source.EntropyThreshold.HasValue && source.EntropyThreshold.Value >= 0)
|
|
{
|
|
target.EntropyThreshold = source.EntropyThreshold.Value;
|
|
}
|
|
|
|
if (source.EntropyMinLength.HasValue && source.EntropyMinLength.Value >= 0)
|
|
{
|
|
target.EntropyMinLength = source.EntropyMinLength.Value;
|
|
}
|
|
|
|
var defaults = target.BlockedPhrases.ToList();
|
|
var merged = new SortedSet<string>(defaults, StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (source.BlockedPhrases is { Count: > 0 })
|
|
{
|
|
foreach (var phrase in source.BlockedPhrases)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(phrase))
|
|
{
|
|
merged.Add(phrase.Trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(source.BlockedPhraseFile))
|
|
{
|
|
var resolvedPath = ResolveGuardrailPath(source.BlockedPhraseFile!, environment);
|
|
foreach (var phrase in GuardrailPhraseLoader.Load(resolvedPath))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(phrase))
|
|
{
|
|
merged.Add(phrase.Trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (merged.Count > 0)
|
|
{
|
|
target.BlockedPhrases.Clear();
|
|
foreach (var phrase in merged)
|
|
{
|
|
target.BlockedPhrases.Add(phrase);
|
|
}
|
|
}
|
|
|
|
var allowlistDefaults = target.AllowlistPatterns.ToList();
|
|
var allowlist = new SortedSet<string>(allowlistDefaults, StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (source.AllowlistPatterns is { Count: > 0 })
|
|
{
|
|
foreach (var pattern in source.AllowlistPatterns)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(pattern))
|
|
{
|
|
allowlist.Add(pattern.Trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(source.AllowlistFile))
|
|
{
|
|
var resolvedPath = ResolveGuardrailPath(source.AllowlistFile!, environment);
|
|
foreach (var pattern in GuardrailAllowlistLoader.Load(resolvedPath))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(pattern))
|
|
{
|
|
allowlist.Add(pattern.Trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allowlist.Count > 0)
|
|
{
|
|
target.AllowlistPatterns.Clear();
|
|
foreach (var pattern in allowlist)
|
|
{
|
|
target.AllowlistPatterns.Add(pattern);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string ResolveGuardrailPath(string configuredPath, IHostEnvironment? environment)
|
|
{
|
|
var trimmed = configuredPath.Trim();
|
|
if (Path.IsPathRooted(trimmed))
|
|
{
|
|
if (!File.Exists(trimmed))
|
|
{
|
|
throw new FileNotFoundException($"Guardrail phrase file {trimmed} was not found.", trimmed);
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
var root = environment?.ContentRootPath;
|
|
if (string.IsNullOrWhiteSpace(root))
|
|
{
|
|
root = AppContext.BaseDirectory;
|
|
}
|
|
|
|
var resolved = Path.GetFullPath(Path.Combine(root!, trimmed));
|
|
if (!File.Exists(resolved))
|
|
{
|
|
throw new FileNotFoundException($"Guardrail phrase file {resolved} was not found.", resolved);
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
}
|