Files
git.stella-ops.org/src/AdvisoryAI/StellaOps.AdvisoryAI.Hosting/ServiceCollectionExtensions.cs
2026-01-13 18:53:39 +02:00

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;
}
}